From 946fc6f16abc78de22365422724443e134f3201b Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 29 Apr 2026 12:48:59 +0200
Subject: [PATCH 01/44] adding new config and settings
---
src/config.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 113 insertions(+)
create mode 100644 src/config.py
diff --git a/src/config.py b/src/config.py
new file mode 100644
index 0000000..2567a16
--- /dev/null
+++ b/src/config.py
@@ -0,0 +1,113 @@
+import os
+from pathlib import Path
+
+from PySide6.QtCore import QByteArray, QSettings
+
+
+class SETSConfig():
+
+ __slots__ = ('autosave_filename', 'box_height', 'box_width', 'config_dir', 'config_subfolders',
+ 'home_dir', 'link_discord', 'link_downloads', 'link_github', 'link_website',
+ 'settings_file', 'ui_scale')
+
+ def __init__(self):
+ self.autosave_filename: str = 'autosave.json'
+ self.box_height: int = 64
+ self.box_width: int = 49
+ self.config_dir: Path = Path()
+ self.config_subfolders: dict[str, Path | None] = {
+ 'library': None,
+ 'cache': None,
+ 'cargo': None,
+ 'images': None,
+ 'ship_images': None,
+ 'backups': None,
+ 'auto_backups': None
+ }
+ self.home_dir: Path = Path()
+ self.link_discord: str = 'https://discord.gg/kxwHxbsqzF'
+ self.link_downloads: str = 'https://github.com/STOCD/SETS/releases'
+ self.link_github: str = 'https://github.com/STOCD'
+ self.link_website: str = 'https://stobuilds.com/apps/sets'
+ self.settings_file: str = 'SETS_settings.ini'
+ self.ui_scale: float = 1.0
+
+ def __repr__(self):
+ return f''
+
+
+class SETSSettings():
+
+ __slots__ = ('_settings', 'default_mark', 'default_save_format', 'default_rarity',
+ 'picker_relative', 'pref_backup', 'state__geometry', 'ui_scale')
+
+ def __init__(self, settings_file_path: Path):
+ self.default_mark: str = ''
+ self.default_save_format: str = 'JSON',
+ self.default_rarity: str = 'Common',
+ self.picker_relative: int = 0
+ self.pref_backup: int = 0
+
+ self.state__geometry: QByteArray = QByteArray()
+
+ if os.name == 'nt':
+ self._settings = QSettings(str(settings_file_path), QSettings.Format.IniFormat)
+ else:
+ self._settings = QSettings(str(settings_file_path), QSettings.Format.NativeFormat)
+
+ self.load_settings()
+
+ def load_settings(self):
+ """
+ Loads settings from settings file given in constructor into attributes.
+ """
+ for setting in self.__slots__:
+ if setting.startswith('_'):
+ continue
+ setting_id = setting.replace('__', '/')
+ if self._settings.contains(setting_id):
+ item_type = type(getattr(self, setting))
+ if item_type is list:
+ settings_item: list = getattr(self, setting)
+ if len(settings_item) > 0:
+ list_element_type = type(settings_item[0])
+ else:
+ list_element_type = str
+ item_list = self._settings.value(setting_id, type=list)
+ if list_element_type is bool:
+ items = [True if el == 'true' else False for el in item_list]
+ setattr(self, setting, items)
+ else:
+ setattr(self, setting, [list_element_type(el) for el in item_list])
+ else:
+ setattr(self, setting, self._settings.value(setting_id, type=item_type))
+
+ def store_settings(self):
+ """
+ Stores settings from attributes to settings file given in constructor.
+ """
+ for setting in self.__slots__:
+ if not setting.startswith('_'):
+ setting_id = setting.replace('__', '/')
+ self._settings.setValue(setting_id, getattr(self, setting))
+
+ def set(self, setting_name: str, value):
+ """
+ Sets setting `setting_name` to `value`. Only use when direct assignment cannot be used
+ (e.g. inside a lambda function).
+ """
+ setattr(self, setting_name, value)
+
+ def set_ui_scale(self, new_value: int) -> str:
+ """
+ Calculates `new_value` / 50 and stores it to `ui_scale`. Returns the calculated value.
+
+ Parameters:
+ - :param new_value: 50 times the ui scale percentage
+ """
+ setting_value = round(new_value / 50, 2)
+ self.ui_scale = setting_value
+ return f'{setting_value:.2f}'
+
+ def __repr__(self):
+ return f''
From 46ca8e5e9fa263cd1ef239f31cc1d5a436eebe96 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 29 Apr 2026 15:07:44 +0200
Subject: [PATCH 02/44] integrating new settings and config
---
src/app.py | 208 ++++++++++++++++++++++++-------------------
src/callbacks.py | 26 ++----
src/cargomanager.py | 4 +-
src/config.py | 14 +--
src/datafunctions.py | 22 ++---
src/iofunc.py | 23 +++--
src/style.py | 6 +-
src/subwindows.py | 8 +-
src/widgetbuilder.py | 22 ++---
9 files changed, 173 insertions(+), 160 deletions(-)
diff --git a/src/app.py b/src/app.py
index e6ed575..9b76bf2 100644
--- a/src/app.py
+++ b/src/app.py
@@ -6,6 +6,7 @@
from PySide6.QtWidgets import QApplication, QFrame, QPlainTextEdit, QScrollArea, QTabWidget, QWidget
from .cargomanager import CargoManager
+from .config import SETSConfig, SETSSettings
from .constants import (
ABOTTOM, ACENTER, AHCENTER, ALEFT, ARIGHT, ATOP, AVCENTER, CAREERS, FACTIONS, MARKS,
PRIMARY_SPECS, RARITIES, SCROLLOFF, SCROLLON, SECONDARY_SPECS, SMAXMAX, SMAXMIN, SMINMAX,
@@ -32,7 +33,7 @@ class SETS():
clear_all, clear_slot, clear_build_callback, copy_equipment_item, edit_equipment_item,
elite_callback, faction_combo_callback, load_build_callback, load_skills_callback,
open_wiki_context, paste_equipment_item, save_build_callback, save_skills_callback,
- select_ship, set_build_item, set_ui_scale_setting, ship_info_callback,
+ select_ship, set_build_item, ship_info_callback,
skill_unlock_callback, spec_combo_callback, species_combo_callback, switch_main_tab,
tier_callback)
from .datafunctions import (
@@ -54,11 +55,7 @@ class SETS():
# (release version, dev version)
versions = ('', '')
# see main.py for contents
- config = {}
- # see main.py for contents
theme = {}
- # see main.py for defaults
- settings: QSettings
# stores widgets that need to be accessed from outside their creating function
widgets: WidgetStorage
# stores refined cargo data
@@ -98,17 +95,19 @@ def __init__(self, theme, args, path, config, versions):
self.config = config
self.widgets = WidgetStorage()
self.cache = Cache()
- self.init_settings()
+ self.config: SETSConfig = SETSConfig()
+ self.config.config_dir = self.get_config_dir_path()
+ self.settings = SETSSettings(self.config.config_dir / self.config.settings_file)
self.init_config()
self.prepare_tooltip_css()
self.init_environment()
self.downloader = Downloader(
- self.config['config_subfolders']['images'],
- self.config['config_subfolders']['ship_images'])
- self.cargo: CargoManager = CargoManager(self.config['config_subfolders'])
+ self.config.config_subfolders['images'],
+ self.config.config_subfolders['ship_images'])
+ self.cargo: CargoManager = CargoManager(self.config.config_subfolders)
self.images: ImageManager = ImageManager(
- Path(self.config['config_subfolders']['images']),
- Path(self.config['config_subfolders']['ship_images']),
+ Path(self.config.config_subfolders['images']),
+ Path(self.config.config_subfolders['ship_images']),
self.cargo, self.downloader)
self.app, self.window = self.create_main_window()
self.cache_icons()
@@ -119,8 +118,8 @@ def __init__(self, theme, args, path, config, versions):
self.setup_main_layout()
self.picker_window = Picker(
self, self.window,
- default_rarity_getter=lambda: self.settings.value('default_rarity'),
- default_mark_getter=lambda: self.settings.value('default_mark'))
+ default_rarity_getter=lambda: self.settings.default_rarity,
+ default_mark_getter=lambda: self.settings.default_mark)
self.edit_window = ItemEditor(self, self.window)
self.ship_selector_window = ShipSelector(self, self.window)
self.context_menu = self.create_context_menu()
@@ -135,45 +134,70 @@ def run(self) -> int:
"""
return self.app.exec()
- def init_settings(self):
+ def setup_config_dir(self, dir_path: Path) -> None | OSError:
+ """
+ Sets up config directory.
+ """
+ try:
+ dir_path.mkdir(exist_ok=True)
+ for folder in self.config.config_subfolders:
+ folder_path = dir_path / folder
+ folder_path.mkdir(exist_ok=True)
+ self.config.config_subfolders[folder] = folder_path
+ except OSError as e:
+ return e
+
+ def get_config_dir_path(self, override: str | None = None) -> Path | None:
"""
- Prepares settings. Loads stored settings. Saves current settings for next startup.
+ Identifies appropriate config directory and returns path to that directory. Returns `None`
+ if no usable config dir could be identified.
"""
- settings_path = os.path.abspath(os.path.join(self.app_dir, self.config['settings_path']))
- self.settings = QSettings(settings_path, QSettings.Format.IniFormat)
- for setting, value in self.config['default_settings'].items():
- if self.settings.value(setting, None) is None:
- self.settings.setValue(setting, value)
+ if override is not None:
+ config_dir = Path(override)
+ if self.setup_config_dir(config_dir) is None:
+ return config_dir
+ else:
+ return
+
+ if os.name == 'nt':
+ for env_name in ('APPDATA', 'USERPROFILE'):
+ config_basedir = os.getenv(env_name)
+ if config_basedir is not None:
+ config_dir = Path(config_basedir, 'SETS')
+ if self.setup_config_dir(config_dir) is None:
+ return config_dir
+ else:
+ config_basedir = os.getenv('XDG_CONFIG_HOME')
+ if config_basedir is not None:
+ config_dir = Path(config_basedir, 'SETS')
+ if self.setup_config_dir(config_dir) is None:
+ return config_dir
+ home_dir = os.getenv('HOME')
+ if home_dir is None:
+ return
+ config_dir = Path(home_dir, '.config', 'SETS')
+ if self.setup_config_dir(config_dir) is None:
+ return config_dir
+ config_dir = Path(home_dir, '.sets')
+ if self.setup_config_dir(config_dir) is None:
+ return config_dir
def init_config(self):
"""
Prepares config.
"""
- config_folder = os.path.abspath(os.path.join(
- self.app_dir, self.config['config_folder_path']))
- self.config['config_folder_path'] = config_folder
- for folder, path in self.config['config_subfolders'].items():
- self.config['config_subfolders'][folder] = os.path.join(config_folder, path)
- self.config['autosave_filename'] = os.path.join(
- config_folder, self.config['autosave_filename'])
- self.config['ui_scale'] = self.settings.value('ui_scale', type=float)
- self.box_width = self.config['box_width'] * self.config['ui_scale'] * 0.8
- self.box_height = self.config['box_height'] * self.config['ui_scale'] * 0.8
+ self.config.autosave_path = self.config.config_dir / self.config.autosave_filename
+ self.config.ui_scale = self.settings.ui_scale
+ # TODO move these to new theme
+ self.box_width = self.config.box_width * self.config.ui_scale * 0.8
+ self.box_height = self.config.box_height * self.config.ui_scale * 0.8
def init_environment(self):
"""
- Creates required folders if necessary.
+ Creates external files before starting the app.
"""
- create_folder(self.config['config_folder_path'])
- create_folder(self.config['config_subfolders']['library'])
- create_folder(self.config['config_subfolders']['cache'])
- create_folder(self.config['config_subfolders']['cargo'])
- create_folder(self.config['config_subfolders']['images'])
- create_folder(self.config['config_subfolders']['ship_images'])
- create_folder(self.config['config_subfolders']['backups'])
- create_folder(self.config['config_subfolders']['auto_backups'])
- if not os.path.exists(self.config['autosave_filename']):
- store_json(self.empty_build(), self.config['autosave_filename'])
+ if not self.config.autosave_path.exists():
+ store_json(self.empty_build(), str(self.config.autosave_path))
def cache_icons(self):
"""
@@ -209,7 +233,7 @@ def main_window_close_callback(self, event):
Executed when application is closed.
"""
window_geometry = self.window.saveGeometry()
- self.settings.setValue('geometry', window_geometry)
+ self.settings.state__geometry = window_geometry
self.autosave()
event.accept()
@@ -233,8 +257,8 @@ def create_main_window(self, argv=[]) -> tuple[QApplication, QWidget]:
window = QWidget()
window.setWindowIcon(load_icon('SETS_icon_small.png', self.app_dir))
window.setWindowTitle('STO Equipment and Trait Selector')
- if self.settings.value('geometry'):
- window.restoreGeometry(self.settings.value('geometry'))
+ if self.settings.state__geometry:
+ window.restoreGeometry(self.settings.state__geometry)
window.closeEvent = self.main_window_close_callback
app.focusWindowChanged.connect(self.hide_tooltips)
QThread.currentThread().setPriority(QThread.Priority.TimeCriticalPriority)
@@ -253,7 +277,7 @@ def setup_main_layout(self):
main_layout = VBoxLayout(margins=0, spacing=0)
banner = ImageLabel(get_asset_path('sets_banner.png', self.app_dir), (2880, 126))
main_layout.addWidget(banner)
- frame_width = 8 * self.config['ui_scale']
+ frame_width = 8 * self.config.ui_scale
tabber_layout = VBoxLayout(margins=frame_width, spacing=0)
splash_tabber = QTabWidget()
splash_tabber.setStyleSheet(self.get_style_class('QTabWidget', 'tabber'))
@@ -273,7 +297,7 @@ def setup_main_layout(self):
content_layout.setColumnStretch(0, 1)
content_layout.setColumnStretch(1, 4)
- margin = 3 * self.config['ui_scale']
+ margin = 3 * self.config.ui_scale
menu_layout = GridLayout(margins=(margin, margin, margin, 0), spacing=0)
menu_layout.setColumnStretch(0, 2)
menu_layout.setColumnStretch(1, 5)
@@ -345,7 +369,7 @@ def setup_main_layout(self):
seperator = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@sets', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- seperator.setFixedWidth(self.theme['defaults']['sep'] * self.config['ui_scale'])
+ seperator.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
sidebar_layout.addWidget(seperator, 0, 1, 2, 1)
sidebar.setLayout(sidebar_layout)
content_layout.addWidget(sidebar, 1, 0)
@@ -374,7 +398,7 @@ def setup_ship_frame(self):
Creates ship info frame
"""
frame = self.widgets.sidebar_frames[0]
- csp = self.theme['defaults']['csp'] * self.config['ui_scale']
+ csp = self.theme['defaults']['csp'] * self.config.ui_scale
layout = VBoxLayout(margins=csp, spacing=csp)
image_frame = self.create_frame(size_policy=SMINMIN)
@@ -454,7 +478,7 @@ def setup_space_build_frame(self):
Creates space build layout
"""
frame = self.widgets.build_frames[0]
- isp = self.theme['defaults']['isp'] * 2 * self.config['ui_scale']
+ isp = self.theme['defaults']['isp'] * 2 * self.config.ui_scale
layout = GridLayout(margins=isp, spacing=isp)
layout.setColumnStretch(0, 1)
layout.setColumnStretch(10, 1)
@@ -476,7 +500,7 @@ def setup_space_build_frame(self):
layout.addLayout(hangar_layout, 4, 1, alignment=ALEFT)
sep1 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep1.setFixedWidth(self.theme['defaults']['sep'] * self.config['ui_scale'])
+ sep1.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
layout.addWidget(sep1, 0, 2, 5, 1)
deflector_layout = self.create_build_section('Deflector', 1, 'space', 'deflector', True)
@@ -492,7 +516,7 @@ def setup_space_build_frame(self):
layout.addLayout(shield_layout, 4, 3, alignment=ALEFT)
sep2 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep2.setFixedWidth(self.theme['defaults']['sep'] * self.config['ui_scale'])
+ sep2.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
layout.addWidget(sep2, 0, 4, 5, 1)
uni_layout = self.create_build_section(
@@ -509,7 +533,7 @@ def setup_space_build_frame(self):
layout.addLayout(tac_layout, 3, 5, alignment=ALEFT)
sep3 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep3.setFixedWidth(self.theme['defaults']['sep'] * self.config['ui_scale'])
+ sep3.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
layout.addWidget(sep3, 0, 6, 5, 1)
# Boffs
@@ -549,7 +573,7 @@ def setup_space_build_frame(self):
layout.addLayout(trait_layout, 0, 9, 6, 1, alignment=ATOP)
# Doffs
- spacing = self.theme['defaults']['bw'] * self.config['ui_scale']
+ spacing = self.theme['defaults']['bw'] * self.config.ui_scale
doff_container = self.create_frame(size_policy=SMINMAX)
doff_container_layout = VBoxLayout(spacing=spacing * 2)
doff_label = self.create_label('Space Duty Officers')
@@ -572,7 +596,7 @@ def setup_ground_build_frame(self):
Creates Ground build frame
"""
frame = self.widgets.build_frames[1]
- isp = self.theme['defaults']['isp'] * 2 * self.config['ui_scale']
+ isp = self.theme['defaults']['isp'] * 2 * self.config.ui_scale
layout = GridLayout(margins=isp, spacing=isp)
layout.setColumnStretch(0, 1)
layout.setColumnStretch(8, 1)
@@ -587,7 +611,7 @@ def setup_ground_build_frame(self):
layout.addLayout(devices_layout, 2, 1, alignment=ALEFT)
sep1 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep1.setFixedWidth(self.theme['defaults']['sep'] * self.config['ui_scale'])
+ sep1.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
layout.addWidget(sep1, 0, 2)
kit_layout = self.create_build_section('Kit Frame:', 1, 'ground', 'kit', True)
layout.addLayout(kit_layout, 0, 3, alignment=ALEFT)
@@ -599,7 +623,7 @@ def setup_ground_build_frame(self):
layout.addLayout(shield_layout, 3, 3, alignment=ALEFT)
sep2 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep2.setFixedWidth(self.theme['defaults']['sep'] * self.config['ui_scale'])
+ sep2.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
layout.addWidget(sep2, 0, 4)
# Boffs
@@ -613,7 +637,7 @@ def setup_ground_build_frame(self):
layout.addLayout(boff_4_layout, 3, 5, alignment=ALEFT)
sep3 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep3.setFixedWidth(self.theme['defaults']['sep'] * self.config['ui_scale'])
+ sep3.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
layout.addWidget(sep3, 0, 6)
# Traits
@@ -628,7 +652,7 @@ def setup_ground_build_frame(self):
layout.addLayout(trait_layout, 0, 7, 4, 1, alignment=ATOP)
# Doffs
- spacing = self.theme['defaults']['bw'] * self.config['ui_scale']
+ spacing = self.theme['defaults']['bw'] * self.config.ui_scale
doff_container = self.create_frame(size_policy=SMINMAX)
doff_container_layout = VBoxLayout(spacing=spacing * 2)
doff_label = self.create_label('Ground Duty Officers')
@@ -648,7 +672,7 @@ def setup_ground_build_frame(self):
# sidebar
sidebar_frame = self.widgets.sidebar_frames[1]
- csp = self.theme['defaults']['csp'] * self.config['ui_scale']
+ csp = self.theme['defaults']['csp'] * self.config.ui_scale
sidebar_layout = GridLayout(margins=(csp, isp, csp, csp), spacing=csp)
sidebar_layout.setColumnStretch(0, 1)
desc_label = self.create_label('Build Description:')
@@ -667,12 +691,12 @@ def setup_character_frame(self, frame: QFrame):
"""
Creates character customization area.
"""
- csp = self.theme['defaults']['csp'] * self.config['ui_scale']
+ csp = self.theme['defaults']['csp'] * self.config.ui_scale
layout = GridLayout(margins=csp, spacing=csp)
layout.setColumnStretch(1, 1)
seperator = self.create_frame(size_policy=SMINMAX, style_override={
'background-color': '@sets', 'margin': '@isp'})
- sep = self.theme['defaults']['sep'] * self.config['ui_scale']
+ sep = self.theme['defaults']['sep'] * self.config.ui_scale
seperator.setFixedHeight(sep)
layout.addWidget(seperator, 0, 0, 1, 2, alignment=ATOP) # ATOP makes it respect the margin?
char_name = self.create_entry(placeholder='NAME')
@@ -733,8 +757,8 @@ def setup_space_skill_frame(self):
Creates Space skill GUI
"""
frame = self.widgets.build_frames[2]
- isp = self.theme['defaults']['isp'] * self.config['ui_scale']
- csp = self.theme['defaults']['csp'] * self.config['ui_scale']
+ isp = self.theme['defaults']['isp'] * self.config.ui_scale
+ csp = self.theme['defaults']['csp'] * self.config.ui_scale
col_layout = GridLayout(margins=isp, spacing=csp)
col_layout.setRowStretch(0, 1)
col_layout.setColumnStretch(0, 3)
@@ -762,7 +786,7 @@ def setup_space_skill_frame(self):
'Captain
(25 points required)',
'Admiral
(35 points required)'
)
- sep_height = self.theme['hr']['height'] * self.config['ui_scale']
+ sep_height = self.theme['hr']['height'] * self.config.ui_scale
for rank, skill_groups in enumerate(self.cache.skills['space']):
header_layout = GridLayout(spacing=isp)
left_sep = self.create_frame('hr', size_policy=SMINMAX)
@@ -788,7 +812,7 @@ def setup_space_skill_frame(self):
scroll_area.setWidget(scroll_frame)
seperator = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@sets'})
- seperator.setFixedWidth(self.theme['defaults']['sep'] * self.config['ui_scale'])
+ seperator.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
col_layout.addWidget(seperator, 0, 1)
bonus_bar_container = self.create_frame(size_policy=SMINMIN)
# bonus bars
@@ -846,8 +870,8 @@ def setup_ground_skill_frame(self):
Creates Ground skill GUI
"""
frame = self.widgets.build_frames[3]
- isp = self.theme['defaults']['isp'] * self.config['ui_scale']
- csp = self.theme['defaults']['csp'] * self.config['ui_scale']
+ isp = self.theme['defaults']['isp'] * self.config.ui_scale
+ csp = self.theme['defaults']['csp'] * self.config.ui_scale
col_layout = GridLayout(margins=isp, spacing=csp)
col_layout.setRowStretch(0, 1)
col_layout.setColumnStretch(0, 3)
@@ -901,7 +925,7 @@ def setup_ground_skill_frame(self):
tree_frame.setLayout(tree_layout)
seperator = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@sets'})
- seperator.setFixedWidth(self.theme['defaults']['sep'] * self.config['ui_scale'])
+ seperator.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
col_layout.addWidget(seperator, 0, 1)
bonus_bar_container = self.create_frame(size_policy=SMINMIN)
# bonus bars
@@ -994,7 +1018,7 @@ def setup_settings_frame(self):
Populates the settings frame.
"""
settings_frame = self.widgets.build_frames[5]
- isp = self.theme['defaults']['isp'] * self.config['ui_scale']
+ isp = self.theme['defaults']['isp'] * self.config.ui_scale
settings_layout = HBoxLayout(margins=(2 * isp, isp, isp, isp), spacing=isp)
scroll_layout = VBoxLayout(margins=(0, isp, 0, 0), spacing=isp)
scroll_layout.setSpacing(isp)
@@ -1018,8 +1042,8 @@ def setup_settings_frame(self):
ui_scale_label = self.create_label('UI Scale')
sec_1.addWidget(ui_scale_label, 0, 0, alignment=ALEFT)
ui_scale_slider = self.create_annotated_slider(
- default_value=round(self.settings.value('ui_scale', type=float) * 50, 0),
- min=25, max=75, callback=self.set_ui_scale_setting)
+ default_value=round(self.settings.ui_scale * 50, 0),
+ min=25, max=75, callback=self.settings.set_ui_scale)
sec_1.addLayout(ui_scale_slider, 0, 2, alignment=ALEFT)
ui_scale_desc = self.create_label('Requires restart.', 'hint_label')
sec_1.addWidget(ui_scale_desc, 0, 4, alignment=ALEFT)
@@ -1027,41 +1051,41 @@ def setup_settings_frame(self):
sec_1.addWidget(mark_label, 1, 0, alignment=ALEFT)
mark_combo = self.create_combo_box(style_override={'font': '@small_text'})
mark_combo.addItems(('',) + MARKS)
- mark_combo.setCurrentText(self.settings.value('default_mark'))
+ mark_combo.setCurrentText(self.settings.default_mark)
mark_combo.currentTextChanged.connect(
- lambda new_mark: self.settings.setValue('default_mark', new_mark))
+ lambda new_mark: self.settings.set('default_mark', new_mark))
sec_1.addWidget(mark_combo, 1, 2, alignment=ALEFT)
rarity_label = self.create_label('Default Rarity')
sec_1.addWidget(rarity_label, 2, 0, alignment=ALEFT)
rarity_combo = self.create_combo_box(style_override={'font': '@small_text'})
rarity_combo.addItems(RARITIES.keys())
- rarity_combo.setCurrentText(self.settings.value('default_rarity'))
+ rarity_combo.setCurrentText(self.settings.default_rarity)
rarity_combo.currentTextChanged.connect(
- lambda new_rarity: self.settings.setValue('default_rarity', new_rarity))
+ lambda new_rarity: self.settings.set('default_rarity', new_rarity))
sec_1.addWidget(rarity_combo, 2, 2, alignment=ALEFT | AVCENTER)
picker_rel_label = self.create_label('Picker Position')
sec_1.addWidget(picker_rel_label, 3, 0, alignment=ALEFT)
picker_rel_combo = self.create_combo_box(style_override={'font': '@small_text'})
picker_rel_combo.addItems(('Absolute', 'Relative'))
- picker_rel_combo.setCurrentIndex(self.settings.value('picker_relative', type=int))
+ picker_rel_combo.setCurrentIndex(self.settings.picker_relative)
picker_rel_combo.currentIndexChanged.connect(
- lambda new_i: self.settings.setValue('picker_relative', new_i))
+ lambda new_i: self.settings.set('picker_relative', new_i))
sec_1.addWidget(picker_rel_combo, 3, 2, alignment=ALEFT | AVCENTER)
picker_rel_label = self.create_label('Default Save Format')
sec_1.addWidget(picker_rel_label, 4, 0, alignment=ALEFT)
picker_rel_combo = self.create_combo_box(style_override={'font': '@small_text'})
picker_rel_combo.addItems(('JSON', 'PNG'))
- picker_rel_combo.setCurrentText(self.settings.value('default_save_format'))
+ picker_rel_combo.setCurrentText(self.settings.default_save_format)
picker_rel_combo.currentTextChanged.connect(
- lambda new_t: self.settings.setValue('default_save_format', new_t))
+ lambda new_t: self.settings.set('default_save_format', new_t))
sec_1.addWidget(picker_rel_combo, 4, 2, alignment=ALEFT | AVCENTER)
backup_label = self.create_label('Preferred Backup')
sec_1.addWidget(backup_label, 5, 0, alignment=ALEFT)
backup_combo = self.create_combo_box(style_override={'font': '@small_text'})
backup_combo.addItems(('Auto', 'Manual'))
- backup_combo.setCurrentIndex(self.settings.value('pref_backup', type=int))
+ backup_combo.setCurrentIndex(self.settings.pref_backup)
backup_combo.currentIndexChanged.connect(
- lambda new_i: self.settings.setValue('pref_backup', new_i))
+ lambda new_i: self.settings.set('pref_backup', new_i))
sec_1.addWidget(backup_combo, 5, 2, alignment=ALEFT | AVCENTER)
scroll_layout.addLayout(sec_1)
@@ -1076,14 +1100,14 @@ def setup_settings_frame(self):
sec_2.setColumnStretch(3, 1)
cargo_clear_button = self.create_button('Clear Cargo Data')
cargo_clear_button.clicked.connect(
- lambda: delete_folder_contents(self.config['config_subfolders']['cargo']))
+ lambda: delete_folder_contents(self.config.config_subfolders['cargo']))
sec_2.addWidget(cargo_clear_button, 0, 0, alignment=ALEFT)
cargo_clear_label = self.create_label(
'Clears cargo data. Restart to refresh data.', 'hint_label')
sec_2.addWidget(cargo_clear_label, 0, 2, alignment=ALEFT)
cache_clear_button = self.create_button('Clear Cache')
cache_clear_button.clicked.connect(
- lambda: delete_folder_contents(self.config['config_subfolders']['cache']))
+ lambda: delete_folder_contents(self.config.config_subfolders['cache']))
sec_2.addWidget(cache_clear_button, 1, 0, alignment=ALEFT)
cache_clear_label = self.create_label(
'Clears cache. Restart to rebuild cache.', 'hint_label')
@@ -1119,7 +1143,7 @@ def setup_settings_frame(self):
# sidebar
sidebar_frame = self.widgets.sidebar_frames[5]
- csp = self.theme['defaults']['csp'] * self.config['ui_scale']
+ csp = self.theme['defaults']['csp'] * self.config.ui_scale
sidebar_layout = VBoxLayout(margins=csp, spacing=isp)
sidebar_layout.setAlignment(ATOP)
sidebar_layout.addWidget(self.create_label('About SETS:', 'label_heading'), alignment=ALEFT)
@@ -1133,20 +1157,20 @@ def setup_settings_frame(self):
sidebar_layout.addWidget(about_label)
link_button_style = {
'Website': {
- 'callback': lambda: open_url(self.config['link_website']), 'align': AHCENTER},
+ 'callback': lambda: open_url(self.config.link_website), 'align': AHCENTER},
'Github': {
- 'callback': lambda: open_url(self.config['link_github']), 'align': AHCENTER},
+ 'callback': lambda: open_url(self.config.link_github), 'align': AHCENTER},
'STOBuilds Discord': {
- 'callback': lambda: open_url(self.config['link_discord']), 'align': AHCENTER},
+ 'callback': lambda: open_url(self.config.link_discord), 'align': AHCENTER},
'Downloads': {
- 'callback': lambda: open_url(self.config['link_downloads']), 'align': AHCENTER}
+ 'callback': lambda: open_url(self.config.link_downloads), 'align': AHCENTER}
}
button_layout, buttons = self.create_button_series(
link_button_style, 'button', shape='column', ret=True)
- buttons[0].setToolTip(self.config['link_website'])
- buttons[1].setToolTip(self.config['link_github'])
- buttons[2].setToolTip(self.config['link_discord'])
- buttons[3].setToolTip(self.config['link_downloads'])
+ buttons[0].setToolTip(self.config.link_website)
+ buttons[1].setToolTip(self.config.link_github)
+ buttons[2].setToolTip(self.config.link_discord)
+ buttons[3].setToolTip(self.config.link_downloads)
link_button_frame = self.create_frame()
link_button_frame.setLayout(button_layout)
sidebar_layout.addWidget(link_button_frame, alignment=AHCENTER)
diff --git a/src/callbacks.py b/src/callbacks.py
index d423536..d926e6c 100644
--- a/src/callbacks.py
+++ b/src/callbacks.py
@@ -230,7 +230,7 @@ def picker(
image_suffix = f'__{environment}__{build_key}'
else:
items = []
- if self.settings.value('picker_relative', type=int) == 1:
+ if self.settings.picker_relative == 1:
pos = button.parent().mapToGlobal(button.pos())
else:
pos = None
@@ -467,24 +467,12 @@ def clear_all(self):
self.autosave()
-def set_ui_scale_setting(self, new_value: int):
- """
- Calculates new_value / 50 and stores it to settings.
-
- Parameters:
- - :param new_value: 50 times the ui scale percentage
- """
- setting_value = f'{new_value / 50:.2f}'
- self.settings.setValue('ui_scale', setting_value)
- return setting_value
-
-
def load_build_callback(self):
"""
Loads build from file
"""
load_path = browse_path(
- self, self.config['config_subfolders']['library'],
+ self, str(self.config.config_subfolders['library']),
'SETS Files (*.json *.png);;JSON file (*.json);;PNG image (*.png);;Any File (*.*)')
if load_path != '':
load_build_file(self, load_path)
@@ -495,7 +483,7 @@ def load_skills_callback(self):
Loads skills from file
"""
load_path = browse_path(
- self, self.config['config_subfolders']['library'],
+ self, str(self.config.config_subfolders['library']),
'SETS Files (*.json *.png);;JSON file (*.json);;PNG image (*.png);;Any File (*.*)')
if load_path != '':
load_skill_tree_file(self, load_path)
@@ -511,8 +499,8 @@ def save_build_callback(self):
proposed_filename = f"({self.widgets.ship['button'].text()})"
if self.widgets.ship['name'].text() != '':
proposed_filename = f"{self.widgets.ship['name'].text()} {proposed_filename}"
- default_path = os.path.join(self.config['config_subfolders']['library'], proposed_filename)
- if self.settings.value('default_save_format') == 'PNG':
+ default_path = str(self.config.config_subfolders['library'] / proposed_filename)
+ if self.settings.default_save_format == 'PNG':
file_types = 'PNG image (*.png);;JSON file (*.json);;Any File (*.*)'
else:
file_types = 'JSON file (*.json);;PNG image (*.png);;Any File (*.*)'
@@ -525,8 +513,8 @@ def save_skills_callback(self):
"""
Save skills to file
"""
- default_path = os.path.join(self.config['config_subfolders']['library'], 'Skill Tree')
- if self.settings.value('default_save_format') == 'PNG':
+ default_path = str(self.config.config_subfolders['library'] / 'Skill Tree')
+ if self.settings.default_save_format == 'PNG':
file_types = 'PNG image (*.png);;JSON file (*.json);;Any File (*.*)'
else:
file_types = 'JSON file (*.json);;PNG image (*.png);;Any File (*.*)'
diff --git a/src/cargomanager.py b/src/cargomanager.py
index ea075e5..c2b9487 100644
--- a/src/cargomanager.py
+++ b/src/cargomanager.py
@@ -8,12 +8,12 @@
class CargoManager():
"""Manages Cargo data and cache"""
- def __init__(self, folders: dict[str, str]):
+ def __init__(self, folders: dict[str, Path]):
"""
Parameters:
- :param folders: folder names and paths of config folder
"""
- self._folders: dict[str, Path] = {name: Path(path) for name, path in folders.items()}
+ self._folders: dict[str, Path] = folders
self.boff_abilities: dict[str, dict[str, dict]] = {
'space': self.boff_dict(),
'ground': self.boff_dict(),
diff --git a/src/config.py b/src/config.py
index 2567a16..82595e1 100644
--- a/src/config.py
+++ b/src/config.py
@@ -6,12 +6,13 @@
class SETSConfig():
- __slots__ = ('autosave_filename', 'box_height', 'box_width', 'config_dir', 'config_subfolders',
- 'home_dir', 'link_discord', 'link_downloads', 'link_github', 'link_website',
- 'settings_file', 'ui_scale')
+ __slots__ = ('autosave_filename', 'autosave_path', 'box_height', 'box_width', 'config_dir',
+ 'config_subfolders', 'home_dir', 'link_discord', 'link_downloads', 'link_github',
+ 'link_website', 'settings_file', 'ui_scale')
def __init__(self):
self.autosave_filename: str = 'autosave.json'
+ self.autosave_path: Path = Path()
self.box_height: int = 64
self.box_width: int = 49
self.config_dir: Path = Path()
@@ -39,14 +40,15 @@ def __repr__(self):
class SETSSettings():
__slots__ = ('_settings', 'default_mark', 'default_save_format', 'default_rarity',
- 'picker_relative', 'pref_backup', 'state__geometry', 'ui_scale')
+ 'picker_relative', 'pref_backup', 'ui_scale', 'state__geometry')
def __init__(self, settings_file_path: Path):
self.default_mark: str = ''
- self.default_save_format: str = 'JSON',
- self.default_rarity: str = 'Common',
+ self.default_save_format: str = 'JSON'
+ self.default_rarity: str = 'Common'
self.picker_relative: int = 0
self.pref_backup: int = 0
+ self.ui_scale: float = 1
self.state__geometry: QByteArray = QByteArray()
diff --git a/src/datafunctions.py b/src/datafunctions.py
index eb6e779..666753e 100644
--- a/src/datafunctions.py
+++ b/src/datafunctions.py
@@ -44,7 +44,7 @@ def finish_backend_init():
exit_splash(self)
enter_splash(self)
- load_build_file(self, self.config['autosave_filename'], update_ui=False)
+ load_build_file(self, str(self.config.autosave_path), update_ui=False)
self.downloader.default_session_from_env()
exec_in_thread(
self, populate_cache, self, finished=finish_backend_init,
@@ -349,7 +349,7 @@ def load_base_images(self, threaded_worker: ThreadObject):
self.cache.overlays.check = QImage(get_asset_path('check_overlay.png', self.app_dir))
threaded_worker.update_splash.emit('Loading: Images (Skills)')
- img_folder = self.config['config_subfolders']['images']
+ img_folder = self.config.config_subfolders['images']
for rank_group in self.cache.skills['space']:
for skill_group in rank_group:
for skill_node in skill_group['nodes']:
@@ -377,7 +377,7 @@ def load_images(self, threaded_worker=None):
Parameters:
- :param threaded_worker: (unused; required for compatability with employed threading method)
"""
- img_folder = self.config['config_subfolders']['images']
+ img_folder = self.config.config_subfolders['images']
for img_name, img in self.cache.images.items():
if img.isNull():
load_image(img_name, img, img_folder)
@@ -396,8 +396,8 @@ def download_images(self, threaded_worker: ThreadObject):
else:
self.cache.images_failed.pop(img)
images = self.cache.images_set - no_retry_images - get_downloaded_icons(
- Path(self.config['config_subfolders']['images']))
- img_folder = self.config['config_subfolders']['images']
+ Path(self.config.config_subfolders['images']))
+ img_folder = self.config.config_subfolders['images']
images_to_download = images - self.cache.boff_abilities['all'].keys()
for image_name in images_to_download:
@@ -444,7 +444,7 @@ def autosave(self):
Saves build to autosave file.
"""
if not self.building:
- store_json(self.build, self.config['autosave_filename'])
+ store_json(self.build, str(self.config.autosave_path))
def map_build_items(self, old_build: dict, new_build: dict, mapping):
@@ -764,7 +764,7 @@ def load_legacy_build_image(self):
Loads legacy build from image file
"""
load_path = browse_path(
- self, self.config['config_subfolders']['library'],
+ self, self.config.config_subfolders['library'],
'PNG image (*.png);;Any File (*.*)')
if load_path != '':
_, _, extension = load_path.rpartition('.')
@@ -1024,11 +1024,11 @@ def backup_cargo_data(self):
cargo_files = (
'boff_abilities.json', 'doffs.json', 'equipment.json', 'modifiers.json',
'ship_list.json', 'starship_traits.json', 'traits.json')
- cargo_folder = self.config['config_subfolders']['cargo']
- backups_folder = self.config['config_subfolders']['backups']
+ cargo_folder = self.config.config_subfolders['cargo']
+ backups_folder = self.config.config_subfolders['backups']
for file_name in cargo_files:
- cargo_path = os.path.join(cargo_folder, file_name)
- backups_path = os.path.join(backups_folder, file_name)
+ cargo_path = str(cargo_folder / file_name)
+ backups_path = str(backups_folder / file_name)
copy_file(cargo_path, backups_path)
diff --git a/src/iofunc.py b/src/iofunc.py
index e6742c1..862936e 100644
--- a/src/iofunc.py
+++ b/src/iofunc.py
@@ -69,7 +69,7 @@ def get_cargo_data(self, filename: str, url: str, ignore_cache_age=False) -> dic
- :param url: url to cargo table
- :param ignore_cache_age: True if cache of any age should be accepted
"""
- filepath = os.path.join(self.config['config_subfolders']['cargo'], filename)
+ filepath = str(self.config.config_subfolders['cargo'] / filename)
cargo_data = None
# try loading from cache
@@ -90,10 +90,9 @@ def get_cargo_data(self, filename: str, url: str, ignore_cache_age=False) -> dic
return cargo_data
except (requests.exceptions.RequestException, json.JSONDecodeError):
if ignore_cache_age:
- backup_path = os.path.join(self.config['config_subfolders']['backups'], filename)
- auto_backup_path = os.path.join(
- self.config['config_subfolders']['auto_backups'], filename)
- if self.settings.value('pref_backup', type=int) == 0:
+ backup_path = str(self.config.config_subfolders['backups'] / filename)
+ auto_backup_path = str(self.config.config_subfolders['auto_backups'] / filename)
+ if self.settings.pref_backup == 0:
backup_paths = (auto_backup_path, backup_path)
else:
backup_paths = (backup_path, auto_backup_path)
@@ -119,7 +118,7 @@ def get_cached_cargo_data(self, filename: str) -> dict | list:
Parameters:
- :param filename: name of the cache file
"""
- filepath = os.path.join(self.config['config_subfolders']['cache'], filename)
+ filepath = str(self.config.config_subfolders['cache'] / filename)
if os.path.exists(filepath) and os.path.isfile(filepath):
last_modified = os.path.getmtime(filepath)
if (datetime.now() - datetime.fromtimestamp(last_modified)).days < 7:
@@ -138,7 +137,7 @@ def store_to_cache(self, data, filename: str):
- :param data: data that will be stored
- :param filename: filename of the cache file
"""
- filepath = os.path.join(self.config['config_subfolders']['cache'], filename)
+ filepath = str(self.config.config_subfolders['cache'] / filename)
store_json(data, filepath)
@@ -196,8 +195,8 @@ def get_ship_image(self, image_name: str, threaded_worker):
- :param threaded_worker: thread object supplying signals
"""
image_url = WIKI_IMAGE_URL + image_name.replace(' ', '_')
- image_path = os.path.join(
- self.config['config_subfolders']['ship_images'], quote_plus(image_name))
+ image_path = str(
+ self.config.config_subfolders['ship_images'] / quote_plus(image_name))
_, _, fmt = image_name.rpartition('.')
image = QImage(image_path)
if image.isNull():
@@ -231,7 +230,7 @@ def image(self, image_name: str) -> QImage:
"""
img = self.cache.images[image_name]
if img.isNull():
- img_folder = self.config['config_subfolders']['images']
+ img_folder = self.config.config_subfolders['images']
load_image(image_name, img, img_folder)
return img
@@ -258,9 +257,9 @@ def auto_backup_cargo_file(self, filename: str):
Parameters:
- :param filename: name of the file to back up
"""
- source_path = os.path.join(self.config['config_subfolders']['cargo'], filename)
+ source_path = str(self.config.config_subfolders['cargo'] / filename)
if os.path.exists(source_path):
- target_path = os.path.join(self.config['config_subfolders']['auto_backups'], filename)
+ target_path = str(self.config.config_subfolders['auto_backups'] / filename)
shutil__copyfile(source_path, target_path)
diff --git a/src/style.py b/src/style.py
index adb4696..0d642fd 100644
--- a/src/style.py
+++ b/src/style.py
@@ -91,7 +91,7 @@ def get_css(self, style: dict) -> str:
values.
"""
css = str()
- ui_scale = self.config['ui_scale']
+ ui_scale = self.config.ui_scale
for key, val in style.items():
if isinstance(val, str) and val.startswith('@'):
v = self.theme['defaults'][val[1:]]
@@ -129,7 +129,7 @@ def theme_font(self, key=None, font_spec=()) -> QFont:
except KeyError:
font = self.theme['app']['font']
font_family = (font[0], *self.theme['app']['font-fallback'])
- font_size = int(font[1] * self.config['ui_scale'])
+ font_size = int(font[1] * self.config.ui_scale)
try:
font_weight = WEIGHT_CONVERSION[font[2]]
except KeyError:
@@ -159,7 +159,7 @@ def prepare_tooltip_css(self):
"""
Converts dictionaries containing tooltip style to css
"""
- ui_scale = self.config['ui_scale']
+ ui_scale = self.config.ui_scale
tooltips = self.theme['tooltip']
for tag, style in self.theme['tooltip_def'].items():
css = ''
diff --git a/src/subwindows.py b/src/subwindows.py
index 2969882..9f80cd7 100644
--- a/src/subwindows.py
+++ b/src/subwindows.py
@@ -128,7 +128,7 @@ def __init__(
self._result = None
self._modifiers = {}
self._image_suffix = ''
- ui_scale = sets.config['ui_scale']
+ ui_scale = sets.config.ui_scale
spacing = sets.theme['defaults']['isp'] * ui_scale
layout = VBoxLayout(margins=(spacing, 0, spacing, spacing), spacing=0)
top_layout = HBoxLayout(spacing=spacing)
@@ -325,7 +325,7 @@ def __init__(self, sets, parent_window, style: str = 'picker'):
self.setMinimumSize(10, 10)
self.setSizePolicy(SMAXMAX)
- ui_scale = sets.config['ui_scale']
+ ui_scale = sets.config.ui_scale
spacing = sets.theme['defaults']['isp'] * ui_scale
layout = VBoxLayout(margins=spacing, spacing=spacing)
self._ship_data_model = QStringListModel()
@@ -417,7 +417,7 @@ def __init__(self, sets, parent_window, style: str = 'picker'):
self._item = self.empty_item
self._result = None
self._modifiers = {}
- ui_scale = sets.config['ui_scale']
+ ui_scale = sets.config.ui_scale
csp = sets.theme['defaults']['csp'] * ui_scale
layout = VBoxLayout(spacing=csp)
rarity_layout = HBoxLayout(spacing=csp)
@@ -498,7 +498,7 @@ class ExportWindow(QDialog):
"""
def __init__(self, sets, parent_window, data_getter: Callable):
super().__init__(parent=parent_window)
- thick = sets.theme['app']['frame_thickness'] * sets.config['ui_scale']
+ thick = sets.theme['app']['frame_thickness'] * sets.config.ui_scale
dialog_layout = VBoxLayout(margins=thick)
main_frame = create_frame(sets, size_policy=SMINMIN)
dialog_layout.addWidget(main_frame)
diff --git a/src/widgetbuilder.py b/src/widgetbuilder.py
index 69c3672..095c4d7 100644
--- a/src/widgetbuilder.py
+++ b/src/widgetbuilder.py
@@ -242,13 +242,13 @@ def create_item_button(self, style_override: dict = {}) -> ItemButton:
"""
label = create_label(self, '', 'infobox')
frame = create_frame(self, 'infobox_frame')
- margin = self.theme['defaults']['csp'] * self.config['ui_scale']
+ margin = self.theme['defaults']['csp'] * self.config.ui_scale
layout = VBoxLayout(margin)
layout.addWidget(label, alignment=ATOP)
frame.setLayout(layout)
button = ItemButton(
self.box_width, self.box_height, self.theme['item'], label, frame,
- margin + self.theme['defaults']['bw'] * self.config['ui_scale'])
+ margin + self.theme['defaults']['bw'] * self.config.ui_scale)
return button
@@ -268,7 +268,7 @@ def create_build_section(
"""
layout = QGridLayout()
layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(self.theme['defaults']['margin'] * self.config['ui_scale'])
+ layout.setSpacing(self.theme['defaults']['margin'] * self.config.ui_scale)
label = create_label(self, label_text, style_override={'margin': (0, 0, 6, 0)})
label_size_policy = label.sizePolicy()
label_size_policy.setRetainSizeWhenHidden(True)
@@ -300,7 +300,7 @@ def create_boff_station_space(
"""
layout = QGridLayout()
layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(self.theme['defaults']['margin'] * self.config['ui_scale'])
+ layout.setSpacing(self.theme['defaults']['margin'] * self.config.ui_scale)
layout.setColumnStretch(3, 1)
if specialization != '':
specialization = f' / {specialization}'
@@ -313,7 +313,7 @@ def create_boff_station_space(
else:
label_options = (profession + specialization,)
widget_storage = self.widgets.build['space']
- label_layout = HBoxLayout(spacing=self.config['ui_scale'] * 3)
+ label_layout = HBoxLayout(spacing=self.config.ui_scale * 3)
icon_label = TooltipLabel('', create_label(self, '', 'label_tooltip'))
widget_storage['boff_label_icons'][boff_id] = icon_label
label_layout.addWidget(icon_label, alignment=ALEFT)
@@ -347,7 +347,7 @@ def create_boff_station_ground(self, boff_id: int) -> VBoxLayout:
- :param boff_id: identifies the boff station
"""
widget_storage = self.widgets.build['ground']
- m = self.theme['defaults']['margin'] * self.config['ui_scale']
+ m = self.theme['defaults']['margin'] * self.config.ui_scale
layout = VBoxLayout(spacing=m)
label_layout = HBoxLayout(spacing=m)
label_layout.setAlignment(ALEFT)
@@ -384,7 +384,7 @@ def create_personal_trait_section(self, environment: str) -> QGridLayout:
"""
layout = QGridLayout()
layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(self.theme['defaults']['margin'] * self.config['ui_scale'])
+ layout.setSpacing(self.theme['defaults']['margin'] * self.config.ui_scale)
label = create_label(self, 'Personal Traits', style_override={'margin': (0, 0, 6, 0)})
layout.addWidget(label, 0, 0, 1, 4, alignment=ALEFT)
widget_storage = self.widgets.build[environment]
@@ -410,7 +410,7 @@ def create_starship_trait_section(self) -> QGridLayout:
"""
layout = QGridLayout()
layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(self.theme['defaults']['margin'] * self.config['ui_scale'])
+ layout.setSpacing(self.theme['defaults']['margin'] * self.config.ui_scale)
label = create_label(self, 'Starship Traits', style_override={'margin': (0, 0, 6, 0)})
label.sizePolicy().setRetainSizeWhenHidden(True)
layout.addWidget(label, 0, 0, 1, 4, alignment=ALEFT)
@@ -440,7 +440,7 @@ def create_doff_section(self, environment: str) -> GridLayout:
"""
Creates duty officer section
"""
- spacing = self.theme['defaults']['bw'] * self.config['ui_scale']
+ spacing = self.theme['defaults']['bw'] * self.config.ui_scale
doff_layout = GridLayout(spacing=spacing)
doff_layout.setColumnStretch(1, 1)
for i in range(6):
@@ -466,7 +466,7 @@ def create_skill_group_space(self, group_data: dict, id_offset: int) -> GridLayo
- :param group_data: skill group data
- :param id_offset: index of the first skill node in self.widgets and self.build
"""
- layout = GridLayout(spacing=self.theme['defaults']['csp'] * self.config['ui_scale'])
+ layout = GridLayout(spacing=self.theme['defaults']['csp'] * self.config.ui_scale)
# one skill with 3 ranks
if group_data['grouping'] == 'column':
for index, node in enumerate(group_data['nodes']):
@@ -547,7 +547,7 @@ def create_bonus_bar_segment(
seg.setEnabled(False)
seg.setCheckable(True)
seg.setStyleSheet(get_style_class(self, 'QPushButton', style, style_override))
- seg.setFixedSize(7 * self.config['ui_scale'], 17 * self.config['ui_scale'])
+ seg.setFixedSize(7 * self.config.ui_scale, 17 * self.config.ui_scale)
self.widgets.skill_bonus_bars[bar][index] = seg
return seg
From 0563e2ad77467a122fc1c784d5ece5d62eef78ce Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Thu, 30 Apr 2026 14:56:05 +0200
Subject: [PATCH 03/44] adding new theme
---
src/theme.py | 749 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 749 insertions(+)
create mode 100644 src/theme.py
diff --git a/src/theme.py b/src/theme.py
new file mode 100644
index 0000000..ddc7b35
--- /dev/null
+++ b/src/theme.py
@@ -0,0 +1,749 @@
+import copy
+
+from PySide6.QtGui import QFont, QIcon
+
+WEIGHT_CONVERSION = {
+ 'normal': QFont.Weight.Normal,
+ 'bold': QFont.Weight.Bold,
+ 'extrabold': QFont.Weight.ExtraBold,
+ 'medium': QFont.Weight.Medium
+}
+
+
+class ThemeOptions:
+ """Contains Theme options affecting the UI, but not directly related to the style"""
+
+ __slots__ = ('box_height', 'box_width', 'default_box_height', 'default_box_width')
+
+ def __init__(self, initial_options: dict[str] = {}):
+ """
+ Parameters:
+ - :param initial_options: options to use instead of defaults (optional)
+ """
+ self.box_height: float = 64.0
+ self.box_width: float = 49.0
+ self.default_box_height: float = 64.0
+ self.default_box_width: float = 49.0
+ if len(initial_options) > 0:
+ for option_name in self.__slots__:
+ if option_value := initial_options.get(option_name):
+ setattr(self, option_name, option_value)
+
+
+class AppTheme:
+ """Encapsulates theme functions and data."""
+
+ def __init__(self, scale: float, theme_tree: dict[str] = {}, theme_options: dict[str] = {}):
+ """
+ Parameters:
+ - :param scale: Used to adjust font sizes, margins, paddings, etc.
+ - :param theme_tree: theme data to use instead of default theme
+ - :param theme_tree: options that affect the UI, but are not directly related to the style
+ """
+ self.scale: float = scale
+ self.icons: dict[str, QIcon] = dict()
+ self.opt: ThemeOptions = ThemeOptions(theme_options)
+ self.opt.box_height = self.opt.default_box_height * self.scale
+ self.opt.box_width = self.opt.default_box_width * self.scale
+ if len(theme_tree) > 0:
+ self._theme_data: dict[str, dict] = theme_tree
+ else:
+ self._theme_data: dict[str, dict] = self.get_default_theme()
+
+ def __getitem__(self, key: str):
+ return self._theme_data[key]
+
+ def get_style(self, widget: str, override: dict[str] = {}) -> str:
+ """
+ Returns style sheet according to default style of widget with override style. Returns
+ empty string if widget style is not defined in current theme.
+
+ Parameters:
+ - :param widget: name of the widget to grab the style for from the current theme
+ - :param override: contains additional style or override style to replace the default \
+ style with (optional)
+
+ :return: str containing css style sheet
+ """
+ if widget in self._theme_data:
+ if len(override) > 0:
+ style = self.merge_style(self._theme_data[widget], override)
+ else:
+ style = self._theme_data[widget]
+ return self.get_css(style)
+ else:
+ return ''
+
+ def get_style_class(self, class_name: str, widget: str, override: dict[str] = {}) -> str:
+ """
+ Returns style sheet according to default style of widget with override style. Style only
+ applies to `class_name`. Sub-controls (prefixed with `::`), pseudo-states (prefixed with
+ `:`) and descendant selectors (prefixed with `~`) defined in current theme are formatted to
+ only apply to the given `class_name`. Returns empty string with wdget style is not defined
+ in current theme.
+
+ Parameters:
+ - :param class_name: name of the widget class to be styled
+ - :param widget: name of the widget to grab the style for from the current theme; may be \
+ empty string to only apply override styles
+ - :param override: contains additional style or override style to replace the default \
+ style with (optional)
+
+ :return: str containing css style sheet
+ """
+ if widget == '':
+ style = override
+ elif widget in self._theme_data:
+ if len(override) > 0:
+ style: dict[str] = self.merge_style(self._theme_data[widget], override)
+ else:
+ style: dict[str] = self._theme_data[widget]
+ else:
+ return ''
+ style_sheet = f'{class_name} {{{self.get_css(style)}}}'
+ for prop, value in style.items():
+ if prop.startswith(':'):
+ style_sheet += f''' {class_name}{prop} {{{self.get_css(value)}}}'''
+ elif prop.startswith('~'):
+ style_sheet += f' {self.get_style_class(f"{class_name} {prop[1:]}", '', value)}'
+ return style_sheet
+
+ def merge_style(self, s1: dict[str], s2: dict[str]) -> dict[str]:
+ """
+ Returns new dictionary where the given styles are merged. Values in the second style take
+ precedence. Up to one sub-dictionary is merged recursively.
+
+ Parameters:
+ - :param s1: Style-dict 1
+ - :param s2: Style-dict 2
+
+ :return: merged dictionary
+ """
+ result = copy.deepcopy(s1)
+ for key, value in s2.items():
+ if key in result.keys() and isinstance(result[key], dict) and isinstance(value, dict):
+ result[key].update(value)
+ continue
+ result[key] = value
+ return result
+
+ def get_css(self, style: dict[str]) -> str:
+ """
+ Converts style dictionary into css style sheet. Values starting with `@` are treated as
+ shortcuts and replaced with values from the `default` key of the current theme. Ignores
+ property `font`, sub-controls (prefixed with `::`), pseudo-states (prefixed with
+ `:`) and descendant selectors (prefixed with `~`).
+
+ Parameters:
+ - :param style: dictionary containg style to be converted to css
+
+ :return: css style sheet
+ """
+ style_sheet = str()
+ for prop, raw_value in style.items():
+ if isinstance(raw_value, str) and raw_value.startswith('@'):
+ prop_value = self._theme_data['defaults'][raw_value[1:]]
+ else:
+ prop_value = raw_value
+ if prop.startswith(':') or prop.startswith('~') or prop == 'font':
+ continue
+ elif isinstance(prop_value, int):
+ style_sheet += f'{prop}:{prop_value * self.scale}px;'
+ elif isinstance(prop_value, tuple):
+ scaled_values = map(lambda s: str(s * self.scale), prop_value)
+ style_sheet += f'''{prop}:{'px '.join(scaled_values)}px;'''
+ else:
+ style_sheet += f'{prop}:{prop_value};'
+ return style_sheet
+
+ def get_font(self, widget: str, font_spec: tuple[str, int, str] | str = ()) -> QFont:
+ """
+ Returns QFont object with font specified in current theme or font_spec. Adds default
+ fallback font families.
+
+ Parameters:
+ - :param widget: name of style to get font from
+ - :param font_spec: font tuple consisting of family, size and weight OR font shortcut \
+ (optional)
+
+ :return: configured QFont object
+ """
+ try:
+ if len(font_spec) != 3 and isinstance(font_spec, tuple):
+ font_spec = self._theme_data[widget]['font']
+ if isinstance(font_spec, str) and font_spec.startswith('@'):
+ font = self._theme_data['defaults'][font_spec[1:]]
+ else:
+ font = font_spec
+ except KeyError:
+ font = self._theme_data['app']['font']
+ font_family = (font[0], *self._theme_data['app']['font-fallback'])
+ font_size = int(font[1] * self.scale)
+ font_weight = WEIGHT_CONVERSION[font[2]]
+ font = QFont(font_family, font_size, font_weight)
+ font.setHintingPreference(QFont.HintingPreference.PreferNoHinting)
+ font.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
+ return font
+
+ def create_style_sheet(self, d: dict[str, dict]) -> str:
+ """
+ Creates Stylesheet from dictionary. Dictionary keys represent css selector. Ignores
+ property `font`, sub-controls (prefixed with `::`), pseudo-states (prefixed with
+ `:`) and descendant selectors (prefixed with `~`).
+
+ Parameters:
+ - :param d: style dictionary
+
+ :return: string containing style sheet
+ """
+ style_sheet = str()
+ for prop, prop_value in d.items():
+ style_sheet += f'{prop} {{{self.get_css(prop_value)}}}'
+ return style_sheet
+
+ def get_default_theme(self) -> dict[str, dict]:
+ """
+ Returns default theme.
+ """
+ return {
+ # general style
+ 'app': {
+ 'bg': '#1a1a1a',
+ 'fg': '#eeeeee',
+ 'sets': '#c59129',
+ 'font': ('Overpass', 11, 'normal'),
+ 'heading': ('Overpass', 14, 'bold'),
+ 'subhead': ('Overpass', 12, 'medium'),
+ 'font-fallback': ('Yu Gothic UI', 'Nirmala UI', 'Microsoft YaHei UI', 'sans-serif'),
+ 'frame_thickness': 8,
+ # this styles every item of the given type
+ 'style': {
+ # scroll bar trough (invisible)
+ 'QScrollBar': {
+ 'background': 'none',
+ 'border-style': 'none',
+ 'border-radius': 0,
+ 'margin': 0
+ },
+ 'QScrollBar:vertical': {
+ 'width': 8,
+ },
+ 'QScrollBar:horizontal': {
+ 'height': 8,
+ },
+ # space above and below the scrollbar handle
+ 'QScrollBar::add-page, QScrollBar::sub-page': {
+ 'background': 'none'
+ },
+ # scroll bar handle
+ 'QScrollBar::handle': {
+ 'background-color': 'rgba(100,100,100,.75)',
+ 'border-radius': 4,
+ 'border': 'none'
+ },
+ # scroll bar arrow buttons
+ 'QScrollBar::add-line, QScrollBar::sub-line': {
+ 'height': 0 # hiding the arrow buttons
+ }
+ }
+ },
+ # shortcuts, @bg -> means bg in this sub-dictionary
+ 'defaults': {
+ 'bg': '#1a1a1a', # background
+ 'mbg': '#242424', # medium background
+ 'lbg': '#404040', # light background
+ 'sets': '#c59129', # accent
+ 'lsets': '#60c59129', # light accent
+ 'font': ('Overpass', 11, 'normal'),
+ 'heading': ('Overpass', 14, 'bold'),
+ 'subhead': ('Overpass', 12, 'medium'),
+ 'small_text': ('Overpass', 10, 'normal'),
+ 'fg': '#eeeeee', # foreground (usually text)
+ 'mfg': '#bbbbbb', # medium foreground
+ 'bc': '#888888', # border color
+ 'bw': 1, # border width
+ 'br': 2, # border radius
+ 'sep': 2, # seperator -> width of major seperating lines
+ 'margin': 10, # default margin between widgets
+ 'csp': 5, # child spacing -> content margin
+ 'isp': 15, # item spacing
+ },
+ # dark frame
+ 'frame': {
+ 'background-color': '@bg',
+ 'border-style': 'none',
+ 'margin': 0,
+ 'padding': 0
+ },
+ # medium frame
+ 'medium_frame': {
+ 'background-color': '@mbg',
+ 'margin': 0,
+ 'padding': 0
+ },
+ # light frame
+ 'light_frame': {
+ 'background': '@lbg',
+ 'margin': 0,
+ 'padding': 0
+ },
+ # default text (non-button, non-entry, non table)
+ 'label': {
+ 'color': '@fg',
+ 'margin': (3, 0, 3, 0),
+ 'qproperty-indent': '0', # disables auto-indent
+ 'border-style': 'none',
+ 'font': '@font'
+ },
+ # default text (non-button, non-entry, non table)
+ 'hint_label': {
+ 'color': '@mfg',
+ 'margin': (3, 0, 3, 0),
+ 'qproperty-indent': '0', # disables auto-indent
+ 'border-style': 'none',
+ 'font': '@font'
+ },
+ # heading label
+ 'label_heading': {
+ 'color': '@fg',
+ 'qproperty-indent': '0',
+ 'border-style': 'none',
+ 'font': '@heading'
+ },
+ # label for subheading
+ 'label_subhead': {
+ 'color': '@fg',
+ 'qproperty-indent': '0',
+ 'border-style': 'none',
+ 'margin-bottom': 3,
+ 'font': '@subhead'
+ },
+ # default button
+ 'button': {
+ 'background-color': 'none',
+ 'color': '@fg',
+ 'text-decoration': 'none',
+ 'border-width': '@bw',
+ 'border-style': 'solid',
+ 'border-color': '@sets',
+ 'margin': (3, 3, 3, 3),
+ 'padding': (2, 5, 0, 5),
+ 'font': ('Overpass', 13, 'medium'),
+ ':hover': {
+ 'border-color': '@bc'
+ },
+ ':disabled': {
+ 'color': '@bc'
+ },
+ # Tooltip
+ '~QToolTip': {
+ 'background-color': '@mbg',
+ 'border-style': 'solid',
+ 'border-color': '@lbg',
+ 'border-width': '@bw',
+ 'padding': (0, 0, 0, 0),
+ 'color': '@fg',
+ 'font': 'Overpass'
+ }
+ },
+ # heavy button
+ 'heavy_button': {
+ 'background-color': '@sets',
+ 'color': '@fg',
+ 'text-decoration': 'none',
+ 'border-width': '@bw',
+ 'border-style': 'solid',
+ 'border-color': '@sets',
+ 'margin': (3, 3, 3, 3),
+ 'padding': (2, 5, 0, 5),
+ 'font': ('Overpass', 13, 'bold'),
+ ':hover': {
+ 'background-color': '@mbg'
+ },
+ ':disabled': {
+ 'color': '@bc'
+ }
+ },
+ # build item button
+ 'item': {
+ 'background-color': '#242424',
+ 'border-width': 1,
+ 'border-color': '#888888',
+ 'border-highlight-color': '#ffd700'
+ },
+ # build item button
+ 'item_dark': {
+ 'background-color': '#1a1a1a',
+ 'border-width': 1,
+ 'border-color': '#404040',
+ },
+ # checkbox
+ 'checkbox': {
+ '::indicator': {
+ 'width': 16,
+ 'height': 16,
+ 'border-style': 'solid',
+ 'border-width': '@bw',
+ 'border-color': '@bc',
+ 'background-color': '@lbg',
+ },
+ '::indicator:hover': {
+ 'border-color': '@sets'
+ },
+ '::indicator:checked': {
+ 'image': 'url(local/check.svg)'
+ },
+ '::indicator:unchecked': {
+ 'image': 'url(local/uncheck.svg)',
+ }
+ },
+ # holds sub-pages
+ 'tabber': {
+ 'background-color': 'none',
+ 'border': 'none',
+ 'margin': 0,
+ 'padding': 0,
+ '::pane': {
+ 'border': 'none',
+ }
+ },
+ # default tabber buttons (hidden)
+ 'tabber_tab': {
+ '::tab': {
+ 'height': 0,
+ 'width': 0
+ }
+ },
+ # combo box
+ 'combobox': {
+ 'border-style': 'solid',
+ 'border-width': '@bw',
+ 'border-color': '@bc',
+ 'background-color': '@bg',
+ 'padding': (1, 5, 1, 5),
+ 'color': '@fg',
+ 'font': '@subhead',
+ '::down-arrow': {
+ 'image': 'url(local/thick-chevron-down.svg)',
+ 'width': '@margin',
+ },
+ '::drop-down': {
+ 'border-style': 'none',
+ 'padding': (2, 2, 2, 2)
+ },
+ '~QAbstractItemView': {
+ 'background-color': '@mbg',
+ 'border-style': 'solid',
+ 'border-color': '@bc',
+ 'border-width': '@bw',
+ 'border-radius': '@br',
+ 'color': '@fg',
+ 'outline': '0',
+ '::item': {
+ 'border-width': '@bw',
+ 'border-style': 'solid',
+ 'border-color': '@mbg',
+ },
+ '::item:hover': {
+ 'border-color': '@sets',
+ },
+ }
+ },
+ # additional style for doff combobox
+ 'doff_combo': {
+ 'color': '@fg',
+ 'border-style': 'none',
+ 'border-width': 0,
+ 'margin': 0,
+ 'font': '@small_text'
+ },
+ # additional style for boff combobox
+ 'boff_combo': {
+ 'font': '@font',
+ ':disabled': {
+ 'border-color': '@bg',
+ 'border-left-width': 0,
+ 'padding-left': 0
+ },
+ '::down-arrow:disabled': {
+ 'image': 'none',
+ 'width': '@margin',
+ },
+ },
+ # auto-completion popup of combobox
+ 'popup': {
+ 'background-color': '@mbg',
+ 'border-style': 'solid',
+ 'border-color': '@bc',
+ 'border-width': '@bw',
+ 'border-radius': '@br',
+ 'color': '@fg',
+ 'outline': '0',
+ '::item': {
+ 'border-width': '@bw',
+ 'border-style': 'solid',
+ 'border-color': '@mbg',
+ },
+ '::item:hover': {
+ 'border-color': '@sets',
+ },
+ },
+ # line of user-editable text
+ 'entry': {
+ 'background-color': '@mbg',
+ 'color': '@fg',
+ 'border-width': '@bw',
+ 'border-style': 'solid',
+ 'border-color': '@bc',
+ 'font': '@subhead',
+ 'selection-background-color': '@lsets',
+ # cursor is inside the line
+ ':focus': {
+ 'border-color': '@sets'
+ },
+ ':hover': {
+ 'background-color': '@lbg'
+ }
+ },
+ # for item tooltips
+ 'infobox': {
+ 'background-color': '#000000',
+ 'border-style': 'none',
+ 'color': '@fg',
+ # 'margin': 0,
+ # 'padding': 0,
+ },
+ 'infobox_frame': {
+ 'background-color': '#000000',
+ 'border-style': 'solid',
+ 'border-width': '@bw',
+ 'border-color': '@mbg',
+ 'border-radius': '@br',
+ # 'margin': 0,
+ # 'padding': '@sep',
+ },
+ # tooltip for TooltipLabel
+ 'label_tooltip': {
+ 'color': '@fg',
+ 'background-color': '@bg',
+ 'border-color': '@lbg',
+ 'border-radius': '@br',
+ 'border-style': 'solid',
+ 'border-width': '@bw',
+ 'font': '@font',
+ 'padding': 2,
+ 'qproperty-indent': '0', # disables auto-indent
+ },
+ # for formatting tooltip text, will contain css from tooltip_def
+ 'tooltip': {},
+ 'tooltip_def': {
+ 'indent': {
+ 'margin': (0, 0, 0, 20),
+ },
+ 'ul': {
+ 'margin': (0, 0, 0, 20),
+ '-qt-list-indent': '0',
+ },
+ 'li': {
+ 'margin-bottom': 1,
+ },
+ 'boff_header': {
+ 'color': '#42afca',
+ 'font-size': 'large',
+ 'font-weight': 'bold',
+ 'margin': 0
+ },
+ 'boff_subheader': {
+ 'font-size': 10,
+ 'margin': (0, 0, 20, 0)
+ },
+ 'trait_header': {
+ 'color': '#42afca',
+ 'font-size': 'large',
+ 'font-weight': 'bold',
+ 'margin': 0, # padding: 0
+ },
+ 'trait_subheader': {
+ 'color': '#42afca',
+ 'font-size': 10,
+ 'margin': (0, 0, 20, 0),
+ },
+ 'equipment_name': {
+ 'font-size': 'large',
+ 'font-weight': 'bold',
+ 'margin': 0
+ },
+ 'equipment_type_subheader': {
+ 'font-size': 10,
+ 'margin': (0, 0, 20, 0),
+ },
+ 'equipment_head': {
+ 'color': '#42afca',
+ 'font-size': 12,
+ 'margin': (10, 0, 0, 0)
+ },
+ 'equipment_subhead': {
+ 'color': '#f4f400',
+ 'font-size': 10,
+ 'margin': 0
+ },
+ 'equipment_who': {
+ 'color': '#ff6347',
+ 'font-size': 10,
+ 'margin': (0, 0, 10, 0)
+ },
+ 'skill_ultimate_name': {
+ 'color': '#ffd700;',
+ 'font-size': 12,
+ 'margin': (10, 0, 0, 0)
+ },
+ },
+ # picker window
+ 'picker': {
+ 'background-color': '@bg',
+ 'border-color': '@sets',
+ 'border-width': 3,
+ 'border-style': 'solid',
+ 'border-radius': '@br'
+ },
+ # list widget displaying items in picker
+ 'picker_list': {
+ 'background-color': '@bg',
+ 'color': '@fg',
+ 'border-style': 'none',
+ 'margin': 0,
+ 'font': '@font',
+ 'outline': '0', # removes dotted line around clicked item
+ '::item': {
+ 'border-width': '@bw',
+ 'border-style': 'solid',
+ 'border-color': '@bg',
+ },
+ '::item:selected': {
+ 'background-color': '@bg',
+ 'border-width': '@bw',
+ 'border-style': 'solid',
+ 'border-color': '@bg',
+ },
+ # selected but not the last click of the user
+ '::item:selected:!active': {
+ 'color': '@fg'
+ },
+ '::item:hover': {
+ 'background-color': '@lbg',
+ },
+ '~QScrollBar': {
+ 'border-style': 'none',
+ 'border': 'none',
+ 'border-radius': 0
+ }
+ },
+ # large text editor
+ 'textedit': {
+ 'background-color': '@mbg',
+ 'border-style': 'solid',
+ 'border-width': '@bw',
+ 'border-color': '@bc',
+ 'font': '@font',
+ 'color': '@fg',
+ 'padding': 3,
+ 'selection-background-color': '@lsets'
+ },
+ # context menu
+ 'context_menu': {
+ 'background-color': '@bg',
+ 'border-color': '@lbg',
+ 'border-width': '@bw',
+ 'border-style': 'solid',
+ 'border-radius': '@br',
+ 'padding': '@sep',
+ '::item': {
+ 'color': '@fg',
+ 'font': '@font',
+ 'border-color': '@bg',
+ 'border-radius': 0,
+ 'border-style': 'solid',
+ 'border-width': '@bw',
+ 'padding': (3, 3, 1, 10),
+ },
+ '::icon': {
+ 'padding': (1, 1, 1, 10),
+ },
+ '::item:selected': {
+ 'border-color': '@sets',
+ },
+ '::item:disabled': {
+ 'color': '@mfg'
+ },
+ '::item:disabled:selected': {
+ 'border-color': '@bg'
+ }
+ },
+ # frame for duty officers
+ 'doff_frame': {
+ 'background-color': '@bg',
+ 'border-style': 'solid',
+ 'border-width': '@bw',
+ 'border-color': '@bc',
+ 'padding': 2
+ },
+ # segment of the bonus bar
+ 'bonus_bar': {
+ ':disabled': {
+ 'border-style': 'solid',
+ 'border-top-style': 'none',
+ 'border-bottom-style': 'none',
+ 'border-width': '@bw',
+ 'border-color': '@bc',
+ 'background-color': '@bg',
+ },
+ ':checked': {
+ 'background-color': '@sets'
+ }
+ },
+ # label holding career / ground icon
+ 'unlock_label': {
+ 'border-style': 'none',
+ 'border-top-style': 'solid',
+ 'border-top-width': 1,
+ 'border-top-color': '@bc',
+ 'margin': (0, 0, 3, 0),
+ 'padding': (3, 10, 0, 10)
+ },
+ # horizontal seperator
+ 'hr': {
+ 'background-color': '@lbg',
+ 'border-style': 'none',
+ 'height': 1
+ },
+ # horizontal sliding selector
+ 'slider': {
+ 'font': ('Roboto Mono', 11, 'Normal'),
+ 'color': '@fg',
+ '::groove:horizontal': {
+ 'border-style': 'none',
+ 'background-color': '@lbg',
+ 'border-radius': '@bw',
+ 'height': 3
+ },
+ '::handle:horizontal': {
+ 'border-style': 'solid',
+ 'border-width': '@bw',
+ 'border-color': '@bc',
+ 'background-color': '@bc',
+ 'width': 6,
+ 'margin-top': -7,
+ 'margin-bottom': -7
+ },
+ '::handle:horizontal:hover': {
+ 'border-color': '@sets'
+ },
+ '::handle:horizontal:pressed': {
+ 'background-color': '#666666'
+ },
+ },
+ # small window
+ 'dialog_window': {
+ 'background-color': '@sets'
+ },
+ }
From 29fc153c418baf9a1b7a4b37fe4d21f7342e4765 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Thu, 30 Apr 2026 15:29:54 +0200
Subject: [PATCH 04/44] integrating new theme into app.py
---
src/app.py | 87 ++++++++++++++++++++++++++--------------------------
src/theme.py | 45 ++++++++++++++++++---------
2 files changed, 74 insertions(+), 58 deletions(-)
diff --git a/src/app.py b/src/app.py
index 9b76bf2..b4d0369 100644
--- a/src/app.py
+++ b/src/app.py
@@ -1,7 +1,7 @@
import os
from pathlib import Path
-from PySide6.QtCore import QSettings, Qt, QThread
+from PySide6.QtCore import QDir, Qt, QThread
from PySide6.QtGui import QFontDatabase, QTextOption
from PySide6.QtWidgets import QApplication, QFrame, QPlainTextEdit, QScrollArea, QTabWidget, QWidget
@@ -18,6 +18,7 @@
create_folder, delete_folder_contents, get_asset_path, load_icon, load_json, open_url,
store_json)
from .subwindows import ExportWindow, ItemEditor, Picker, ShipSelector
+from .theme import AppTheme
from .widgets import (
Cache, ContextMenu, GridLayout, HBoxLayout, ImageLabel, ShipButton, ShipImage, TooltipLabel,
VBoxLayout, WidgetStorage)
@@ -54,18 +55,12 @@ class SETS():
app_dir = None
# (release version, dev version)
versions = ('', '')
- # see main.py for contents
- theme = {}
# stores widgets that need to be accessed from outside their creating function
widgets: WidgetStorage
# stores refined cargo data
cache: Cache
# stores current build
build: dict
- # height of items
- box_height: int
- # width of items
- box_width: int
# for picking items
picker_window: Picker
# for selecting ships
@@ -99,6 +94,8 @@ def __init__(self, theme, args, path, config, versions):
self.config.config_dir = self.get_config_dir_path()
self.settings = SETSSettings(self.config.config_dir / self.config.settings_file)
self.init_config()
+ QDir.addSearchPath('local_folder', os.path.join(path, 'local'))
+ self.theme2: AppTheme = AppTheme(self.config.ui_scale)
self.prepare_tooltip_css()
self.init_environment()
self.downloader = Downloader(
@@ -210,17 +207,18 @@ def cache_icons(self):
self.cache.icons['link'] = load_icon('external_link.png', self.app_dir)
self.cache.icons['dual_cannons'] = load_icon('DC_icon.svg', self.app_dir).pixmap(16, 24.5)
self.cache.icons['ground'] = load_icon('ground_icon.png', self.app_dir).pixmap(
- self.box_width * 1.2, self.box_width * 1.2)
+ self.theme2.opt.box_width * 1.2, self.theme2.opt.box_width * 1.2)
self.cache.icons['tac'] = load_icon('tac_icon.png', self.app_dir).pixmap(
- self.box_width, self.box_width)
+ self.theme2.opt.box_width, self.theme2.opt.box_width)
self.cache.icons['tac-small'] = load_icon('tac-small.svg', self.app_dir).pixmap(25, 25)
self.cache.icons['sci'] = load_icon('sci_icon.png', self.app_dir).pixmap(
- self.box_width, self.box_width)
+ self.theme2.opt.box_width, self.theme2.opt.box_width)
self.cache.icons['sci-small'] = load_icon('sci-small.svg', self.app_dir).pixmap(25, 25)
self.cache.icons['eng'] = load_icon('eng_icon.png', self.app_dir).pixmap(
- self.box_width, self.box_width)
+ self.theme2.opt.box_width, self.theme2.opt.box_width)
self.cache.icons['STOCD'] = load_icon('stocd.png', self.app_dir).pixmap(
self.box_height, self.box_height * 182 / 106)
+ self.theme2.icons = self.cache.icons
def cache_item_aliases(self):
"""
@@ -235,6 +233,7 @@ def main_window_close_callback(self, event):
window_geometry = self.window.saveGeometry()
self.settings.state__geometry = window_geometry
self.autosave()
+ self.settings.store_settings()
event.accept()
# ----------------------------------------------------------------------------------------------
@@ -253,7 +252,7 @@ def create_main_window(self, argv=[]) -> tuple[QApplication, QWidget]:
get_asset_path('Overpass-VariableFont_wght.ttf', self.app_dir))
font_database.addApplicationFont(
get_asset_path('RobotoMono-Regular.ttf', self.app_dir))
- app.setStyleSheet(self.create_style_sheet(self.theme['app']['style']))
+ app.setStyleSheet(self.theme2.create_style_sheet(self.theme2['app']['style']))
window = QWidget()
window.setWindowIcon(load_icon('SETS_icon_small.png', self.app_dir))
window.setWindowTitle('STO Equipment and Trait Selector')
@@ -277,7 +276,7 @@ def setup_main_layout(self):
main_layout = VBoxLayout(margins=0, spacing=0)
banner = ImageLabel(get_asset_path('sets_banner.png', self.app_dir), (2880, 126))
main_layout.addWidget(banner)
- frame_width = 8 * self.config.ui_scale
+ frame_width = 8 * self.theme2.scale
tabber_layout = VBoxLayout(margins=frame_width, spacing=0)
splash_tabber = QTabWidget()
splash_tabber.setStyleSheet(self.get_style_class('QTabWidget', 'tabber'))
@@ -297,7 +296,7 @@ def setup_main_layout(self):
content_layout.setColumnStretch(0, 1)
content_layout.setColumnStretch(1, 4)
- margin = 3 * self.config.ui_scale
+ margin = 3 * self.theme2.scale
menu_layout = GridLayout(margins=(margin, margin, margin, 0), spacing=0)
menu_layout.setColumnStretch(0, 2)
menu_layout.setColumnStretch(1, 5)
@@ -369,7 +368,7 @@ def setup_main_layout(self):
seperator = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@sets', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- seperator.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
+ seperator.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
sidebar_layout.addWidget(seperator, 0, 1, 2, 1)
sidebar.setLayout(sidebar_layout)
content_layout.addWidget(sidebar, 1, 0)
@@ -398,7 +397,7 @@ def setup_ship_frame(self):
Creates ship info frame
"""
frame = self.widgets.sidebar_frames[0]
- csp = self.theme['defaults']['csp'] * self.config.ui_scale
+ csp = self.theme2['defaults']['csp'] * self.theme2.scale
layout = VBoxLayout(margins=csp, spacing=csp)
image_frame = self.create_frame(size_policy=SMINMIN)
@@ -418,7 +417,7 @@ def setup_ship_frame(self):
ship_selector.setSizePolicy(SMINMAX)
ship_selector.setStyleSheet(
self.get_style_class('ShipButton', 'button', override={'margin': 0}))
- ship_selector.setFont(self.theme_font(font_spec='@subhead'))
+ ship_selector.setFont(self.theme2.get_font(font_spec='@subhead'))
ship_selector.clicked.connect(self.select_ship)
self.widgets.ship['button'] = ship_selector
ship_layout.addWidget(ship_selector, 0, 0, 1, 4, alignment=ATOP)
@@ -453,7 +452,7 @@ def setup_ship_frame(self):
desc_edit = QPlainTextEdit()
desc_edit.setSizePolicy(SMINMIN)
desc_edit.setStyleSheet(self.get_style_class('QPlainTextEdit', 'textedit'))
- desc_edit.setFont(self.theme_font('textedit'))
+ desc_edit.setFont(self.theme2.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
desc_edit.textChanged.connect(lambda: self.set_build_item(
self.build['space'], 'ship_desc', desc_edit.toPlainText(), autosave=False))
@@ -478,7 +477,7 @@ def setup_space_build_frame(self):
Creates space build layout
"""
frame = self.widgets.build_frames[0]
- isp = self.theme['defaults']['isp'] * 2 * self.config.ui_scale
+ isp = self.theme2['defaults']['isp'] * 2 * self.theme2.scale
layout = GridLayout(margins=isp, spacing=isp)
layout.setColumnStretch(0, 1)
layout.setColumnStretch(10, 1)
@@ -500,7 +499,7 @@ def setup_space_build_frame(self):
layout.addLayout(hangar_layout, 4, 1, alignment=ALEFT)
sep1 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep1.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
+ sep1.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep1, 0, 2, 5, 1)
deflector_layout = self.create_build_section('Deflector', 1, 'space', 'deflector', True)
@@ -516,7 +515,7 @@ def setup_space_build_frame(self):
layout.addLayout(shield_layout, 4, 3, alignment=ALEFT)
sep2 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep2.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
+ sep2.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep2, 0, 4, 5, 1)
uni_layout = self.create_build_section(
@@ -533,7 +532,7 @@ def setup_space_build_frame(self):
layout.addLayout(tac_layout, 3, 5, alignment=ALEFT)
sep3 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep3.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
+ sep3.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep3, 0, 6, 5, 1)
# Boffs
@@ -573,7 +572,7 @@ def setup_space_build_frame(self):
layout.addLayout(trait_layout, 0, 9, 6, 1, alignment=ATOP)
# Doffs
- spacing = self.theme['defaults']['bw'] * self.config.ui_scale
+ spacing = self.theme2['defaults']['bw'] * self.theme2.scale
doff_container = self.create_frame(size_policy=SMINMAX)
doff_container_layout = VBoxLayout(spacing=spacing * 2)
doff_label = self.create_label('Space Duty Officers')
@@ -596,7 +595,7 @@ def setup_ground_build_frame(self):
Creates Ground build frame
"""
frame = self.widgets.build_frames[1]
- isp = self.theme['defaults']['isp'] * 2 * self.config.ui_scale
+ isp = self.theme2['defaults']['isp'] * 2 * self.theme2.scale
layout = GridLayout(margins=isp, spacing=isp)
layout.setColumnStretch(0, 1)
layout.setColumnStretch(8, 1)
@@ -611,7 +610,7 @@ def setup_ground_build_frame(self):
layout.addLayout(devices_layout, 2, 1, alignment=ALEFT)
sep1 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep1.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
+ sep1.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep1, 0, 2)
kit_layout = self.create_build_section('Kit Frame:', 1, 'ground', 'kit', True)
layout.addLayout(kit_layout, 0, 3, alignment=ALEFT)
@@ -623,7 +622,7 @@ def setup_ground_build_frame(self):
layout.addLayout(shield_layout, 3, 3, alignment=ALEFT)
sep2 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep2.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
+ sep2.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep2, 0, 4)
# Boffs
@@ -637,7 +636,7 @@ def setup_ground_build_frame(self):
layout.addLayout(boff_4_layout, 3, 5, alignment=ALEFT)
sep3 = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep3.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
+ sep3.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep3, 0, 6)
# Traits
@@ -652,7 +651,7 @@ def setup_ground_build_frame(self):
layout.addLayout(trait_layout, 0, 7, 4, 1, alignment=ATOP)
# Doffs
- spacing = self.theme['defaults']['bw'] * self.config.ui_scale
+ spacing = self.theme2['defaults']['bw'] * self.theme2.scale
doff_container = self.create_frame(size_policy=SMINMAX)
doff_container_layout = VBoxLayout(spacing=spacing * 2)
doff_label = self.create_label('Ground Duty Officers')
@@ -672,14 +671,14 @@ def setup_ground_build_frame(self):
# sidebar
sidebar_frame = self.widgets.sidebar_frames[1]
- csp = self.theme['defaults']['csp'] * self.config.ui_scale
+ csp = self.theme2['defaults']['csp'] * self.theme2.scale
sidebar_layout = GridLayout(margins=(csp, isp, csp, csp), spacing=csp)
sidebar_layout.setColumnStretch(0, 1)
desc_label = self.create_label('Build Description:')
sidebar_layout.addWidget(desc_label, 0, 0)
desc_edit = QPlainTextEdit()
desc_edit.setStyleSheet(self.get_style_class('QPlainTextEdit', 'textedit'))
- desc_edit.setFont(self.theme_font('textedit'))
+ desc_edit.setFont(self.theme2.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
desc_edit.textChanged.connect(lambda: self.set_build_item(
self.build['ground'], 'ground_desc', desc_edit.toPlainText(), autosave=False))
@@ -691,12 +690,12 @@ def setup_character_frame(self, frame: QFrame):
"""
Creates character customization area.
"""
- csp = self.theme['defaults']['csp'] * self.config.ui_scale
+ csp = self.theme2['defaults']['csp'] * self.theme2.scale
layout = GridLayout(margins=csp, spacing=csp)
layout.setColumnStretch(1, 1)
seperator = self.create_frame(size_policy=SMINMAX, style_override={
'background-color': '@sets', 'margin': '@isp'})
- sep = self.theme['defaults']['sep'] * self.config.ui_scale
+ sep = self.theme2['defaults']['sep'] * self.theme2.scale
seperator.setFixedHeight(sep)
layout.addWidget(seperator, 0, 0, 1, 2, alignment=ATOP) # ATOP makes it respect the margin?
char_name = self.create_entry(placeholder='NAME')
@@ -757,8 +756,8 @@ def setup_space_skill_frame(self):
Creates Space skill GUI
"""
frame = self.widgets.build_frames[2]
- isp = self.theme['defaults']['isp'] * self.config.ui_scale
- csp = self.theme['defaults']['csp'] * self.config.ui_scale
+ isp = self.theme2['defaults']['isp'] * self.theme2.scale
+ csp = self.theme2['defaults']['csp'] * self.theme2.scale
col_layout = GridLayout(margins=isp, spacing=csp)
col_layout.setRowStretch(0, 1)
col_layout.setColumnStretch(0, 3)
@@ -786,7 +785,7 @@ def setup_space_skill_frame(self):
'Captain
(25 points required)',
'Admiral
(35 points required)'
)
- sep_height = self.theme['hr']['height'] * self.config.ui_scale
+ sep_height = self.theme2['hr']['height'] * self.theme2.scale
for rank, skill_groups in enumerate(self.cache.skills['space']):
header_layout = GridLayout(spacing=isp)
left_sep = self.create_frame('hr', size_policy=SMINMAX)
@@ -812,7 +811,7 @@ def setup_space_skill_frame(self):
scroll_area.setWidget(scroll_frame)
seperator = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@sets'})
- seperator.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
+ seperator.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
col_layout.addWidget(seperator, 0, 1)
bonus_bar_container = self.create_frame(size_policy=SMINMIN)
# bonus bars
@@ -851,7 +850,7 @@ def setup_space_skill_frame(self):
sidebar_layout.addWidget(desc_label, 0, 0, 1, 2)
desc_edit = QPlainTextEdit()
desc_edit.setStyleSheet(self.get_style_class('QPlainTextEdit', 'textedit'))
- desc_edit.setFont(self.theme_font('textedit'))
+ desc_edit.setFont(self.theme2.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
desc_edit.textChanged.connect(lambda: self.set_build_item(
self.build['skill_desc'], 'space', desc_edit.toPlainText(), autosave=False))
@@ -870,8 +869,8 @@ def setup_ground_skill_frame(self):
Creates Ground skill GUI
"""
frame = self.widgets.build_frames[3]
- isp = self.theme['defaults']['isp'] * self.config.ui_scale
- csp = self.theme['defaults']['csp'] * self.config.ui_scale
+ isp = self.theme2['defaults']['isp'] * self.theme2.scale
+ csp = self.theme2['defaults']['csp'] * self.theme2.scale
col_layout = GridLayout(margins=isp, spacing=csp)
col_layout.setRowStretch(0, 1)
col_layout.setColumnStretch(0, 3)
@@ -925,7 +924,7 @@ def setup_ground_skill_frame(self):
tree_frame.setLayout(tree_layout)
seperator = self.create_frame(size_policy=SMAXMIN, style_override={
'background-color': '@sets'})
- seperator.setFixedWidth(self.theme['defaults']['sep'] * self.config.ui_scale)
+ seperator.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
col_layout.addWidget(seperator, 0, 1)
bonus_bar_container = self.create_frame(size_policy=SMINMIN)
# bonus bars
@@ -959,7 +958,7 @@ def setup_ground_skill_frame(self):
sidebar_layout.addWidget(desc_label, 0, 0, 1, 2)
desc_edit = QPlainTextEdit()
desc_edit.setStyleSheet(self.get_style_class('QPlainTextEdit', 'textedit'))
- desc_edit.setFont(self.theme_font('textedit'))
+ desc_edit.setFont(self.theme2.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
desc_edit.textChanged.connect(lambda: self.set_build_item(
self.build['skill_desc'], 'ground', desc_edit.toPlainText(), autosave=False))
@@ -996,7 +995,7 @@ def create_context_menu(self) -> ContextMenu:
"""
menu = ContextMenu()
menu.setStyleSheet(self.get_style_class('ContextMenu', 'context_menu'))
- menu.setFont(self.theme_font('context_menu'))
+ menu.setFont(self.theme2.get_font('context_menu'))
menu.addAction(self.cache.icons['copy'], 'Copy Item', self.copy_equipment_item)
menu.addAction(self.cache.icons['paste'], 'Paste Item', self.paste_equipment_item)
menu.addAction(self.cache.icons['clear'], 'Clear Slot', self.clear_slot)
@@ -1018,7 +1017,7 @@ def setup_settings_frame(self):
Populates the settings frame.
"""
settings_frame = self.widgets.build_frames[5]
- isp = self.theme['defaults']['isp'] * self.config.ui_scale
+ isp = self.theme2['defaults']['isp'] * self.theme2.scale
settings_layout = HBoxLayout(margins=(2 * isp, isp, isp, isp), spacing=isp)
scroll_layout = VBoxLayout(margins=(0, isp, 0, 0), spacing=isp)
scroll_layout.setSpacing(isp)
@@ -1143,7 +1142,7 @@ def setup_settings_frame(self):
# sidebar
sidebar_frame = self.widgets.sidebar_frames[5]
- csp = self.theme['defaults']['csp'] * self.config.ui_scale
+ csp = self.theme2['defaults']['csp'] * self.theme2.scale
sidebar_layout = VBoxLayout(margins=csp, spacing=isp)
sidebar_layout.setAlignment(ATOP)
sidebar_layout.addWidget(self.create_label('About SETS:', 'label_heading'), alignment=ALEFT)
diff --git a/src/theme.py b/src/theme.py
index ddc7b35..38a0e75 100644
--- a/src/theme.py
+++ b/src/theme.py
@@ -1,6 +1,6 @@
import copy
-from PySide6.QtGui import QFont, QIcon
+from PySide6.QtGui import QFont, QIcon, QPixmap
WEIGHT_CONVERSION = {
'normal': QFont.Weight.Normal,
@@ -41,14 +41,15 @@ def __init__(self, scale: float, theme_tree: dict[str] = {}, theme_options: dict
- :param theme_tree: options that affect the UI, but are not directly related to the style
"""
self.scale: float = scale
- self.icons: dict[str, QIcon] = dict()
+ self.icons: dict[str, QIcon | QPixmap] = dict()
self.opt: ThemeOptions = ThemeOptions(theme_options)
- self.opt.box_height = self.opt.default_box_height * self.scale
- self.opt.box_width = self.opt.default_box_width * self.scale
+ self.opt.box_height = self.opt.default_box_height * self.scale * 0.8
+ self.opt.box_width = self.opt.default_box_width * self.scale * 0.8
if len(theme_tree) > 0:
self._theme_data: dict[str, dict] = theme_tree
else:
self._theme_data: dict[str, dict] = self.get_default_theme()
+ self.prepare_tooltip_css()
def __getitem__(self, key: str):
return self._theme_data[key]
@@ -156,7 +157,7 @@ def get_css(self, style: dict[str]) -> str:
style_sheet += f'{prop}:{prop_value};'
return style_sheet
- def get_font(self, widget: str, font_spec: tuple[str, int, str] | str = ()) -> QFont:
+ def get_font(self, widget: str = '', font_spec: tuple[str, int, str] | str = ()) -> QFont:
"""
Returns QFont object with font specified in current theme or font_spec. Adds default
fallback font families.
@@ -168,15 +169,12 @@ def get_font(self, widget: str, font_spec: tuple[str, int, str] | str = ()) -> Q
:return: configured QFont object
"""
- try:
- if len(font_spec) != 3 and isinstance(font_spec, tuple):
- font_spec = self._theme_data[widget]['font']
- if isinstance(font_spec, str) and font_spec.startswith('@'):
- font = self._theme_data['defaults'][font_spec[1:]]
- else:
- font = font_spec
- except KeyError:
- font = self._theme_data['app']['font']
+ if len(font_spec) != 3 and isinstance(font_spec, tuple):
+ font_spec = self._theme_data[widget]['font']
+ if isinstance(font_spec, str) and font_spec.startswith('@'):
+ font = self._theme_data['defaults'][font_spec[1:]]
+ else:
+ font = font_spec
font_family = (font[0], *self._theme_data['app']['font-fallback'])
font_size = int(font[1] * self.scale)
font_weight = WEIGHT_CONVERSION[font[2]]
@@ -200,6 +198,24 @@ def create_style_sheet(self, d: dict[str, dict]) -> str:
for prop, prop_value in d.items():
style_sheet += f'{prop} {{{self.get_css(prop_value)}}}'
return style_sheet
+
+ def prepare_tooltip_css(self):
+ """
+ Converts dictionaries containing tooltip style to css
+ """
+ ui_scale = self.scale
+ tooltips = self._theme_data['tooltip']
+ for tag, style in self._theme_data['tooltip_def'].items():
+ css = ''
+ for prop, val in style.items():
+ if isinstance(val, int):
+ unit = 'pt' if prop == 'font-size' else 'px'
+ css += f'{prop}:{val * ui_scale}{unit};'
+ elif isinstance(val, tuple):
+ css += f'''{prop}:{'px '.join(map(lambda s: str(s * ui_scale), val))}px;'''
+ else:
+ css += f'{prop}:{val};'
+ tooltips[tag] = css
def get_default_theme(self) -> dict[str, dict]:
"""
@@ -656,6 +672,7 @@ def get_default_theme(self) -> dict[str, dict]:
'border-width': '@bw',
'border-style': 'solid',
'border-radius': '@br',
+ 'font': '@font',
'padding': '@sep',
'::item': {
'color': '@fg',
From f0b9fab87891667df6b9af7fccbbd888ca3a7e3e Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Tue, 5 May 2026 07:40:37 +0200
Subject: [PATCH 05/44] making cargomanager build cache
---
src/cargomanager.py | 348 +++++++++++++++++++++++++++++++++++++++++++-
src/config.py | 2 +-
src/iofunc.py | 19 ++-
src/textedit.py | 59 ++++++++
src/theme.py | 61 +++++---
5 files changed, 464 insertions(+), 25 deletions(-)
diff --git a/src/cargomanager.py b/src/cargomanager.py
index c2b9487..05fd530 100644
--- a/src/cargomanager.py
+++ b/src/cargomanager.py
@@ -1,24 +1,107 @@
from pathlib import Path
from time import time
-from .constants import SEVEN_DAYS_IN_SECONDS
-from .iofunc import load_json__new
+from .config import SETSSettings
+from .constants import (
+ CAREERS, BOFF_RANKS, DOFF_QUERY_URL, EQUIPMENT_TYPES, ITEM_QUERY_URL, MODIFIER_QUERY,
+ PRIMARY_SPECS, SEVEN_DAYS_IN_SECONDS, SHIP_QUERY_URL, STARSHIP_TRAIT_QUERY_URL, TRAIT_QUERY_URL,
+ TRAYSKILL_QUERY)
+from .downloader import Downloader
+from .iofunc import load_json__new, store_json__new
+from .textedit import (
+ create_equipment_tooltip__new, create_trait_tooltip__new, dewikify, parse_wikitext,
+ sanitize_equipment_name)
+from .theme import AppTheme
class CargoManager():
"""Manages Cargo data and cache"""
- def __init__(self, folders: dict[str, Path]):
+ def __init__(
+ self, folders: dict[str, Path], downloader: Downloader, settings: SETSSettings,
+ theme: AppTheme):
"""
Parameters:
- :param folders: folder names and paths of config folder
"""
self._folders: dict[str, Path] = folders
- self.boff_abilities: dict[str, dict[str, dict]] = {
+ self._downloader: Downloader = downloader
+ self._settings: SETSSettings = settings
+ self._theme: AppTheme = theme
+ self.ships: dict[str, dict[str]] = dict()
+ self.equipment: dict[str, dict[str, dict[str]]] = {
+ equipment_type: dict() for equipment_type in EQUIPMENT_TYPES.values()}
+ self.modifiers: dict[str, dict[str, dict[str, str | bool]]] = {
+ type_: dict() for type_ in EQUIPMENT_TYPES.values()}
+ self.starship_traits: dict[str, dict[str]] = dict()
+ self.space_traits: dict[str, dict[str, dict[str]]] = {
+ 'traits': dict(),
+ 'rep_traits': dict(),
+ 'active_rep_traits': dict()
+ }
+ self.ground_traits: dict[str, dict[str, dict[str]]] = {
+ 'traits': dict(),
+ 'rep_traits': dict(),
+ 'active_rep_traits': dict()
+ }
+ self.ground_doffs: dict[str, dict[str, dict[str]]] = dict()
+ self.space_doffs: dict[str, dict[str, dict[str]]] = dict()
+ self.boff_abilities: dict[str, dict[str, dict[str, list[str]] | dict[str, str]]] = {
'space': self.boff_dict(),
'ground': self.boff_dict(),
'all': dict()
}
+ self.images_set: set[str] = set()
+ self.alt_images: dict[str, str] = dict()
+
+ def provision_cargo_data(self):
+ """
+ (Down-) loads cargo data or gets cached cargo data.
+ """
+ images_updated = False
+ self.ships = self.get_cached_data('ships.json')
+ if self.ships is None:
+ self.cache_ship_data()
+ images_updated = True
+ self.equipment = self.get_cached_data('equipment.json')
+ if self.equipment is None:
+ self.cache_equipment_data()
+ images_updated = True
+ self.space_traits = self.get_cached_data('space_traits.json')
+ self.ground_traits = self.get_cached_data('ground_traits.json')
+ if self.space_traits is None or self.ground_traits is None:
+ self.cache_trait_data()
+ images_updated = True
+ self.starship_traits = self.get_cached_data('starship_traits.json')
+ if self.starship_traits is None:
+ self.cache_starship_trait_data()
+ images_updated = True
+ self.boff_abilities = self.get_cached_data('boff_abilities.json')
+ if self.boff_abilities is None:
+ self.cache_boff_data()
+ images_updated = True
+ self.modifiers = self.get_cached_data('modifiers.json')
+ if self.modifiers is None:
+ self.cache_modifier_data()
+ self.space_doffs = self.get_cached_data('space_doffs.json')
+ self.ground_doffs = self.get_cached_data('ground_doffs.json')
+ if self.space_doffs is None or self.ground_doffs is None:
+ self.cache_duty_officer_data()
+ alt_images = self.get_cached_data('alt_images.json')
+ if alt_images is None:
+ alt_images = dict()
+ all_images = self.get_cached_data('images_list.json')
+ if all_images is None:
+ images_set = set()
+ else:
+ images_set = set(all_images)
+ if images_updated:
+ alt_images.update(self.alt_images)
+ store_json__new(alt_images, self._folders['cache'] / 'alt_images.json')
+ images_set |= self.images_set
+ store_json__new(list(images_set), self._folders['cache'] / 'images_list.json')
+ self.alt_images = alt_images
+ self.images_set = images_set
def get_cached_data(self, file_name: str) -> dict | list | None:
"""
@@ -32,6 +115,263 @@ def get_cached_data(self, file_name: str) -> dict | list | None:
if time() - last_modified < SEVEN_DAYS_IN_SECONDS:
return load_json__new(file_path)
return None
+
+ def cache_ship_data(self):
+ """
+ Retrieves ship data and caches it.
+ """
+ ship_cargo_data: list[dict[str]] = self.get_cargo_data('ship_list.json', SHIP_QUERY_URL)
+ self.ships = {ship['Page']: ship for ship in ship_cargo_data}
+ store_json__new(self.ships, self._folders['cache'] / 'ships.json')
+
+ def cache_equipment_data(self):
+ """
+ Retrieves equipment data and caches it.
+ """
+ equipment_cargo_data: list[dict[str, str | None]] = self.get_cargo_data(
+ 'equipment.json', ITEM_QUERY_URL)
+ equipment_types = set(EQUIPMENT_TYPES.keys())
+ tooltip_styles = self._theme.tooltips
+ elite_hangar = {
+ 'Hangar - Elite Federation Mission Scout Ships',
+ 'Hangar - Elite Valor Fighters'
+ }
+ for item in equipment_cargo_data:
+ if item['type'] in equipment_types:
+ if item['type'] == 'Hangar Bay' and item['name'] not in elite_hangar and (
+ item['name'].startswith('Hangar - Advanced')
+ or item['name'].startswith('Hangar - Elite')):
+ continue
+ name = sanitize_equipment_name(item['name'])
+ self.equipment[EQUIPMENT_TYPES[item['type']]][name] = {
+ 'Page': item['Page'],
+ 'name': name,
+ 'rarity': item['rarity'],
+ 'type': item['type'],
+ 'tooltip': create_equipment_tooltip__new(item, tooltip_styles)
+ }
+ self.images_set.add(name)
+ self.equipment['fore_weapons'].update(self.equipment['ship_weapon'])
+ self.equipment['aft_weapons'].update(self.equipment['ship_weapon'])
+ del self.equipment['ship_weapon']
+ self.equipment['tac_consoles'].update(self.equipment['uni_consoles'])
+ self.equipment['sci_consoles'].update(self.equipment['uni_consoles'])
+ self.equipment['eng_consoles'].update(self.equipment['uni_consoles'])
+ self.equipment['uni_consoles'].update(self.equipment['tac_consoles'])
+ self.equipment['uni_consoles'].update(self.equipment['sci_consoles'])
+ self.equipment['uni_consoles'].update(self.equipment['eng_consoles'])
+ store_json__new(self.equipment, self._folders['cache'] / 'equipment.json')
+
+ def cache_trait_data(self):
+ """
+ Retrieves personal and reputation trait data and caches it.
+ """
+ trait_cargo_data: list[dict[str]] = self.get_cargo_data('traits.json', TRAIT_QUERY_URL)
+ tooltip_styles = self._theme.tooltips
+ for trait in trait_cargo_data:
+ name = trait['name']
+ if trait['type'] != 'doff' and trait['type'] != 'boff' and name is not None:
+ if trait['type'] == 'reputation':
+ trait_type = 'rep_traits'
+ elif trait['type'] == 'activereputation':
+ trait_type = 'active_rep_traits'
+ else:
+ trait_type = 'traits'
+ try:
+ trait_data = {
+ 'Page': trait['Page'],
+ 'name': name,
+ 'tooltip': create_trait_tooltip__new(
+ name, trait['description'], trait_type, trait['environment'],
+ tooltip_styles)
+ }
+ if trait['environment'] == 'space':
+ self.space_traits[trait_type][name] = trait_data
+ else:
+ self.ground_traits[trait_type][name] = trait_data
+ if trait['icon_name'] is None:
+ self.images_set.add(name)
+ else:
+ self.images_set.add(trait['icon_name'])
+ self.alt_images[f'{name}__{trait["environment"]}__{trait_type}'] = (
+ trait['icon_name'])
+ # catch wrong values in trait['environment'] (cargo issue)
+ except (KeyError, AttributeError):
+ pass
+ store_json__new(self.space_traits, 'space_traits.json')
+ store_json__new(self.ground_traits, 'ground_traits.json')
+
+ def cache_starship_trait_data(self):
+ """
+ Retrieves starship trait data and caches it.
+ """
+ shiptrait_cargo = self.get_cargo_data('starship_traits.json', STARSHIP_TRAIT_QUERY_URL)
+ styles = self._theme.tooltips
+ for ship_trait in shiptrait_cargo:
+ name = ship_trait['name']
+ if ship_trait['icon_name'] is None:
+ self.images_set.add(name)
+ else:
+ self.images_set.add(ship_trait['icon_name'])
+ self.alt_images[f"{name}__space__starship_traits"] = ship_trait['icon_name']
+ self.starship_traits[name] = {
+ 'Page': ship_trait['Page'],
+ 'name': name,
+ 'obtained': ship_trait['obtained'],
+ 'tooltip': (
+ f"{name}
"
+ f"Starship Trait
"
+ f"{ship_trait['short']}
{parse_wikitext(ship_trait['detailed'], styles)}")
+ }
+ store_json__new(self.starship_traits, 'starship_traits.json')
+
+ def cache_boff_data(self):
+ """
+ Retrieves bridge officer data and caches it.
+ """
+ boff_cargo: list[dict[str, str]] = self.get_cargo_data(
+ 'boff_abilities.json', TRAYSKILL_QUERY)
+ boff_types = CAREERS | PRIMARY_SPECS
+ styles = self._theme.tooltips
+ rank_numbers = ((1, 'I'), (2, 'II'), (3, 'III'))
+ for boff_ability in boff_cargo:
+ boff_region = boff_ability['region'].lower()
+ boff_type = boff_ability['type']
+ if boff_type not in boff_types or boff_region != 'space' and boff_region != 'ground':
+ continue
+ boff_name = boff_ability['name']
+ ability_item = {
+ 'Page': boff_ability['_pageName'],
+ 'name': boff_name,
+ 'I': '',
+ 'II': '',
+ 'III': ''
+ }
+ desc = boff_ability['description']
+ desc_long = boff_ability['description long']
+ for decimal, roman in rank_numbers:
+ rank_id = BOFF_RANKS.get(boff_ability[f'rank{decimal}rank'], 0) - 1
+ if rank_id >= 0:
+ self.boff_abilities[boff_region][boff_type][rank_id].append(
+ boff_name + ' ' + roman)
+ ability_item[roman] = (
+ f"{boff_name} {roman}
"
+ f"{desc}
{desc_long}
"
+ f"{parse_wikitext(dewikify(boff_ability[f'rank{decimal}info']), styles)}")
+ self.boff_abilities['all'][boff_name] = ability_item
+ self.images_set |= self.boff_abilities['all'].keys()
+ store_json__new(self.boff_abilities, 'boff_abilities.json')
+
+ def cache_modifier_data(self):
+ """
+ Retrieves modifier data and caches it.
+ """
+ mod_cargo_data: list[dict[str, str | list[str] | int | None]] = self.get_cargo_data('modifiers.json', MODIFIER_QUERY)
+ for modifier in mod_cargo_data:
+ try:
+ if modifier['available'][0] == '':
+ modifier['available'] = list()
+ except (IndexError, TypeError):
+ modifier['available'] = list()
+ for mod_type in modifier['type']:
+ mod_name = modifier['modifier'].replace('>', '>')
+ try:
+ epic = bool(modifier['isepic'])
+ self.modifiers[EQUIPMENT_TYPES[mod_type]][mod_name] = {
+ 'stats': modifier['stats'],
+ 'available': modifier['available'],
+ 'epic': epic,
+ 'isunique': False if epic else bool(modifier['isunique']),
+ }
+ except KeyError:
+ pass
+ self.modifiers['fore_weapons'].update(self.modifiers['ship_weapon'])
+ self.modifiers['aft_weapons'].update(self.modifiers['ship_weapon'])
+ del self.modifiers['ship_weapon']
+ self.modifiers['uni_consoles'].update(self.modifiers['sci_consoles'])
+ self.modifiers['uni_consoles'].update(self.modifiers['eng_consoles'])
+ self.modifiers['uni_consoles'].update(self.modifiers['tac_consoles'])
+ store_json__new(self.modifiers, 'modifiers.json')
+
+ def cache_duty_officer_data(self):
+ """
+ Retrieves duty officer data and caches it.
+ """
+ doff_cargo_data = self.get_cargo_data('doffs.json', DOFF_QUERY_URL)
+ for doff in doff_cargo_data:
+ doff['description'] = dewikify(doff['description'], remove_formatting=True)
+ for rarity in ('white', 'green', 'blue', 'purple', 'violet', 'gold'):
+ if isinstance(doff[rarity], str):
+ doff[rarity] = dewikify(doff[rarity], remove_formatting=True)
+ if doff['shipdutytype'] == 'Space':
+ self.cache_doff_single(self.space_doffs, doff)
+ elif doff['shipdutytype'] == 'Ground':
+ self.cache_doff_single(self.ground_doffs, doff)
+ elif doff['shipdutytype'] is not None:
+ self.cache_doff_single(self.space_doffs, doff)
+ self.cache_doff_single(self.ground_doffs, doff)
+ store_json__new(self.space_doffs, 'space_doffs.json')
+ store_json__new(self.ground_doffs, 'ground_doffs.json')
+
+ def cache_doff_single(self, cache: dict, doff: dict):
+ """
+ Puts a single doff into cache.
+
+ Parameters:
+ - :param cache: cache dictionary to store doff into
+ - :param doff: the doff itself
+ """
+ try:
+ cache[doff['spec']][doff['description']] = doff
+ except KeyError:
+ cache[doff['spec']] = dict()
+ cache[doff['spec']][doff['description']] = doff
+
+ def get_cargo_data(
+ self, filename: str, url: str, ignore_cache_age: bool = False) -> dict | list:
+ """
+ Retrieves cargo data for specific table. Downloads cargo data from wiki if cargo cache is
+ empty. Updates cargo cache.
+
+ Parameters:
+ - :param filename: filename of cache file
+ - :param url: url to cargo table
+ - :param ignore_cache_age: True if cache of any age should be accepted
+ """
+ cargo_file = self._folders['cargo'] / filename
+
+ # try loading from cache
+ if cargo_file.is_file():
+ last_modified = cargo_file.stat().st_mtime
+ if time() - last_modified < SEVEN_DAYS_IN_SECONDS or ignore_cache_age:
+ cargo_data = load_json__new(cargo_file)
+ if cargo_data is not None:
+ return cargo_data
+
+ # download cargo data if loading from cache failed or data should be updated
+ cargo_data = self._downloader.download_cargo_table(url, filename)
+ if cargo_data is None:
+ if ignore_cache_age:
+ backup_path = self._folders['backups'] / filename
+ auto_backup_path = self._folders['auto_backups'] / filename
+ if self._settings.pref_backup == 0:
+ backup_paths = (auto_backup_path, backup_path)
+ else:
+ backup_paths = (backup_path, auto_backup_path)
+ for path in backup_paths:
+ if path.is_file():
+ cargo_data = load_json__new(path)
+ if cargo_data is not None:
+ store_json__new(cargo_data, cargo_file)
+ return cargo_data
+ # TODO what happens when both backups fail?
+ else:
+ return self.get_cargo_data(filename, url, ignore_cache_age=True)
+ else:
+ if cargo_file.is_file():
+ cargo_file.copy_into(self._folders['auto_backups'])
+ store_json__new(cargo_data, cargo_file)
+ return cargo_data
def boff_dict(self):
return {
diff --git a/src/config.py b/src/config.py
index 82595e1..8ca6fc1 100644
--- a/src/config.py
+++ b/src/config.py
@@ -47,7 +47,7 @@ def __init__(self, settings_file_path: Path):
self.default_save_format: str = 'JSON'
self.default_rarity: str = 'Common'
self.picker_relative: int = 0
- self.pref_backup: int = 0
+ self.pref_backup: int = 0 # 0: auto backup preferred, 1: manual backup preferred
self.ui_scale: float = 1
self.state__geometry: QByteArray = QByteArray()
diff --git a/src/iofunc.py b/src/iofunc.py
index 862936e..7659c3a 100644
--- a/src/iofunc.py
+++ b/src/iofunc.py
@@ -1,6 +1,6 @@
from datetime import datetime
import json
-from json import load as json__load, JSONDecodeError
+from json import dump as json__dump, load as json__load, JSONDecodeError
import os
from pathlib import Path
from shutil import copyfile as shutil__copyfile, rmtree as shutil__rmtree
@@ -364,6 +364,23 @@ def load_json(path: str) -> dict | list:
return data
+def store_json__new(data: dict | list, path: Path) -> bool:
+ """
+ Stores data to json file at path. Overwrites file at target location. Raises ValueError if path
+ is not absolute. Returns `False` if file could not be saved, `True` otherwise.
+
+ Paramters:
+ - :param data: dictionary or list that should be stored
+ - :param path: file path to store the data to
+ """
+ try:
+ with path.open('w') as file:
+ json__dump(data, file)
+ return True
+ except OSError:
+ return False
+
+
def store_json(data: dict | list, path: str):
"""
Stores data to json file at path. Overwrites file at target location. Raises ValueError if path
diff --git a/src/textedit.py b/src/textedit.py
index fc80cba..a8b8cca 100644
--- a/src/textedit.py
+++ b/src/textedit.py
@@ -1,6 +1,7 @@
from re import sub as re_sub
from .constants import CAREER_ABBR, RARITY_COLORS, SKILL_PREFIXES, WIKI_URL
+from .theme import TooltipCSS
def get_tooltip(self, name: str, type_: str, environment: str = 'space') -> str:
@@ -182,6 +183,32 @@ def create_equipment_tooltip(
f"{parse_wikitext(dewikify(item[f'text{i}']), tags)}
")
return tooltip
+def create_equipment_tooltip__new(item: dict, tooltip_style: TooltipCSS) -> str:
+ """
+ Creates tooltip for equipment from raw item data.
+
+ Parameters:
+ - :param item: item data (from cargo table)
+ - :param tooltip_style: object containing style data
+ """
+ tooltip = ''
+ if item['who'] is not None:
+ tooltip += f"{item['who']}
"
+ for i in range(1, 10, 1):
+ if item[f'head{i}'] is not None:
+ tooltip += (
+ f""
+ f"{format_wikitext(dewikify(item[f'head{i}']))}
")
+ if item[f'subhead{i}'] is not None:
+ tooltip += (
+ f""
+ f"{format_wikitext(dewikify(item[f'subhead{i}']))}
")
+ if item[f'text{i}'] is not None:
+ tooltip += (
+ f""
+ f"{parse_wikitext(dewikify(item[f'text{i}']), tooltip_style)}
")
+ return tooltip
+
def create_trait_tooltip(
name: str, description: str, type_: str, environment: str, head_style: str,
@@ -217,6 +244,38 @@ def create_trait_tooltip(
tooltip = ''
return tooltip
+def create_trait_tooltip__new(
+ name: str, description: str, type_: str, environment: str,
+ styles: TooltipCSS) -> str:
+ """
+ Creates tooltip for trait from trait description.
+
+ Parameters:
+ - :param name: name of the trait
+ - :param description: description of the trait
+ - :param type_: type of the trait; one of "traits", "rep_traits", "active_rep_traits"
+ - :param environment: "space" / "ground"
+ - :param styles: object containing style data
+ """
+ if type_ == 'traits':
+ tooltip = (
+ f"{name}
"
+ f"Personal {environment.capitalize()} Trait
"
+ f"{parse_wikitext(dewikify(description), styles)}
")
+ elif type_ == 'rep_traits':
+ tooltip = (
+ f"{name}
"
+ f"{environment.capitalize()} Reputation Trait
"
+ f"{parse_wikitext(dewikify(description), styles)}
")
+ elif type_ == 'active_rep_traits':
+ tooltip = (
+ f"{name}
"
+ f"Active {environment.capitalize()} Reputation Trait
"
+ f"{parse_wikitext(dewikify(description), styles)}
")
+ else:
+ tooltip = ''
+ return tooltip
+
def parse_wikitext(text: str, tags) -> str:
"""
diff --git a/src/theme.py b/src/theme.py
index 38a0e75..1c2084c 100644
--- a/src/theme.py
+++ b/src/theme.py
@@ -30,6 +30,47 @@ def __init__(self, initial_options: dict[str] = {}):
setattr(self, option_name, option_value)
+class TooltipCSS:
+ """
+ Contains css used for styling tooltips.
+ """
+
+ __slots__ = ('boff_header', 'boff_subheader', 'equipment_head', 'equipment_name',
+ 'equipment_subhead', 'equipment_type_subheader', 'equipment_who', 'indent', 'li',
+ 'skill_ultimate_name', 'trait_header', 'trait_subheader', 'ul')
+
+ def __init__(self, tooltips: dict[str, dict[str]], scale: float):
+ self.boff_header: str = self.get_tooltip_css(tooltips['boff_header'], scale)
+ self.boff_subheader: str = self.get_tooltip_css(tooltips['boff_subheader'], scale)
+ self.equipment_head: str = self.get_tooltip_css(tooltips['equipment_head'], scale)
+ self.equipment_name: str = self.get_tooltip_css(tooltips['equipment_name'], scale)
+ self.equipment_subhead: str = self.get_tooltip_css(tooltips['equipment_subhead'], scale)
+ self.equipment_type_subheader: str = self.get_tooltip_css(
+ tooltips['equipment_type_subheader'], scale)
+ self.equipment_who: str = self.get_tooltip_css(tooltips['equipment_who'], scale)
+ self.indent: str = self.get_tooltip_css(tooltips['indent'], scale)
+ self.li: str = self.get_tooltip_css(tooltips['li'], scale)
+ self.skill_ultimate_name: str = self.get_tooltip_css(tooltips['skill_ultimate_name'], scale)
+ self.trait_header: str = self.get_tooltip_css(tooltips['trait_header'], scale)
+ self.trait_subheader: str = self.get_tooltip_css(tooltips['trait_subheader'], scale)
+ self.ul: str = self.get_tooltip_css(tooltips['ul'], scale)
+
+ def get_tooltip_css(self, style_data: dict[str], scale: float):
+ """
+ Converts dictionary containing tooltip style to css
+ """
+ css = str()
+ for prop, val in style_data.items():
+ if isinstance(val, int):
+ unit = 'pt' if prop == 'font-size' else 'px'
+ css += f'{prop}:{val * scale}{unit};'
+ elif isinstance(val, tuple):
+ css += f'''{prop}:{'px '.join(map(lambda s: str(s * scale), val))}px;'''
+ else:
+ css += f'{prop}:{val};'
+ return css
+
+
class AppTheme:
"""Encapsulates theme functions and data."""
@@ -49,7 +90,7 @@ def __init__(self, scale: float, theme_tree: dict[str] = {}, theme_options: dict
self._theme_data: dict[str, dict] = theme_tree
else:
self._theme_data: dict[str, dict] = self.get_default_theme()
- self.prepare_tooltip_css()
+ self.tooltips: TooltipCSS = TooltipCSS(self._theme_data['tooltip_def'], scale)
def __getitem__(self, key: str):
return self._theme_data[key]
@@ -198,24 +239,6 @@ def create_style_sheet(self, d: dict[str, dict]) -> str:
for prop, prop_value in d.items():
style_sheet += f'{prop} {{{self.get_css(prop_value)}}}'
return style_sheet
-
- def prepare_tooltip_css(self):
- """
- Converts dictionaries containing tooltip style to css
- """
- ui_scale = self.scale
- tooltips = self._theme_data['tooltip']
- for tag, style in self._theme_data['tooltip_def'].items():
- css = ''
- for prop, val in style.items():
- if isinstance(val, int):
- unit = 'pt' if prop == 'font-size' else 'px'
- css += f'{prop}:{val * ui_scale}{unit};'
- elif isinstance(val, tuple):
- css += f'''{prop}:{'px '.join(map(lambda s: str(s * ui_scale), val))}px;'''
- else:
- css += f'{prop}:{val};'
- tooltips[tag] = css
def get_default_theme(self) -> dict[str, dict]:
"""
From c00b157d6946074b9f89921972d6029b60f2837e Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Tue, 5 May 2026 12:26:42 +0200
Subject: [PATCH 06/44] adding skills to cargomanager
---
src/cargomanager.py | 25 +++++++++++++++++++++++--
1 file changed, 23 insertions(+), 2 deletions(-)
diff --git a/src/cargomanager.py b/src/cargomanager.py
index 05fd530..c1dcb44 100644
--- a/src/cargomanager.py
+++ b/src/cargomanager.py
@@ -18,13 +18,14 @@ class CargoManager():
"""Manages Cargo data and cache"""
def __init__(
- self, folders: dict[str, Path], downloader: Downloader, settings: SETSSettings,
- theme: AppTheme):
+ self, folders: dict[str, Path], app_dir: Path, downloader: Downloader,
+ settings: SETSSettings, theme: AppTheme):
"""
Parameters:
- :param folders: folder names and paths of config folder
"""
self._folders: dict[str, Path] = folders
+ self._app_dir: Path = app_dir
self._downloader: Downloader = downloader
self._settings: SETSSettings = settings
self._theme: AppTheme = theme
@@ -51,9 +52,29 @@ def __init__(
'ground': self.boff_dict(),
'all': dict()
}
+ self.item_aliases: dict = dict()
+ self.skills = {
+ 'space': dict(),
+ 'space_unlocks': dict(),
+ 'ground': dict(),
+ 'ground_unlocks': dict()
+ }
self.images_set: set[str] = set()
self.alt_images: dict[str, str] = dict()
+ def load_static_data(self):
+ """
+ Loads skill data and item aliases.
+ """
+ local_folder = self._app_dir / 'local'
+ self.item_aliases = load_json__new(local_folder / 'aliases.json')
+ space_skill_data = load_json__new(local_folder / 'space_skills.json')
+ self.skills['space'] = space_skill_data['space']
+ self.skills['space_unlocks'] = space_skill_data['space_unlocks']
+ ground_skill_data = load_json__new(local_folder / 'ground_skills.json')
+ self.skills['ground'] = ground_skill_data['ground']
+ self.skills['ground_unlocks'] = ground_skill_data['ground_unlocks']
+
def provision_cargo_data(self):
"""
(Down-) loads cargo data or gets cached cargo data.
From a5ec3bc520babeddd6496bc30dfae54661ac6f6c Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 6 May 2026 16:34:32 +0200
Subject: [PATCH 07/44] adding build manager
---
src/buildhelpers.py | 166 +++++++++++
src/buildmanager.py | 706 ++++++++++++++++++++++++++++++++++++++++++++
src/imagemanager.py | 85 +++++-
src/textedit.py | 62 ++++
src/widgets.py | 33 +++
5 files changed, 1046 insertions(+), 6 deletions(-)
create mode 100644 src/buildhelpers.py
create mode 100644 src/buildmanager.py
diff --git a/src/buildhelpers.py b/src/buildhelpers.py
new file mode 100644
index 0000000..2ad1b31
--- /dev/null
+++ b/src/buildhelpers.py
@@ -0,0 +1,166 @@
+from .constants import BOFF_RANKS, BUILD_VERSION
+
+
+def empty_build(build_type: str = 'full') -> dict[str, int | dict[str]]:
+ """
+ Creates empty build and returns it.
+
+ Parameters:
+ - :param build_type: `build` -> space and ground build; `skills` -> space and ground skills;
+ `full` -> space and ground build and skills
+ """
+ # None means not available on the build; empty string means empty slot
+ new_build = {
+ '_version': BUILD_VERSION,
+ 'space': {
+ 'active_rep_traits': [None] * 5,
+ 'aft_weapons': [None] * 5,
+ 'boffs': [[None] * 4, [None] * 4, [None] * 4, [None] * 4, [None] * 4, [None] * 4],
+ 'boff_specs': [[None, None]] * 6,
+ 'core': [''],
+ 'deflector': [''],
+ 'devices': [None] * 6,
+ 'doffs_spec': [''] * 6,
+ 'doffs_variant': [''] * 6,
+ 'eng_consoles': [None] * 5,
+ 'engines': [''],
+ 'experimental': [None],
+ 'fore_weapons': [None] * 5,
+ 'hangars': [None] * 2,
+ 'rep_traits': [None] * 5,
+ 'sci_consoles': [None] * 5,
+ 'sec_def': [None],
+ 'shield': [''],
+ 'ship': '',
+ 'ship_name': '',
+ 'ship_desc': '',
+ 'starship_traits': [None] * 7,
+ 'tac_consoles': [None] * 5,
+ 'tier': '',
+ 'traits': ['', '', '', '', '', '', '', '', '', None, None, ''],
+ 'uni_consoles': [None] * 3,
+ },
+ 'ground': {
+ 'active_rep_traits': [None] * 5,
+ 'armor': [''],
+ 'boffs': [[''] * 4, [''] * 4, [''] * 4, [''] * 4],
+ 'boff_profs': ['Tactical'] * 4,
+ 'boff_specs': ['Command'] * 4,
+ 'ground_desc': '',
+ 'ground_devices': ['', '', '', '', None],
+ 'doffs_spec': [''] * 6,
+ 'doffs_variant': [''] * 6,
+ 'ev_suit': [''],
+ 'kit': [''],
+ 'kit_modules': ['', '', '', '', '', None],
+ 'rep_traits': [''] * 5,
+ 'personal_shield': [''],
+ 'traits': ['', '', '', '', '', '', '', '', '', None, None, ''],
+ 'weapons': [''] * 2,
+ },
+ 'captain': {
+ 'career': '',
+ 'elite': False,
+ 'faction': '',
+ 'name': '',
+ 'primary_spec': '',
+ 'secondary_spec': '',
+ 'species': '',
+ },
+ }
+
+ new_skills = {
+ '_version': BUILD_VERSION,
+ 'space_skills': {
+ 'eng': [False] * 30,
+ 'sci': [False] * 30,
+ 'tac': [False] * 30,
+ },
+ 'skill_unlocks': {
+ 'eng': [None] * 5,
+ 'sci': [None] * 5,
+ 'tac': [None] * 5,
+ 'ground': [None] * 5
+ },
+ 'ground_skills': [
+ [False] * 6,
+ [False] * 6,
+ [False] * 4,
+ [False] * 4
+ ],
+ 'skill_desc': {
+ 'space': '',
+ 'ground': ''
+ }
+ }
+
+ if build_type == 'build':
+ return new_build
+ elif build_type == 'full':
+ new_build.update(new_skills)
+ return new_build
+ elif build_type == 'skills':
+ return new_skills
+
+def get_variable_slot_counts(ship_data: dict[str], ship_tier: str) -> tuple[int]:
+ """
+ returns the number of universal consoles, devices and starship traits the given ship build
+ should have
+
+ Parameters:
+ - :param ship_data: ship specifications
+ - :param ship_tier: selected ship tier
+
+ :return: 6-tuple containing universal consoles, engineering consoles, science consoles, \
+ tactical consoles, devices, starship traits
+ """
+ if ship_data['name'] == '':
+ uni_consoles = 3
+ starship_traits = 7
+ devices = 6
+ eng_consoles = 5
+ sci_consoles = 5
+ tac_consoles = 5
+ else:
+ uni_consoles = 0
+ starship_traits = 5
+ devices = ship_data['devices']
+ eng_consoles = ship_data['consoleseng']
+ sci_consoles = ship_data['consolessci']
+ tac_consoles = ship_data['consolestac']
+ if 'Innovation Effects' in ship_data['abilities']:
+ uni_consoles += 1
+ elif ship_data['name'] == 'Federation Intel Holoship':
+ uni_consoles += 1
+ if '-X2' in ship_tier:
+ uni_consoles += 2
+ starship_traits += 2
+ devices += 2
+ elif '-X' in ship_tier:
+ uni_consoles += 1
+ starship_traits += 1
+ devices += 1
+ if ship_tier.startswith(('T5-U', 'T5-X')):
+ if ship_data['t5uconsole'] == 'eng':
+ eng_consoles += 1
+ elif ship_data['t5uconsole'] == 'sci':
+ sci_consoles += 1
+ elif ship_data['t5uconsole'] == 'tac':
+ tac_consoles += 1
+ return uni_consoles, eng_consoles, sci_consoles, tac_consoles, devices, starship_traits
+
+def get_boff_spec(seat_details: str) -> tuple[int, str, str]:
+ """
+ Returns rank, profession and specialization from cargo string
+
+ Parameters:
+ - :param seat_details: contains rank, profession and specialization:
+ " -"
+ """
+ if '-' in seat_details:
+ rank_and_profession, spec = seat_details.split('-')
+ else:
+ rank_and_profession = seat_details
+ spec = ''
+ rank_name, _, profession = rank_and_profession.rpartition(' ')
+ return (BOFF_RANKS[rank_name], profession, spec)
diff --git a/src/buildmanager.py b/src/buildmanager.py
new file mode 100644
index 0000000..e60b920
--- /dev/null
+++ b/src/buildmanager.py
@@ -0,0 +1,706 @@
+from pathlib import Path
+
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import QCheckBox, QComboBox, QLabel, QLineEdit, QPlainTextEdit, QPushButton
+
+from .buildhelpers import get_boff_spec, get_variable_slot_counts, empty_build
+from .cargomanager import CargoManager
+from .constants import SHIP_TEMPLATE
+from .imagemanager import ImageManager
+from .iofunc import store_json__new
+from .textedit import add_equipment_tooltip_header__new, get_ultimate_skill_unlock_tooltip__new
+from .theme import TooltipCSS
+from .widgets import ItemButton, ShipButton, ShipImage, Thread, TooltipLabel
+
+
+
+class SpaceBuild():
+ """Stores widgets for space build"""
+ def __init__(self):
+ self.active_rep_traits: list[ItemButton] = [None] * 5
+ self.aft_weapons: list[ItemButton] = [None] * 5
+ self.aft_weapons_label: QLabel = None
+ self.boffs: list[list[ItemButton]] = [
+ [None] * 4, [None] * 4, [None] * 4, [None] * 4, [None] * 4, [None] * 4]
+ self.boff_labels: list[QComboBox] = [None] * 6
+ self.boff_label_icons: list[TooltipLabel] = [None] * 6
+ self.core: list[ItemButton] = [None]
+ self.deflector: list[ItemButton] = [None]
+ self.devices: list[ItemButton] = [None] * 6
+ self.doffs_spec: list[QComboBox] = [None] * 6
+ self.doffs_variant: list[QComboBox] = [None] * 6
+ self.eng_consoles: list[ItemButton] = [None] * 5
+ self.eng_consoles_label: QLabel = None
+ self.engines: list[ItemButton] = [None]
+ self.experimental: list[ItemButton] = [None]
+ self.experimental_label: QLabel = [None]
+ self.fore_weapons: list[ItemButton] = [None]
+ self.hangars: list[ItemButton] = [None] * 2
+ self.hangars_label: QLabel = None
+ self.rep_traits: list[ItemButton] = [None] * 5
+ self.sci_consoles: list[ItemButton] = [None] * 5
+ self.sci_consoles_label: QLabel = None
+ self.sec_def: list[ItemButton] = [None]
+ self.sec_def_label: list[ItemButton] = [None]
+ self.shield: list[ItemButton] = [None]
+ self.starship_traits: list[ItemButton] = [None]
+ self.tac_consoles: list[ItemButton] = [None] * 5
+ self.tac_consoles_label: QLabel = None
+ self.traits: list[ItemButton] = [None] * 12
+ self.uni_consoles: list[ItemButton] = [None] * 3
+ self.uni_consoles_label: QLabel = None
+
+
+class GroundBuild():
+ """Stores widgets for ground build"""
+ def __init__(self):
+ self.active_rep_traits: list[ItemButton] = [None] * 5
+ self.armor: list[ItemButton] = [None]
+ self.boffs: list[list[ItemButton]] = [[None] * 4, [None] * 4, [None] * 4, [None] * 4]
+ self.boff_profs: list[QComboBox] = [None] * 4
+ self.boff_specs: list[QComboBox] = [None] * 4
+ self.ground_devices: list[ItemButton] = [None] * 5
+ self.desc: QPlainTextEdit = None
+ self.doffs_spec: list[QComboBox] = [None] * 6
+ self.doffs_variant: list[QComboBox] = [None] * 6
+ self.ev_suit: list[ItemButton] = [None]
+ self.kit: list[ItemButton] = [None]
+ self.kit_modules: list[ItemButton] = [None] * 6
+ self.rep_traits: list[ItemButton] = [None] * 5
+ self.personal_shield: list[ItemButton] = [None]
+ self.traits: list[ItemButton] = [None] * 12
+ self.weapons: list[ItemButton] = [None] * 2
+
+
+class SkillTree():
+ """Stores widgets for space and ground skill tree"""
+ def __init__(self):
+ self.space: dict[str, list[ItemButton]] = {
+ 'eng': [None] * 30,
+ 'sci': [None] * 30,
+ 'tac': [None] * 30
+ }
+ self.ground: list[list[ItemButton]] = [
+ [False] * 6,
+ [False] * 6,
+ [False] * 4,
+ [False] * 4,
+ ]
+ self.unlocks: dict[str, list[ItemButton]] = {
+ 'eng': [None] * 5,
+ 'sci': [None] * 5,
+ 'tac': [None] * 5,
+ 'ground': [None] * 5
+ }
+ self.bonus_bars: dict[str, list[QPushButton]] = {
+ 'eng': [None] * 24,
+ 'sci': [None] * 24,
+ 'tac': [None] * 24,
+ 'ground': [None] * 10,
+ }
+ self.count_labels: dict[str, QLabel] = {
+ 'eng': None,
+ 'sci': None,
+ 'tac': None,
+ 'ground': None
+ }
+ self.space_desc: QPlainTextEdit
+ self.ground_desc: QPlainTextEdit
+
+
+class ShipBuild():
+ """Stores widgets for ship description."""
+ def __init__(self):
+ self.image: ShipImage
+ self.button: ShipButton
+ self.tier: QComboBox
+ self.dc: TooltipLabel
+ self.name: QLineEdit
+ self.desc: QPlainTextEdit
+
+
+class CharacterBuild():
+ """Stores WIdgets for character building."""
+ def __init__(self):
+ self.name: QLineEdit
+ self.elite: QCheckBox
+ self.career: QComboBox
+ self.faction: QComboBox
+ self.species: QComboBox
+ self.primary: QComboBox
+ self.secondary: QComboBox
+
+
+class BuildManager():
+ """Manages build data and widgets"""
+
+ def __init__(
+ self, cache: CargoManager, images: ImageManager, autosave_path: Path,
+ tooltip_styles: TooltipCSS):
+ self._building: bool = False # disables side-effects (including autosave)
+ self._cache: CargoManager = cache
+ self._images: ImageManager = images
+ self._image_thread: Thread = Thread(target=self._images.get_ship_image)
+ self._image_thread.result.connect(lambda image: self.ship.image.set_image(image))
+ self._autosave_path: Path = autosave_path
+ self._tooltip_styles: TooltipCSS = tooltip_styles
+ self.space: SpaceBuild = SpaceBuild()
+ self.ground: GroundBuild = GroundBuild()
+ self.skills: SkillTree = SkillTree()
+ self.ship: ShipBuild = ShipBuild()
+ self.character: CharacterBuild = CharacterBuild()
+ self._build_data: dict[str, int | dict[str]] = empty_build()
+ self._skill_state: dict[str, int | list[int]] = {
+ 'space_points_total': 0,
+ 'space_points_eng': 0,
+ 'space_points_sci': 0,
+ 'space_points_tac': 0,
+ 'space_points_rank': [0] * 5,
+ 'ground_points_total': 0
+ }
+
+ def autosave(self):
+ """
+ Saves build to autosave file.
+ """
+ if not self._building:
+ store_json__new(self._build_data, self._autosave_path)
+
+ def load_build(self):
+ """
+ Updates UI to show the build currently in self._build_data
+ """
+ self._building = True
+ # ship section
+ ship = self._build_data['space']['ship']
+ if ship == '' or ship == '':
+ ship_data = SHIP_TEMPLATE
+ self.ship.button.setText('')
+ self.ship.tier.clear()
+ self.ship.image.set_image(self._images.empty)
+ self.ship.dc.hide()
+ else:
+ self.ship.button.setText(ship)
+ ship_data = self._cache.ships[ship]
+ self.set_ship_image(ship_data['image'][5:])
+ tier = self._build_data['space']['tier']
+ ship_tier = ship_data['tier']
+ self.ship.tier.clear()
+ if ship_tier == 6:
+ self.ship.tier.addItems(('T6', 'T6-X', 'T6-X2'))
+ elif ship_tier == 5:
+ self.ship.tier.addItems(('T5', 'T5-U', 'T5-X', 'T5-X2'))
+ else:
+ self.ship.tier.addItem(f'T{ship_tier}')
+ self.ship.tier.setCurrentText(tier)
+ if ship_data['equipcannons'] == 'yes':
+ self.ship.dc.show()
+ else:
+ self.ship.dc.hide()
+ self.ship.name.setText(self._build_data['space']['ship_name'])
+ self.ship.desc.setPlainText(self._build_data['space']['ship_desc'])
+
+ # Character section
+ elite_captain = self._build_data['captain']['elite']
+ self.character.name.setText(self._build_data['captain']['name'])
+ elite_state = Qt.CheckState.Checked if elite_captain else Qt.CheckState.Unchecked
+ self.character.elite.setCheckState(elite_state)
+ self.character.career.setCurrentText(self._build_data['captain']['career'])
+ species = self._build_data['captain']['species']
+ self.character.faction.setCurrentText(self._build_data['captain']['faction'])
+ self.character.species.setCurrentText(species)
+ if species != 'Alien':
+ self.space.traits[10].hide()
+ self.ground.traits[10].hide()
+ self.character.primary.setCurrentText(self._build_data['captain']['primary_spec'])
+ self.character.secondary.setCurrentText(self._build_data['captain']['secondary_spec'])
+
+ # Space Build Section
+ if ship == '' or ship == '':
+ self.align_space_frame(ship_data, clear=True)
+ else:
+ self.align_space_frame(ship_data)
+ self.load_equipment_cat('fore_weapons', 'space')
+ self.load_equipment_cat('aft_weapons', 'space')
+ self.load_equipment_cat('experimental', 'space')
+ self.load_equipment_cat('devices', 'space')
+ self.load_equipment_cat('hangars', 'space')
+ self.load_equipment_cat('deflector', 'space')
+ self.load_equipment_cat('sec_def', 'space')
+ self.load_equipment_cat('engines', 'space')
+ self.load_equipment_cat('core', 'space')
+ self.load_equipment_cat('shield', 'space')
+ self.load_equipment_cat('uni_consoles', 'space')
+ self.load_equipment_cat('eng_consoles', 'space')
+ self.load_equipment_cat('sci_consoles', 'space')
+ self.load_equipment_cat('tac_consoles', 'space')
+ self.load_boff_stations('space')
+ self.load_trait_cat('traits', 'space')
+ if not elite_captain:
+ self.space.traits[9].hide()
+ self.load_trait_cat('starship_traits', 'space')
+ self.load_trait_cat('rep_traits', 'space')
+ self.load_trait_cat('active_rep_traits', 'space')
+ self.load_doffs('space')
+
+ # Ground Build Section
+ self.ground.desc.setPlainText(self._build_data['ground']['ground_desc'])
+ self.load_equipment_cat('kit_modules', 'ground')
+ if not elite_captain:
+ self.ground.kit_modules[5].hide()
+ self.load_equipment_cat('weapons', 'ground')
+ self.load_equipment_cat('ground_devices', 'ground')
+ if not elite_captain:
+ self.ground.ground_devices[4].hide()
+ self.load_equipment_cat('kit', 'ground')
+ self.load_equipment_cat('armor', 'ground')
+ self.load_equipment_cat('ev_suit', 'ground')
+ self.load_equipment_cat('personal_shield', 'ground')
+ self.load_boff_stations('ground')
+ self.load_trait_cat('traits', 'ground')
+ if not elite_captain:
+ self.ground.traits[9].hide()
+ self.load_trait_cat('rep_traits', 'ground')
+ self.load_trait_cat('active_rep_traits', 'ground')
+ self.load_doffs('ground')
+
+ self.load_skill_pages()
+
+ self._building = False
+ self.autosave()
+
+ def set_ship_image(self, image_name: str):
+ """
+ Updates ship image with image specified by `image_name`.
+
+ Parameters:
+ - :param image_name: name of the image to obtain and show
+ """
+ if self._image_thread.isRunning():
+ self._image_thread.finished.connect(
+ lambda name=image_name: self.set_ship_image(name),
+ type=Qt.ConnectionType.SingleShotConnection)
+ else:
+ self._image_thread.set_args((image_name,))
+ self._image_thread.start()
+
+ def align_space_frame(self, ship_data: dict, clear: bool = False):
+ """
+ Hides / shows the appropriate buttons of the ship build. Updates Boff stations.
+
+ Parameters:
+ - :param ship_data: ship specifications
+ - :param clear: set to True to clear build
+ """
+ uni, eng, sci, tac, devices, starship_traits = get_variable_slot_counts(
+ ship_data, self._build_data['space']['tier'])
+
+ self.update_equipment_cat('fore_weapons', ship_data['fore'], clear)
+ self.update_equipment_cat('aft_weapons', ship_data['aft'], clear, can_hide=True)
+ self.update_equipment_cat('experimental', ship_data['experimental'], clear, can_hide=True)
+ self.update_equipment_cat('devices', devices, clear)
+ self.update_equipment_cat('hangars', ship_data['hangars'], clear, can_hide=True)
+ self.update_equipment_cat('sec_def', ship_data['secdeflector'], clear, can_hide=True)
+ if clear:
+ self.space.deflector[0].clear()
+ self._build_data['space']['deflector'][0] = ''
+ self.space.engines[0].clear()
+ self._build_data['space']['engines'][0] = ''
+ self.space.core[0].clear()
+ self._build_data['space']['core'][0] = ''
+ self.space.shield[0].clear()
+ self._build_data['space']['shield'][0] = ''
+ self.update_equipment_cat('uni_consoles', uni, clear, can_hide=True)
+ self.update_equipment_cat('eng_consoles', eng, clear, can_hide=True)
+ self.update_equipment_cat('sci_consoles', sci, clear, can_hide=True)
+ self.update_equipment_cat('tac_consoles', tac, clear, can_hide=True)
+
+ self.update_starship_traits(starship_traits, clear)
+
+ boff_specs = map(lambda s: get_boff_spec(self, s), ship_data['boffs'])
+ if 'Science Destroyer' in ship_data['type']:
+ for boff_num, boff_details in enumerate(sorted(boff_specs, reverse=True)):
+ if (boff_details[0] == 3 and boff_details[1] == 'Tactical'
+ or boff_details[0] == 4 and boff_details[1] == 'Science'):
+ self.update_boff_seat(boff_num, *boff_details, clear, sci_destroyer_seat=True)
+ else:
+ self.update_boff_seat(boff_num, *boff_details, clear)
+ else:
+ for boff_num, boff_details in enumerate(sorted(boff_specs, reverse=True)):
+ self.update_boff_seat(boff_num, *boff_details, clear)
+ for boff_to_hide in range(boff_num + 1, 6):
+ self.update_boff_seat(boff_to_hide, rank=0, profession='', clear=clear, hide_seat=True)
+
+ def update_equipment_cat(
+ self, build_key: str, target_quantity: int | None, clear: bool = False,
+ can_hide: bool = False):
+ """
+ Shows/hides appropriate amount of buttons of the given category; updates build; space build
+ only
+
+ Parameters:
+ - :param build_key: key to self.build and self.widgets
+ - :param target_quantity: number of slots that should be available in this category
+ - :param clear: True to clear build
+ - :param can_hide: hides/shows category label when target_quantity is 0/None
+ """
+ if target_quantity is None or target_quantity == 0:
+ target_quantity = 0
+ getattr(self.space, build_key + '_label').hide()
+ elif can_hide:
+ getattr(self.space, build_key + '_label').show()
+ buttons: list[ItemButton] = getattr(self.space, build_key)
+ max_quantity = len(buttons)
+ for show_index in range(target_quantity):
+ buttons[show_index].show()
+ if clear:
+ buttons[show_index].clear()
+ self._build_data['space'][build_key][show_index] = ''
+ for hide_index in range(target_quantity, max_quantity):
+ buttons[hide_index].clear()
+ buttons[hide_index].hide()
+ self._build_data['space'][build_key][hide_index] = None
+
+ def update_starship_traits(self, target_quantity: int, clear: bool = False):
+ """
+ Shows/hides appropriate amount of starship trait buttons; updates `self.build`
+
+ Parameters:
+ - :param target_quantity: number of slots that should be available in this category
+ - :param clear: True to clear build
+ """
+ buttons = self.space.starship_traits
+ for show_index in range(target_quantity):
+ buttons[show_index].show()
+ if clear:
+ buttons[show_index].clear()
+ self._build_data['space']['starship_traits'][show_index] = ''
+ for hide_index in range(target_quantity, 7):
+ buttons[hide_index].clear()
+ buttons[hide_index].hide()
+ self._build_data['space']['starship_traits'][hide_index] = None
+
+ def update_boff_seat(
+ self, boff_id: int, rank: int, profession: str, specialization: str = '',
+ clear: bool = False, hide_seat: bool = False, sci_destroyer_seat: bool = False):
+ """
+ Shows/hides appropriate amount of buttons of the boff seat; updates build; space build only
+
+ Parameters:
+ - :param boff_id: boff number counted from the top/beginning
+ - :param rank: number of slots that should be available in this category
+ - :param profession: seat profession
+ - :param specialization: seat specialization
+ - :param clear: set to True to clear build
+ - :param hide_seat: hides/shows seat label
+ - :param sci_destroyer_seat: set to `True` to upgrade seat to commander and show info label
+ """
+ buttons = self.space.boffs[boff_id]
+ max_quantity = 4
+ if sci_destroyer_seat:
+ rank = 4
+ for show_index in range(rank):
+ buttons[show_index].show()
+ if clear:
+ buttons[show_index].clear()
+ self._build_data['space']['boffs'][boff_id][show_index] = ''
+ for hide_index in range(rank, max_quantity):
+ buttons[hide_index].clear()
+ buttons[hide_index].hide()
+ self._build_data['space']['boffs'][boff_id][hide_index] = None
+ label = self.space.boff_labels[boff_id]
+ label.clear()
+ if hide_seat:
+ label.hide()
+ else:
+ label.show()
+ if specialization != '':
+ spec_label = f' / {specialization}'
+ else:
+ spec_label = ''
+ if profession == 'Universal':
+ label_options = (
+ f'Tactical{spec_label}',
+ f'Science{spec_label}',
+ f'Engineering{spec_label}'
+ )
+ label.setDisabled(False)
+ else:
+ label_options = (profession + spec_label,)
+ label.setDisabled(True)
+ label.addItems(label_options)
+ icon_label = self.space.boff_label_icons[boff_id]
+ if sci_destroyer_seat:
+ if profession == 'Science':
+ icon_label.setPixmap(self._images.icons['sci-small'])
+ icon_label._tooltip.setText('Commander slot only available in science mode.')
+ elif profession == 'Tactical':
+ icon_label.setPixmap(self._images.icons['tac-small'])
+ icon_label._tooltip.setText('Commander slot only available in tactical mode.')
+ icon_label.show()
+ else:
+ icon_label.hide()
+ if clear:
+ default_profession = 'Tactical' if profession == 'Universal' else profession
+ self._build_data['space']['boff_specs'][boff_id] = [default_profession, specialization]
+
+ def load_equipment_cat(self, build_key: str, environment: str):
+ """
+ Updates equipment category buttons to show items from build.
+
+ Parameters:
+ - :param build_key: equipment category
+ - :param environment: space/ground
+ """
+ for subkey, item in enumerate(self._build_data[environment][build_key]):
+ if item is not None and item != '':
+ self.slot_equipment_item(item, environment, build_key, subkey)
+ else:
+ getattr(getattr(self, environment), build_key)[subkey].clear()
+
+ def load_trait_cat(self, build_key: str, environment: str):
+ """
+ Updates trait category buttons to show items from build.
+
+ Parameters:
+ - :param build_key: trait category
+ - :param environment: space/ground
+ """
+ for subkey, item in enumerate(self.build[environment][build_key]):
+ if item is not None and item != '':
+ self.slot_trait_item(item, environment, build_key, subkey)
+ else:
+ getattr(getattr(self, environment), build_key)[subkey].clear()
+
+ def slot_equipment_item(
+ self, item: dict[str, str], environment: str, build_key: str, build_subkey: int):
+ """
+ Updates build and UI with item
+
+ Parameters:
+ - :param item: item to be slotted
+ - :param environment: space/ground
+ - :param build_key: key to self.build[environment]
+ - :param build_subkey: index of the item within its build_key (category)
+ """
+ self._build_data[environment][build_key][build_subkey] = item
+ overlay = getattr(self._images.overlays, item['rarity'].lower().replace(' ', ''))
+ tooltip = add_equipment_tooltip_header__new(
+ item, self._cache.equipment[build_key][item['item']], self._tooltip_styles)
+ item_button: ItemButton = getattr(getattr(self, environment), build_key)[build_subkey]
+ item_button.set_item_full(self._images.get(item['item']), overlay, tooltip)
+
+ def slot_trait_item(
+ self, item: dict[str, str], environment: str, build_key: str, build_subkey: int):
+ """
+ Updates build and UI with item
+
+ Parameters:
+ - :param item: item to be slotted
+ - :param environment: space/ground
+ - :param build_key: key to self.build[environment]
+ - :param build_subkey: index of the item within its build_key (category)
+ """
+ item_name = item['item']
+ self._build_data[environment][build_key][build_subkey] = item
+ alt_image_key = f"{item_name}__{environment}__{build_key}"
+ if alt_image_key in self._cache.alt_images:
+ item_image = self._images.get(self._cache.alt_images[alt_image_key])
+ else:
+ item_image = self._images.get(item_name)
+ item_button: ItemButton = getattr(getattr(self, environment), build_key)[build_subkey]
+ if build_key == 'starship_traits':
+ tooltip = self._cache.starship_traits[item_name]['tooltip']
+ elif environment == 'space':
+ tooltip = self._cache.space_traits[build_key][item_name]['tooltip']
+ else:
+ tooltip = self._cache.ground_traits[build_key][item_name]['tooltip']
+ item_button.set_item_full(item_image, None, tooltip)
+
+ def load_boff_stations(self, environment: str):
+ """
+ Updates boff stations to show items from build
+
+ Parameters:
+ - :param environment: "space" / "ground"
+ """
+ if environment == 'space':
+ for boff_id, boff_data in enumerate(self._build_data['space']['boffs']):
+ boff_spec = self._build_data['space']['boff_specs'][boff_id]
+ if boff_spec[1] == '':
+ boff_text = boff_spec[0]
+ else:
+ boff_text = f'{boff_spec[0]} / {boff_spec[1]}'
+ self.space.boff_labels[boff_id].setCurrentText(boff_text)
+ for ability, slot in zip(boff_data, self.space.boffs[boff_id]):
+ if ability is not None and ability != '':
+ slot.set_item_full(
+ self._images.get(ability['item']), None,
+ self._cache.boff_abilities['all'][ability['item']][ability['rank']])
+ else:
+ slot.clear()
+ elif environment == 'ground':
+ for boff_id, boff_data in enumerate(self._build_data['ground']['boffs']):
+ self.ground.boff_profs[boff_id].setCurrentText(
+ self._build_data['ground']['boff_profs'][boff_id])
+ self.ground.boff_specs[boff_id].setCurrentText(
+ self._build_data['ground']['boff_specs'][boff_id])
+ for ability, slot in zip(boff_data, self.ground.boffs[boff_id]):
+ if ability is not None and ability != '':
+ slot.set_item_full(
+ self._images.get(ability['item']), None,
+ self._cache.boff_abilities['all'][ability['item']][ability['rank']])
+ else:
+ slot.clear()
+
+ def load_doffs(self, environment: str):
+ """
+ Updates UI to show doffs in self.build
+
+ Parameters:
+ - :param environment: "space" / "ground"
+ """
+ if environment == 'space':
+ doff_zipper = zip(
+ self.space.doffs_spec, self._build_data['space']['doffs_spec'],
+ self.space.doffs_variant, self._build_data['space']['doffs_variant'])
+ elif environment == 'ground':
+ doff_zipper = zip(
+ self.ground.doffs_spec, self._build_data['ground']['doffs_spec'],
+ self.ground.doffs_variant, self._build_data['ground']['doffs_variant'])
+ for spec_combo, spec, variant_combo, variant in doff_zipper:
+ spec_combo.setCurrentText(spec)
+ if spec != '':
+ variants = getattr(self._cache, f'{environment}_doffs')[spec].keys()
+ variant_combo.addItems({''} | variants)
+ variant_combo.setCurrentText(variant)
+
+ def load_skill_pages(self):
+ """
+ Updates UI to show skill trees in self.build
+ """
+ self.skills.space_desc.setPlainText(self._build_data['skill_desc']['space'])
+ self._skill_state['space_points_eng'] = 0
+ self._skill_state['space_points_sci'] = 0
+ self._skill_state['space_points_tac'] = 0
+ self._skill_state['space_points_rank'] = [0] * 5
+ self._skill_state['space_points_total'] = 0
+ for career in ('eng', 'sci', 'tac'):
+ for skill_id, (button, enable) in enumerate(zip(
+ self.skills.space[career], self._build_data['space_skills'][career])):
+ if enable:
+ button.set_overlay(self._images.overlays.check)
+ button.highlight = True
+ self._skill_state[f'space_points_{career}'] += 1
+ self._skill_state['space_points_rank'][int(skill_id / 6)] += 1
+ else:
+ button.clear_overlay()
+ button.highlight = False
+ self._skill_state['space_points_total'] = sum(self._skill_state['space_points_rank'])
+ for career in ('eng', 'sci', 'tac'):
+ skill_points = self._skill_state[f'space_points_{career}']
+ self.skills.count_labels[career].setText(str(skill_points))
+ for unlock_id, unlock_choice in enumerate(self._build_data['skill_unlocks'][career]):
+ self.set_skill_unlock_space(self, career, unlock_id, unlock_choice, skill_points)
+ if skill_points > 24:
+ skill_points = 24
+ for i in range(skill_points):
+ self.skills.bonus_bars[career][i].setChecked(True)
+ for i in range(skill_points, 24, 1):
+ self.skills.bonus_bars[career][i].setChecked(False)
+
+ self.skills.ground_desc.setPlainText(self._build_data['skill_desc']['ground'])
+ self._skill_state['ground_points_total'] = 0
+ ground_skills: list[list[bool]] = self._build_data['ground_skills']
+ for skill_buttons, skill_data in zip(self.skills.ground, ground_skills):
+ for skill_button, enable in zip(skill_buttons, skill_data):
+ if enable:
+ skill_button.set_overlay(self._images.overlays.check)
+ skill_button.highlight = True
+ self._skill_state['ground_points_total'] += 1
+ else:
+ skill_button.clear_overlay()
+ skill_button.highlight = False
+ self.skills.count_labels['ground'].setText(str(self._skill_state['ground_points_total']))
+ for i in range(self._skill_state['ground_points_total']):
+ self.skills.bonus_bars['ground'][i].setChecked(True)
+ for i in range(self._skill_state['ground_points_total'], 10, 1):
+ self.skills.bonus_bars['ground'][i].setChecked(False)
+ for unlock_id, unlock_choice in enumerate(self._build_data['skill_unlocks']['ground']):
+ self.set_skill_unlock_ground(unlock_id, unlock_choice)
+
+ def set_skill_unlock_space(
+ self, career: str, id: int, state: int | None = None, points_spent: int = -1):
+ """
+ Sets unlock button to state and updates build
+
+ Parameters:
+ - :param career: "eng" / "sci" / "tac"
+ - :param id: id of the unlock, counted from the unlock with the lowest requirement
+ - :param state: `0`, `1` set the button to the respective unlock, `None` clears
+ """
+ unlock_button = self.skills.unlocks[career][id]
+ if id == 4:
+ if points_spent > 27 and state == self._build_data['skill_unlocks'][career][id]:
+ return
+ if state is None:
+ unlock_button.clear()
+ self._build_data['skill_unlocks'][career][id] = None
+ else:
+ unlock_data = self._cache.skills['space_unlocks'][career][4]
+ unlock_button.set_item(self._images.get(unlock_data['name']))
+ if points_spent > 26:
+ unlock_button.tooltip = get_ultimate_skill_unlock_tooltip__new(
+ unlock_data, state, 3, self._tooltip_styles)
+ self._build_data['skill_unlocks'][career][id] = 3
+ else:
+ unlock_button.tooltip = get_ultimate_skill_unlock_tooltip__new(
+ unlock_data, state, points_spent - 24, self._tooltip_styles)
+ self._build_data['skill_unlocks'][career][id] = state
+ if not self._building:
+ unlock_button.force_tooltip_update()
+ else:
+ if state is None:
+ unlock_button.clear()
+ self._build_data['skill_unlocks'][career][id] = None
+ else:
+ unlock_data = self._cache.skills['space_unlocks'][career][id]['nodes'][state]
+ if state == 0:
+ unlock_button.set_item(self._images.get('arrow-up'))
+ elif state == 1:
+ unlock_button.set_item(self._images.get('arrow-down'))
+ unlock_button.tooltip = (
+ f""
+ f"{unlock_data['name']}
"
+ f""
+ f"Space Skill
{unlock_data['desc']}
")
+ self._build_data['skill_unlocks'][career][id] = state
+ if not self._building:
+ unlock_button.force_tooltip_update()
+
+ def set_skill_unlock_ground(self, id: int, state: int | None):
+ """
+ Sets unlock button to state and updates build
+
+ Parameters:
+ - :param id: id of the unlock, counted from the unlock with the lowest requirement
+ - :param state: `0`, `1` set the button to the respective unlock, `None` clears
+ """
+ unlock_button = self.skills.unlocks['ground'][id]
+ if state is None:
+ unlock_button.clear()
+ self._build_data['skill_unlocks']['ground'][id] = None
+ else:
+ unlock_data = self._cache.skills['ground_unlocks'][id]['nodes'][state]
+ if state == 0:
+ unlock_button.set_item(self._images.get('arrow-up'))
+ elif state == 1:
+ unlock_button.set_item(self._images.get('arrow-down'))
+ unlock_button.tooltip = (
+ f""
+ f"{unlock_data['name']}
"
+ f""
+ f"Space Skill
{unlock_data['desc']}
")
+ self._build_data['skill_unlocks']['ground'][id] = state
+ if not self._building:
+ unlock_button.force_tooltip_update()
diff --git a/src/imagemanager.py b/src/imagemanager.py
index 793cf1f..e80ed21 100644
--- a/src/imagemanager.py
+++ b/src/imagemanager.py
@@ -1,35 +1,67 @@
from os import listdir as os__listdir
from pathlib import Path
-from PySide6.QtGui import QImage
+from PySide6.QtGui import QIcon, QImage, QPixmap
from time import time
from urllib.parse import quote_plus, unquote_plus
from .cargomanager import CargoManager
from .constants import SEVEN_DAYS_IN_SECONDS
from .downloader import Downloader
-from .iofunc import get_cached_cargo_data
+from .iofunc import get_image_file_name
+
+
+class Overlays():
+ """Stores overlay icons."""
+
+ __slots__ = ('common', 'uncommon', 'rare', 'veryrare', 'ultrarare', 'epic', 'check')
+
+ def __init__(self):
+ self.common: QImage
+ self.uncommon: QImage
+ self.rare: QImage
+ self.veryrare: QImage
+ self.ultrarare: QImage
+ self.epic: QImage
+ self.check: QImage
class ImageManager():
"""Manages icons and ship images"""
def __init__(
- self, images_dir: Path, ship_images_dir: Path, cargo_cache: CargoManager,
+ self, images_dir: Path, ship_images_dir: Path, app_dir: Path, cargo_cache: CargoManager,
downloader: Downloader):
"""
Parameters:
- :param images_dir: path to directory storing icons
- :param ship_images_dir: path to directory storing ship images
+ - :param app_dir: path to directory containing the app installation
- :param cargo_cache: used to access cache
- :param downloader: used to download icons and ship images
"""
self._images_dir: Path = images_dir
self._ship_images_dir: Path = ship_images_dir
+ self._app_dir: Path = app_dir
self._cargo_cache: CargoManager = cargo_cache
self._downloader: Downloader = downloader
- self.empty = QImage()
+ self.empty: QImage = QImage()
+ self.overlays: Overlays = Overlays()
+ self.icons: dict[str, QIcon | QPixmap] = dict()
+ self._images: dict[str, QImage] = dict()
self.image_set: set[str] = set()
self.failed_images: dict[str, int] = dict()
+
+ def get(self, image_name: str) -> QImage:
+ """
+ Returns image from cache if cached, loads and returns image if not cached.
+
+ Parameters:
+ - :param image_name: name of the image
+ """
+ image = self._images[image_name]
+ if image.isNull():
+ image.load(self._images_dir / get_image_file_name(image_name))
+ return image
def get_downloaded_icons(self) -> set[str]:
"""
@@ -89,7 +121,7 @@ def get_skill_icons(self, skill_cache: dict[str, dict]) -> set[str]:
icons.add(skill_node['image'])
return icons
- def get_ship_image(self, image_name: str, threaded_worker):
+ def get_ship_image(self, image_name: str) -> QImage:
"""
Tries to load ship image from local filesystem. If it is not avilable, downloads and
stores it. Passes the image back using the provided signal. TODO improve result handling
@@ -104,4 +136,45 @@ def get_ship_image(self, image_name: str, threaded_worker):
# TODO integrate with failed images
self._downloader.download_ship_image(image_name, {})
image = QImage(image_path)
- threaded_worker.result.emit((image,))
+ return image
+
+ def load_base_images(self):
+ """
+ Loads all images that are required for the app to start (skills, overlays)
+ """
+ local_folder = self._app_dir / 'local'
+ self._images = {image_name: QImage() for image_name in self.image_set}
+ self.overlays.common = QImage(local_folder / 'Common_icon.png')
+ self.overlays.uncommon = QImage(local_folder / 'Uncommon_icon.png')
+ self.overlays.rare = QImage(local_folder / 'Rare_icon.png')
+ self.overlays.veryrare = QImage(local_folder / 'Very_rare_icon.png')
+ self.overlays.ultrarare = QImage(local_folder / 'Ultra_rare_icon.png')
+ self.overlays.epic = QImage(local_folder / 'Epic_icon.png')
+ self.overlays.check = QImage(local_folder / 'check_overlay.png')
+
+ for rank_group in self._cargo_cache.skills['space']:
+ for skill_group in rank_group:
+ for skill_node in skill_group['nodes']:
+ self._images[skill_node['image']] = QImage(
+ self._images_dir / get_image_file_name(skill_node['image']))
+ for skill_group in self._cargo_cache.skills['ground']:
+ for skill_node in skill_group['nodes']:
+ self._images[skill_node['image']] = QImage(
+ self._images_dir / get_image_file_name(skill_node['image']))
+ self._images['arrow-up'] = QImage(local_folder / 'arrow-up.png')
+ self._images['arrow-down'] = QImage(local_folder / 'arrow-down.png')
+ self._images['Focused Frenzy'] = QImage(
+ self._images_dir / get_image_file_name('Focused Frenzy'))
+ self._images['Probability Manipulation'] = QImage(
+ self._images_dir / get_image_file_name('Probability Manipulation'))
+ self._images['EPS Corruption'] = QImage(
+ self._images_dir / get_image_file_name('EPS Corruption'))
+
+ def load_images(self):
+ """
+ Loads images from drive.
+ """
+ image_dir = str(self._images_dir)
+ for image_name, image in self._images.items():
+ if image.isNull():
+ image.load(f'{image_dir}/{get_image_file_name(image_name)}')
diff --git a/src/textedit.py b/src/textedit.py
index a8b8cca..47cd7c1 100644
--- a/src/textedit.py
+++ b/src/textedit.py
@@ -44,6 +44,32 @@ def add_equipment_tooltip_header(self, item: dict, tooltip_body: str, item_type:
return tooltip + tooltip_body
+def add_equipment_tooltip_header__new(
+ item: dict[str, str], item_data: dict[str], tooltip_styles: TooltipCSS) -> str:
+ """
+ Adds equipment header including name, mark, modifiers, rarity and item type to the tooltip body
+ and returns the complete tooltip.
+
+ Parameters:
+ - :param item: item to create the tooltip for
+ - :param item_data: cargo data for the item
+ - :param tooltip_styles: used to style the tooltip
+ """
+ rarity_color = f'color:{RARITY_COLORS[item['rarity']]};'
+ head_style = tooltip_styles.equipment_name + rarity_color
+ subhead_style = tooltip_styles.equipment_type_subheader + rarity_color
+ item_title = item['item']
+ if item['mark'] != '' and item['mark'] is not None:
+ item_title += ' ' + item['mark']
+ mods = ' '.join(mod for mod in item['modifiers'] if mod != '' and mod is not None)
+ if mods != '':
+ item_title += ' ' + mods
+ tooltip = (
+ f"{item_title}
"
+ f"{item['rarity']} {item_data['type']}
")
+ return tooltip + item_data['tooltip']
+
+
def format_skill_tooltip(
self, skill_name: str, skill_data: dict, node_index: int, environment: str) -> str:
"""
@@ -116,6 +142,42 @@ def get_skill_unlock_tooltip_space(self, career: str, unlock_id: int, unlock_cho
f"Space Skill{unlock['desc']}
")
+def get_ultimate_skill_unlock_tooltip__new(
+ unlock: dict[str], unlock_choice: int, enhancements: int, tooltip_styles: TooltipCSS):
+ """
+ Formats tooltip for ultimate skill unlock.
+
+ Parameters:
+ - :param unlock: contains unlock metadata and tooltips
+ - :param unlock_choice: selected enhancement (`-1` for no unlock)
+ - :param enhancements: number of enhancements
+ - :param tooltips_styles: used to style the tooltip
+ """
+ enhancement_style = tooltip_styles.skill_ultimate_name
+ tooltip = (
+ f"{unlock['name']}
"
+ f"Space Skill
"
+ f"{unlock['desc']}
")
+ if enhancements == 1:
+ tooltip += (
+ f"{unlock['options'][unlock_choice]['name']}
"
+ f"{unlock['options'][unlock_choice]['desc']}
")
+ elif enhancements == 2:
+ e1 = (unlock_choice - 1) % 3
+ e2 = (unlock_choice + 1) % 3
+ tooltip += (
+ f"{unlock['options'][e1]['name']}
"
+ f"{unlock['options'][e1]['desc']}
"
+ f"{unlock['options'][e2]['name']}
"
+ f"{unlock['options'][e2]['desc']}
")
+ elif enhancements != 0:
+ for i in range(3):
+ tooltip += (
+ f"{unlock['options'][i]['name']}
"
+ f"{unlock['options'][i]['desc']}
")
+ return tooltip
+
+
def get_ultimate_skill_unlock_tooltip(self, career: str, unlock_choice: int, enhancements: int):
"""
gets tooltip for space unlock from cache and formats it
diff --git a/src/widgets.py b/src/widgets.py
index e957f95..7bb4992 100644
--- a/src/widgets.py
+++ b/src/widgets.py
@@ -1,4 +1,5 @@
from collections import namedtuple
+from typing import Callable
from PySide6.QtCore import QEvent, QObject, QPoint, QRect, QSize, Qt, QThread, Signal, Slot
from PySide6.QtGui import QBrush, QColor, QCursor, QEnterEvent, QImage, QMouseEvent, QPainter, QPen
@@ -466,6 +467,38 @@ def __init__(self, margins=0, spacing: int = 0, parent: QWidget = None):
self.setSpacing(spacing)
+class Thread(QThread):
+ """
+ Thread based on QThread with convenience functionality.
+ """
+ result: Signal = Signal(object)
+ done: Signal = Signal()
+
+ def __init__(self, target: Callable, args: tuple = (), kwargs: dict[str] = {}):
+ self._target: Callable = target
+ self._args: tuple = args
+ self._kwargs: dict[str] = kwargs
+
+ def set_args(self, new_args: tuple) -> bool:
+ """
+ Sets new arguments that should be passed to the target. Only works while thread is not
+ running. Returns `True` on success, `False` on failure.
+ """
+ if self.isRunning():
+ return False
+ else:
+ self._args = new_args
+ return True
+
+ @Slot()
+ def run(self):
+ """
+ This function will be executed in a separate thread.
+ """
+ self.result.emit(self._target(*self._args, **self._kwargs))
+ self.done.emit()
+
+
class PySideThread(QThread):
def __init__(self, parent, finished_func, worker):
self.finished_func = finished_func
From 0aa1ca5bb17ea755118e48ba0b168b6754e75218 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Sat, 9 May 2026 11:07:50 +0200
Subject: [PATCH 08/44] Moving ExportWindow and export helpers to dedicated
module
---
src/buildmanager.py | 8 +-
src/export.py | 390 ----------------------------------
src/exportwindow.py | 492 +++++++++++++++++++++++++++++++++++++++++++
src/subwindows.py | 74 -------
src/widgetbuilder.py | 118 ++++++++++-
5 files changed, 613 insertions(+), 469 deletions(-)
delete mode 100644 src/export.py
create mode 100644 src/exportwindow.py
diff --git a/src/buildmanager.py b/src/buildmanager.py
index e60b920..311350d 100644
--- a/src/buildmanager.py
+++ b/src/buildmanager.py
@@ -13,7 +13,6 @@
from .widgets import ItemButton, ShipButton, ShipImage, Thread, TooltipLabel
-
class SpaceBuild():
"""Stores widgets for space build"""
def __init__(self):
@@ -158,14 +157,17 @@ def __init__(
'space_points_rank': [0] * 5,
'ground_points_total': 0
}
-
+
def autosave(self):
"""
Saves build to autosave file.
"""
if not self._building:
store_json__new(self._build_data, self._autosave_path)
-
+
+ def __getitem__(self, key: str):
+ return self._build_data[key]
+
def load_build(self):
"""
Updates UI to show the build currently in self._build_data
diff --git a/src/export.py b/src/export.py
deleted file mode 100644
index e12efd6..0000000
--- a/src/export.py
+++ /dev/null
@@ -1,390 +0,0 @@
-from .constants import BOFF_RANKS_MD, CAREER_ABBR
-from .textedit import wiki_url
-from .widgets import notempty
-
-
-def create_md_table(self, table: list[list[str]], alignment: list = []) -> str:
- """
- Creates markdown-formatted table from two-dimensional list
-
- Parameters:
- - :param table: two-dimenional list representing the table
- - :param alignment: contains column alignment codes for the table
- """
- text = '|'.join(table[0]) + '\n'
- if len(alignment) == 0:
- text += '|'.join([':--'] * len(table[0])) + '\n'
- else:
- text += '|'.join(alignment) + '\n'
- for row in table[1:]:
- text += '|'.join(row) + '\n'
- return text
-
-
-def md_equipment_table(
- self, environment: str, key: str, header: str, extra_cols: int = 1,
- single_line: bool = False) -> str:
- """
- Returns table segment of equipment table for markdown export.
-
- Parameters:
- - :param environment: "space" / "ground"
- - :param key: key to `self.build[environment]`
- - :param header: header text for section
- - :param extra_cols: how many empty cols should be added
- - :param single_line: whether the sections consists of a single line
- """
- section = [[f'**{header}**']]
- if single_line:
- item = self.build[environment][key][0]
- if item is not None and item != '':
- section[0].append(
- f"[{item['item']} {item['mark']} {''.join(notempty(item['modifiers']))}]"
- f"({wiki_url(self.cache.equipment[key][item['item']]['Page'])})")
- else:
- section[0].append('')
- section[0] += [''] * extra_cols
- else:
- category_items = self.build[environment][key]
- for i, item in enumerate(category_items):
- if item is None:
- if i == 0:
- section[0] += [''] * (extra_cols + 1)
- continue
- if i > 0:
- section.append([' '])
- if item == '':
- section[-1] += [''] * (extra_cols + 1)
- else:
- section[-1].append(
- f"[{item['item']} {item['mark']} {''.join(notempty(item['modifiers']))}]"
- f"({wiki_url(self.cache.equipment[key][item['item']]['Page'])})")
- section[-1] += [''] * extra_cols
- section.append(['--------------', '--------------'] + [''] * extra_cols)
- return section
-
-
-def md_boff_table(self, station: list, header: str, extra_cols: int = 1) -> list:
- """
- Returns table segment of bridge officer table for markdown export.
-
- Parameters:
- - :param station: boff station to convert
- - :param header: station name
- - :param extra_cols: how many empty cols should be added
- """
- section = [[f'**{header}**']]
- for i, ability in enumerate(station):
- if i > 0:
- section.append([' '])
- if ability == '':
- section[i] += [''] * (extra_cols + 1)
- elif ability is None:
- section.pop()
- else:
- section[i].append(f"[{ability['item']}]({wiki_url(ability['item'], 'Ability: ')})")
- section[i] += [''] * extra_cols
- section.append(['--------------', '--------------'] + [''] * extra_cols)
- return section
-
-
-def md_skill_table_space(self, skills: list, offset: int) -> list:
- """
- Returns table segment (one rank) of space skills for markdown export.
-
- Parameters:
- - :param skills: contains all skill groups of one rank
- - :param offset: offset of the first skill node for indexing into `self.build`
- """
- section = [[], []]
- offsets = {'eng': offset, 'tac': offset, 'sci': offset}
- for skill in skills:
- if skill['grouping'] == 'column':
- section[0].append(f"[{skill['skill']}]({skill['link']})")
- unlocked_skills = ''
- if self.build['space_skills'][skill['career']][offsets[skill['career']]]:
- unlocked_skills += '[X] > '
- else:
- unlocked_skills += '[ ] > '
- if self.build['space_skills'][skill['career']][offsets[skill['career']] + 1]:
- unlocked_skills += '[X] > '
- else:
- unlocked_skills += '[ ] > '
- if self.build['space_skills'][skill['career']][offsets[skill['career']] + 2]:
- unlocked_skills += '[X]'
- else:
- unlocked_skills += '[ ]'
- section[1].append(unlocked_skills)
- elif skill['grouping'] == 'pair+1':
- section[0].append(skill['skill'][0])
- unlocked_skills = ''
- if self.build['space_skills'][skill['career']][offsets[skill['career']] + 1]:
- unlocked_skills += f"[[X]]({skill['link'][1]}) < "
- else:
- unlocked_skills += '[ ] < '
- if self.build['space_skills'][skill['career']][offsets[skill['career']]]:
- unlocked_skills += f"[[X]]({skill['link'][0]}) > "
- else:
- unlocked_skills += '[ ] > '
- if self.build['space_skills'][skill['career']][offsets[skill['career']] + 2]:
- unlocked_skills += f"[[X]]({skill['link'][2]})"
- else:
- unlocked_skills += '[ ]'
- section[1].append(unlocked_skills)
- elif skill['grouping'] == 'separate':
- section[0].append(f"[{skill['skill'][0]}]({skill['link']})")
- unlocked_skills = ''
- if self.build['space_skills'][skill['career']][offsets[skill['career']] + 1]:
- unlocked_skills += '[X] < '
- else:
- unlocked_skills += '[ ] < '
- if self.build['space_skills'][skill['career']][offsets[skill['career']]]:
- unlocked_skills += '[X] > '
- else:
- unlocked_skills += '[ ] > '
- if self.build['space_skills'][skill['career']][offsets[skill['career']] + 2]:
- unlocked_skills += '[X]'
- else:
- unlocked_skills += '[ ]'
- section[1].append(unlocked_skills)
- if len(section[0]) == 2 or len(section[0]) == 5:
- section[0].append('')
- section[1].append('')
- offsets[skill['career']] += 3
- return section
-
-
-def get_build_markdown(self, environment: str, type_: str) -> str:
- """
- Converts part of build in self.build to markdown.
-
- Parameters:
- - :param environment: "space" / "ground"; determines which build environment is generated
- - :param type_: "build" / "skills"; determines whether build or skill tree is generated
- """
- if environment == 'space' and type_ == 'build':
- md = (
- f"# SPACE BUILD\n\n**Basic Information** | **Data** \n:--- | :--- \n"
- f"*Ship Name* | {self.build['space']['ship_name']} \n"
- f"*Ship Class* | {self.build['space']['ship']} \n"
- f"*Ship Tier* | {self.build['space']['tier']} \n"
- f"*Player Career* | {self.build['captain']['career']} \n"
- f"*Elite Captain* | {'✓' if self.build['captain']['elite'] else '✗'}\n"
- f"*Player Species* | {self.build['captain']['species']} \n"
- f"*Primary Specialization* | {self.build['captain']['primary_spec']} \n"
- f"*Secondary Specialization* | {self.build['captain']['secondary_spec']} \n\n\n"
- )
- if self.build['space']['ship_desc']:
- md += f"## Build Description\n\n{self.build['space']['ship_desc']}\n\n\n"
-
- md += '## Ship Equipment\n\n'
- equip_table = [['**Basic Information**', '**Component**', '**Notes**']]
- equip_table += md_equipment_table(self, 'space', 'fore_weapons', 'Fore Weapons')
- equip_table += md_equipment_table(self, 'space', 'aft_weapons', 'Aft Weapons')
- equip_table += md_equipment_table(self, 'space', 'deflector', 'Deflector', single_line=True)
- if self.build['space']['sec_def'][0]:
- equip_table += md_equipment_table(
- self, 'space', 'sec_def', 'Secondary Deflector', single_line=True)
- equip_table += md_equipment_table(
- self, 'space', 'engines', 'Impulse Engines', single_line=True)
- equip_table += md_equipment_table(self, 'space', 'core', 'Warp', single_line=True)
- equip_table += md_equipment_table(self, 'space', 'shield', 'Shield', single_line=True)
- equip_table += md_equipment_table(self, 'space', 'devices', 'Devices')
- if self.build['space']['experimental'][0]:
- equip_table += md_equipment_table(
- self, 'space', 'experimental', 'Experimental Weapon', single_line=True)
- if self.build['space']['hangars'][0] or self.build['space']['hangars'][1]:
- equip_table += md_equipment_table(self, 'space', 'hangars', 'Hangars')
- equip_table += md_equipment_table(self, 'space', 'uni_consoles', 'Universal Consoles')
- equip_table += md_equipment_table(self, 'space', 'eng_consoles', 'Engineering Consoles')
- equip_table += md_equipment_table(self, 'space', 'sci_consoles', 'Science Consoles')
- equip_table += md_equipment_table(self, 'space', 'tac_consoles', 'Tactical Consoles')
- md += create_md_table(self, equip_table)
-
- md += '\n\n\n## Bridge Officer Stations\n\n'
- boff_table = [['**Profession**', '**Power**', '**Notes**']]
- for specs, station in zip(self.build['space']['boff_specs'], self.build['space']['boffs']):
- if any(specs):
- station_name = BOFF_RANKS_MD[station.count(None)] + ' ' + specs[0]
- if specs[1] != '':
- station_name += ' / ' + specs[1]
- boff_table += md_boff_table(self, station, station_name)
- md += create_md_table(self, boff_table)
-
- md += '\n\n\n## Traits\n\n'
- trait_table = [['**Starship Traits**', '**Notes**']]
- for trait in notempty(self.build['space']['starship_traits']):
- trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
- md += create_md_table(self, trait_table)
- md += '\n\n\n\n'
- trait_table = [['**Personal Space Traits**', '**Notes**']]
- for trait in notempty(self.build['space']['traits']):
- trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
- md += create_md_table(self, trait_table)
- md += '\n\n\n\n'
- trait_table = [['**Space Reputation Traits**', '**Notes**']]
- for trait in notempty(self.build['space']['rep_traits']):
- trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
- md += create_md_table(self, trait_table)
- md += '\n\n\n\n'
- trait_table = [['**Active Space Reputation Traits**', '**Notes**']]
- for trait in notempty(self.build['space']['active_rep_traits']):
- trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
- md += create_md_table(self, trait_table)
-
- md += '\n\n\n## Active Space Duty Officers\n\n'
- doff_table = [['**Specialization**', '**Power**', '**Notes**']]
- for spec, variant in zip(
- self.build['space']['doffs_spec'], self.build['space']['doffs_variant']):
- if spec != '':
- doff_table.append([f"[{spec}]({wiki_url(spec, 'Specialization: ')})", variant, ''])
- md += create_md_table(self, doff_table)
- return md
- elif environment == 'ground' and type_ == 'build':
- md = (
- f"# GROUND BUILD\n\n**Basic Information** | **Data** \n:--- | :--- \n"
- f"*Player Name* | {self.build['captain']['name']} \n"
- f"*Player Species* | {self.build['captain']['species']} \n"
- f"*Player Career* | {self.build['captain']['career']} \n"
- f"*Elite Captain* | {'✓' if self.build['captain']['elite'] else '✗'}\n"
- f"*Primary Specialization* | {self.build['captain']['primary_spec']} \n"
- f"*Secondary Specialization* | {self.build['captain']['secondary_spec']} \n\n\n"
- )
- if self.build['ground']['ground_desc'] != '':
- md += f"## Build Description\n\n{self.build['ground']['ground_desc']}\n\n\n"
-
- md += '## Personal Equipment\n\n'
- equip_table = [[' ', '**Component**', '**Notes**']]
- equip_table += md_equipment_table(self, 'ground', 'kit', 'Kit Frame', single_line=True)
- equip_table += md_equipment_table(self, 'ground', 'kit_modules', 'Kit Modules')
- equip_table += md_equipment_table(self, 'ground', 'armor', 'Body Armor', single_line=True)
- equip_table += md_equipment_table(self, 'ground', 'ev_suit', 'EV Suit', single_line=True)
- equip_table += md_equipment_table(
- self, 'ground', 'personal_shield', 'Personal Shield', single_line=True)
- equip_table += md_equipment_table(self, 'ground', 'weapons', 'Weapons')
- equip_table += md_equipment_table(self, 'ground', 'ground_devices', 'Devices')
- md += create_md_table(self, equip_table)
-
- md += '\n\n\n## Traits\n\n'
- trait_table = [['**Personal Ground Traits**', '**Notes**']]
- for trait in notempty(self.build['ground']['traits']):
- trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
- md += create_md_table(self, trait_table)
- md += '\n\n\n\n'
- trait_table = [['**Ground Reputation Traits**', '**Notes**']]
- for trait in notempty(self.build['ground']['rep_traits']):
- trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
- md += create_md_table(self, trait_table)
- md += '\n\n\n\n'
- trait_table = [['**Active Ground Reputation Traits**', '**Notes**']]
- for trait in notempty(self.build['ground']['active_rep_traits']):
- trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
- md += create_md_table(self, trait_table)
-
- md += '\n\n\n## Active Ground Duty Officers\n\n'
- doff_table = [['**Specialization**', '**Power**', '**Notes**']]
- for spec, variant in zip(
- self.build['ground']['doffs_spec'], self.build['ground']['doffs_variant']):
- if spec != '':
- doff_table.append([f"[{spec}]({wiki_url(spec, 'Specialization: ')})", variant, ''])
- md += create_md_table(self, doff_table)
-
- md += '\n\n\n## Away Team\n\n'
- boff_table = [['**Profession**', '**Power**', '**Notes**']]
- for profession, specialization, station in zip(
- self.build['ground']['boff_profs'], self.build['ground']['boff_specs'],
- self.build['ground']['boffs']):
- station_name = f"{profession} / {specialization}"
- boff_table += md_boff_table(self, station, station_name)
- md += create_md_table(self, boff_table)
- return md
- elif environment == 'space' and type_ == 'skills':
- md = '# Space Skills\n\n'
- skill_table = [[
- '**Engineering**', '', '',
- '**Science**', '', '',
- '**Tactical**', ' '
- ]]
- offset = 0
- for rank_skills in self.cache.skills['space']:
- skill_table += md_skill_table_space(self, rank_skills, offset)
- skill_table.append([' '] + [''] * 6 + [' '])
- offset += 6
- md += create_md_table(self, skill_table, alignment=[':-:'] * 8)
- md += '\n\n\n\n'
-
- unlock_table = [
- [f"**[Unlocks]({wiki_url('Skill#Space_2')})**"] + [''] * 7 + [' ']
- ]
- for career, career_name in CAREER_ABBR.items():
- row = [f"**{career_name}**"]
- for i, unlock_state in enumerate(self.build['skill_unlocks'][career]):
- if unlock_state is None:
- row.append('')
- else:
- unlock_slot = self.cache.skills['space_unlocks'][career][i]
- if unlock_slot['points_required'] == 24:
- skill_count = self.cache.skills[f"space_points_{career}"]
- link = wiki_url(unlock_slot['name'], 'Ability: ')
- if unlock_state is None:
- row += ['', '', '', ' ']
- elif unlock_state == -1:
- row += [f"[{unlock_slot['name']}]({link})", '', '', ' ']
- elif unlock_state == 3:
- row += [
- f"[{unlock_slot['name']}]({link})",
- unlock_slot['options'][0]['name'],
- unlock_slot['options'][1]['name'],
- unlock_slot['options'][2]['name'],
- ]
- elif skill_count == 25:
- row += [
- f"[{unlock_slot['name']}]({link})",
- unlock_slot['options'][unlock_state]['name']
- ]
- elif skill_count == 26:
- row.append(f"[{unlock_slot['name']}]({link})")
- enhancements = [
- unlock_slot['options'][0]['name'],
- unlock_slot['options'][1]['name'],
- unlock_slot['options'][2]['name'],
- ' '
- ]
- enhancements.pop(unlock_state)
- row += enhancements
- else:
- row.append(unlock_slot['nodes'][unlock_state]['name'])
- if row[-1] == '':
- row[-1] = ' '
- unlock_table.append(row)
- md += create_md_table(self, unlock_table)
- return md
- elif environment == 'ground' and type_ == 'skills':
- md = '# Ground Skills\n\n'
- skill_table = [['**Skill**', '**I**', '**II**']]
- id_offset = 0
- for skill in self.cache.skills['ground']:
- row = [f"[{skill['nodes'][0]['name']}]({skill['link']})"]
- if self.build['ground_skills'][skill['tree']][id_offset]:
- row.append('[X]')
- else:
- row.append('[ ]')
- if self.build['ground_skills'][skill['tree']][id_offset + 1]:
- row.append('[X]')
- else:
- row.append('[ ]')
- skill_table.append(row)
- if skill['tree'] < 2 and id_offset == 4 or skill['tree'] >= 2 and id_offset == 2:
- id_offset = 0
- else:
- id_offset += 2
- md += create_md_table(self, skill_table, alignment=[':--', ':-:', ':-:'])
- md += '\n\n\n\n'
-
- unlock_table = [['', f"**[Unlocks]({wiki_url('Skill#Ground_2')})**", '']]
- for unlock, unlock_state in zip(
- self.cache.skills['ground_unlocks'], self.build['skill_unlocks']['ground']):
- if unlock_state is not None:
- unlock_table.append(['', unlock['nodes'][unlock_state]['name'], ''])
- md += create_md_table(self, unlock_table, alignment=['', ':-:', ''])
- return md
diff --git a/src/exportwindow.py b/src/exportwindow.py
new file mode 100644
index 0000000..404098d
--- /dev/null
+++ b/src/exportwindow.py
@@ -0,0 +1,492 @@
+from PySide6.QtGui import QTextOption
+from PySide6.QtWidgets import QApplication, QDialog, QPlainTextEdit, QWidget
+
+from .buildmanager import BuildManager
+from .cargomanager import CargoManager
+from .constants import AHCENTER, ALEFT, ATOP, BOFF_RANKS_MD, CAREER_ABBR, SMINMAX, SMINMIN
+from .textedit import wiki_url
+from .theme import AppTheme
+from .widgetbuilder import create_button_series2, create_frame2, create_label2
+from .widgets import notempty, VBoxLayout
+
+
+class ExportWindow(QDialog):
+ """
+ Holds Export Window
+ """
+ def __init__(
+ self, theme: AppTheme, parent_window: QWidget, build: BuildManager,
+ cargo: CargoManager):
+ super().__init__(parent=parent_window)
+ self._window: QWidget = parent_window
+ self._build: BuildManager = build
+ self._cargo: CargoManager = cargo
+ thick = theme['app']['frame_thickness'] * theme.scale
+ dialog_layout = VBoxLayout(margins=thick)
+ main_frame = create_frame2(theme, size_policy=SMINMIN)
+ dialog_layout.addWidget(main_frame)
+ main_layout = VBoxLayout(margins=thick, spacing=thick)
+ content_frame = create_frame2(theme, size_policy=SMINMIN)
+ content_layout = VBoxLayout(spacing=thick)
+ content_layout.setAlignment(ATOP)
+
+ header_label = create_label2(theme, 'Markdown Export:', 'label_heading')
+ content_layout.addWidget(header_label, alignment=ALEFT)
+ self._md_textedit = QPlainTextEdit()
+ button_def = {
+ 'default': {'margin-top': 0},
+ 'Space Build': {
+ 'callback': lambda: self.update_export('space', 'build')
+ },
+ 'Ground Build': {
+ 'callback': lambda: self.update_export('ground', 'build')
+ },
+ 'Space Skills': {
+ 'callback': lambda: self.update_export('space', 'skills')
+ },
+ 'Ground Skills': {
+ 'callback': lambda: self.update_export('ground', 'skills')
+ },
+ }
+ top_buttons = create_button_series2(theme, button_def)
+ top_buttons.setAlignment(AHCENTER)
+ content_layout.addLayout(top_buttons)
+ self._md_textedit.setSizePolicy(SMINMIN)
+ self._md_textedit.setStyleSheet(theme.get_style_class('QPlainTextEdit', 'textedit'))
+ self._md_textedit.setFont(theme.get_font('textedit'))
+ self._md_textedit.setWordWrapMode(QTextOption.WrapMode.NoWrap)
+ content_layout.addWidget(self._md_textedit, stretch=1)
+ content_frame.setLayout(content_layout)
+ main_layout.addWidget(content_frame, stretch=1)
+
+ separator = create_frame2(theme, style='light_frame', size_policy=SMINMAX)
+ separator.setFixedHeight(1)
+ main_layout.addWidget(separator)
+ footer_button_def = {
+ 'Copy': {'callback': self.copy_current_markdown},
+ 'Close': {'callback': lambda: self.done(0)}
+ }
+ footer_buttons = create_button_series2(theme, footer_button_def)
+ footer_buttons.setAlignment(AHCENTER)
+ main_layout.addLayout(footer_buttons)
+ main_frame.setLayout(main_layout)
+
+ self.setLayout(dialog_layout)
+ self.setWindowTitle('SETS - Markdown Export')
+ self.setStyleSheet(theme.get_style('dialog_window'))
+
+ def invoke(self):
+ """
+ Shows Export Window.
+ """
+ window_rect = self._window.geometry()
+ self.setGeometry(
+ window_rect.x() + window_rect.width() * 0.25,
+ window_rect.y() + window_rect.height() * 0.25,
+ window_rect.width() * 0.5,
+ window_rect.height() * 0.5)
+ self.update_export('space', 'build')
+ self.open()
+
+ def update_export(self, environment: str, type_: str):
+ """
+ Updates text output area with newly generated markdown output.
+
+ Parameters:
+ - :param environment: `space` or `ground`
+ - :param type_: `build` or `skills`
+ """
+ self._md_textedit.setPlainText(self.get_build_markdown(environment, type_))
+
+ def copy_current_markdown(self):
+ """
+ Copies currently displayed mardown to application clipboard.
+ """
+ QApplication.clipboard().setText(self._md_textedit.toPlainText())
+
+ def create_md_table(self, table: list[list[str]], alignment: list = []) -> str:
+ """
+ Creates markdown-formatted table from two-dimensional list
+
+ Parameters:
+ - :param table: two-dimenional list representing the table
+ - :param alignment: contains column alignment codes for the table
+ """
+ text = '|'.join(table[0]) + '\n'
+ if len(alignment) == 0:
+ text += '|'.join([':--'] * len(table[0])) + '\n'
+ else:
+ text += '|'.join(alignment) + '\n'
+ for row in table[1:]:
+ text += '|'.join(row) + '\n'
+ return text
+
+ def md_equipment_table(
+ self, environment: str, key: str, header: str, extra_cols: int = 1,
+ single_line: bool = False) -> str:
+ """
+ Returns table segment of equipment table for markdown export.
+
+ Parameters:
+ - :param environment: "space" / "ground"
+ - :param key: key to `self.build[environment]`
+ - :param header: header text for section
+ - :param extra_cols: how many empty cols should be added
+ - :param single_line: whether the sections consists of a single line
+ """
+ section = [[f'**{header}**']]
+ if single_line:
+ item = self._build[environment][key][0]
+ if item is not None and item != '':
+ section[0].append(
+ f"[{item['item']} {item['mark']} {''.join(notempty(item['modifiers']))}]"
+ f"({wiki_url(self._cargo.equipment[key][item['item']]['Page'])})")
+ else:
+ section[0].append('')
+ section[0] += [''] * extra_cols
+ else:
+ category_items = self._build[environment][key]
+ for i, item in enumerate(category_items):
+ if item is None:
+ if i == 0:
+ section[0] += [''] * (extra_cols + 1)
+ continue
+ if i > 0:
+ section.append([' '])
+ if item == '':
+ section[-1] += [''] * (extra_cols + 1)
+ else:
+ section[-1].append(
+ f"[{item['item']} {item['mark']} {''.join(notempty(item['modifiers']))}]"
+ f"({wiki_url(self._cargo.equipment[key][item['item']]['Page'])})")
+ section[-1] += [''] * extra_cols
+ section.append(['--------------', '--------------'] + [''] * extra_cols)
+ return section
+
+ def md_boff_table(self, station: list, header: str, extra_cols: int = 1) -> list:
+ """
+ Returns table segment of bridge officer table for markdown export.
+
+ Parameters:
+ - :param station: boff station to convert
+ - :param header: station name
+ - :param extra_cols: how many empty cols should be added
+ """
+ section = [[f'**{header}**']]
+ for i, ability in enumerate(station):
+ if i > 0:
+ section.append([' '])
+ if ability == '':
+ section[i] += [''] * (extra_cols + 1)
+ elif ability is None:
+ section.pop()
+ else:
+ section[i].append(f"[{ability['item']}]({wiki_url(ability['item'], 'Ability: ')})")
+ section[i] += [''] * extra_cols
+ section.append(['--------------', '--------------'] + [''] * extra_cols)
+ return section
+
+ def md_skill_table_space(self, skills: list, offset: int) -> list:
+ """
+ Returns table segment (one rank) of space skills for markdown export.
+
+ Parameters:
+ - :param skills: contains all skill groups of one rank
+ - :param offset: offset of the first skill node for indexing into `self.build`
+ """
+ section = [[], []]
+ offsets = {'eng': offset, 'tac': offset, 'sci': offset}
+ for skill in skills:
+ if skill['grouping'] == 'column':
+ section[0].append(f"[{skill['skill']}]({skill['link']})")
+ unlocked_skills = ''
+ if self._build['space_skills'][skill['career']][offsets[skill['career']]]:
+ unlocked_skills += '[X] > '
+ else:
+ unlocked_skills += '[ ] > '
+ if self._build['space_skills'][skill['career']][offsets[skill['career']] + 1]:
+ unlocked_skills += '[X] > '
+ else:
+ unlocked_skills += '[ ] > '
+ if self._build['space_skills'][skill['career']][offsets[skill['career']] + 2]:
+ unlocked_skills += '[X]'
+ else:
+ unlocked_skills += '[ ]'
+ section[1].append(unlocked_skills)
+ elif skill['grouping'] == 'pair+1':
+ section[0].append(skill['skill'][0])
+ unlocked_skills = ''
+ if self._build['space_skills'][skill['career']][offsets[skill['career']] + 1]:
+ unlocked_skills += f"[[X]]({skill['link'][1]}) < "
+ else:
+ unlocked_skills += '[ ] < '
+ if self._build['space_skills'][skill['career']][offsets[skill['career']]]:
+ unlocked_skills += f"[[X]]({skill['link'][0]}) > "
+ else:
+ unlocked_skills += '[ ] > '
+ if self._build['space_skills'][skill['career']][offsets[skill['career']] + 2]:
+ unlocked_skills += f"[[X]]({skill['link'][2]})"
+ else:
+ unlocked_skills += '[ ]'
+ section[1].append(unlocked_skills)
+ elif skill['grouping'] == 'separate':
+ section[0].append(f"[{skill['skill'][0]}]({skill['link']})")
+ unlocked_skills = ''
+ if self._build['space_skills'][skill['career']][offsets[skill['career']] + 1]:
+ unlocked_skills += '[X] < '
+ else:
+ unlocked_skills += '[ ] < '
+ if self._build['space_skills'][skill['career']][offsets[skill['career']]]:
+ unlocked_skills += '[X] > '
+ else:
+ unlocked_skills += '[ ] > '
+ if self._build['space_skills'][skill['career']][offsets[skill['career']] + 2]:
+ unlocked_skills += '[X]'
+ else:
+ unlocked_skills += '[ ]'
+ section[1].append(unlocked_skills)
+ if len(section[0]) == 2 or len(section[0]) == 5:
+ section[0].append('')
+ section[1].append('')
+ offsets[skill['career']] += 3
+ return section
+
+ def get_build_markdown(self, environment: str, type_: str) -> str:
+ """
+ Converts part of build in self.build to markdown.
+
+ Parameters:
+ - :param environment: "space" / "ground"; determines which build environment is generated
+ - :param type_: "build" / "skills"; determines whether build or skill tree is generated
+ """
+ if environment == 'space' and type_ == 'build':
+ md = (
+ f"# SPACE BUILD\n\n**Basic Information** | **Data** \n:--- | :--- \n"
+ f"*Ship Name* | {self._build['space']['ship_name']} \n"
+ f"*Ship Class* | {self._build['space']['ship']} \n"
+ f"*Ship Tier* | {self._build['space']['tier']} \n"
+ f"*Player Career* | {self._build['captain']['career']} \n"
+ f"*Elite Captain* | {'✓' if self._build['captain']['elite'] else '✗'}\n"
+ f"*Player Species* | {self._build['captain']['species']} \n"
+ f"*Primary Specialization* | {self._build['captain']['primary_spec']} \n"
+ f"*Secondary Specialization* | {self._build['captain']['secondary_spec']} \n\n\n"
+ )
+ if self._build['space']['ship_desc']:
+ md += f"## Build Description\n\n{self._build['space']['ship_desc']}\n\n\n"
+
+ md += '## Ship Equipment\n\n'
+ equip_table = [['**Basic Information**', '**Component**', '**Notes**']]
+ equip_table += self.md_equipment_table('space', 'fore_weapons', 'Fore Weapons')
+ equip_table += self.md_equipment_table('space', 'aft_weapons', 'Aft Weapons')
+ equip_table += self.md_equipment_table(
+ 'space', 'deflector', 'Deflector', single_line=True)
+ if self.build['space']['sec_def'][0]:
+ equip_table += self.md_equipment_table(
+ 'space', 'sec_def', 'Secondary Deflector', single_line=True)
+ equip_table += self.md_equipment_table(
+ 'space', 'engines', 'Impulse Engines', single_line=True)
+ equip_table += self.md_equipment_table('space', 'core', 'Warp', single_line=True)
+ equip_table += self.md_equipment_table('space', 'shield', 'Shield', single_line=True)
+ equip_table += self.md_equipment_table('space', 'devices', 'Devices')
+ if self.build['space']['experimental'][0]:
+ equip_table += self.md_equipment_table(
+ 'space', 'experimental', 'Experimental Weapon', single_line=True)
+ if self._build['space']['hangars'][0] or self._build['space']['hangars'][1]:
+ equip_table += self.md_equipment_table('space', 'hangars', 'Hangars')
+ equip_table += self.md_equipment_table('space', 'uni_consoles', 'Universal Consoles')
+ equip_table += self.md_equipment_table('space', 'eng_consoles', 'Engineering Consoles')
+ equip_table += self.md_equipment_table('space', 'sci_consoles', 'Science Consoles')
+ equip_table += self.md_equipment_table('space', 'tac_consoles', 'Tactical Consoles')
+ md += self.create_md_table(equip_table)
+
+ md += '\n\n\n## Bridge Officer Stations\n\n'
+ boff_table = [['**Profession**', '**Power**', '**Notes**']]
+ for specs, station in zip(
+ self._build['space']['boff_specs'], self._build['space']['boffs']):
+ if any(specs):
+ station_name = BOFF_RANKS_MD[station.count(None)] + ' ' + specs[0]
+ if specs[1] != '':
+ station_name += ' / ' + specs[1]
+ boff_table += self.md_boff_table(station, station_name)
+ md += self.create_md_table(boff_table)
+
+ md += '\n\n\n## Traits\n\n'
+ trait_table = [['**Starship Traits**', '**Notes**']]
+ for trait in notempty(self._build['space']['starship_traits']):
+ trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
+ md += self.create_md_table(trait_table)
+ md += '\n\n\n\n'
+ trait_table = [['**Personal Space Traits**', '**Notes**']]
+ for trait in notempty(self.build['space']['traits']):
+ trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
+ md += self.create_md_table(trait_table)
+ md += '\n\n\n\n'
+ trait_table = [['**Space Reputation Traits**', '**Notes**']]
+ for trait in notempty(self.build['space']['rep_traits']):
+ trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
+ md += self.create_md_table(trait_table)
+ md += '\n\n\n\n'
+ trait_table = [['**Active Space Reputation Traits**', '**Notes**']]
+ for trait in notempty(self.build['space']['active_rep_traits']):
+ trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
+ md += self.create_md_table(trait_table)
+
+ md += '\n\n\n## Active Space Duty Officers\n\n'
+ doff_table = [['**Specialization**', '**Power**', '**Notes**']]
+ for spec, variant in zip(
+ self._build['space']['doffs_spec'], self._build['space']['doffs_variant']):
+ if spec != '':
+ doff_table.append(
+ [f"[{spec}]({wiki_url(spec, 'Specialization: ')})", variant, ''])
+ md += self.create_md_table(doff_table)
+ return md
+ elif environment == 'ground' and type_ == 'build':
+ md = (
+ f"# GROUND BUILD\n\n**Basic Information** | **Data** \n:--- | :--- \n"
+ f"*Player Name* | {self._build['captain']['name']} \n"
+ f"*Player Species* | {self._build['captain']['species']} \n"
+ f"*Player Career* | {self._build['captain']['career']} \n"
+ f"*Elite Captain* | {'✓' if self._build['captain']['elite'] else '✗'}\n"
+ f"*Primary Specialization* | {self._build['captain']['primary_spec']} \n"
+ f"*Secondary Specialization* | {self._build['captain']['secondary_spec']} \n\n\n"
+ )
+ if self._build['ground']['ground_desc'] != '':
+ md += f"## Build Description\n\n{self._build['ground']['ground_desc']}\n\n\n"
+
+ md += '## Personal Equipment\n\n'
+ equip_table = [[' ', '**Component**', '**Notes**']]
+ equip_table += self.md_equipment_table('ground', 'kit', 'Kit Frame', single_line=True)
+ equip_table += self.md_equipment_table('ground', 'kit_modules', 'Kit Modules')
+ equip_table += self.md_equipment_table(
+ 'ground', 'armor', 'Body Armor', single_line=True)
+ equip_table += self.md_equipment_table('ground', 'ev_suit', 'EV Suit', single_line=True)
+ equip_table += self.md_equipment_table(
+ 'ground', 'personal_shield', 'Personal Shield', single_line=True)
+ equip_table += self.md_equipment_table('ground', 'weapons', 'Weapons')
+ equip_table += self.md_equipment_table('ground', 'ground_devices', 'Devices')
+ md += self.create_md_table(equip_table)
+
+ md += '\n\n\n## Traits\n\n'
+ trait_table = [['**Personal Ground Traits**', '**Notes**']]
+ for trait in notempty(self._build['ground']['traits']):
+ trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
+ md += self.create_md_table(trait_table)
+ md += '\n\n\n\n'
+ trait_table = [['**Ground Reputation Traits**', '**Notes**']]
+ for trait in notempty(self._build['ground']['rep_traits']):
+ trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
+ md += self.create_md_table(trait_table)
+ md += '\n\n\n\n'
+ trait_table = [['**Active Ground Reputation Traits**', '**Notes**']]
+ for trait in notempty(self._build['ground']['active_rep_traits']):
+ trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
+ md += self.create_md_table(trait_table)
+
+ md += '\n\n\n## Active Ground Duty Officers\n\n'
+ doff_table = [['**Specialization**', '**Power**', '**Notes**']]
+ for spec, variant in zip(
+ self._build['ground']['doffs_spec'], self._build['ground']['doffs_variant']):
+ if spec != '':
+ doff_table.append(
+ [f"[{spec}]({wiki_url(spec, 'Specialization: ')})", variant, ''])
+ md += self.create_md_table(doff_table)
+
+ md += '\n\n\n## Away Team\n\n'
+ boff_table = [['**Profession**', '**Power**', '**Notes**']]
+ for profession, specialization, station in zip(
+ self._build['ground']['boff_profs'], self._build['ground']['boff_specs'],
+ self._build['ground']['boffs']):
+ station_name = f"{profession} / {specialization}"
+ boff_table += self.md_boff_table(station, station_name)
+ md += self.create_md_table(boff_table)
+ return md
+ elif environment == 'space' and type_ == 'skills':
+ md = '# Space Skills\n\n'
+ skill_table = [[
+ '**Engineering**', '', '',
+ '**Science**', '', '',
+ '**Tactical**', ' '
+ ]]
+ offset = 0
+ for rank_skills in self._cargo.skills['space']:
+ skill_table += self.md_skill_table_space(rank_skills, offset)
+ skill_table.append([' '] + [''] * 6 + [' '])
+ offset += 6
+ md += self.create_md_table(skill_table, alignment=[':-:'] * 8)
+ md += '\n\n\n\n'
+
+ unlock_table = [
+ [f"**[Unlocks]({wiki_url('Skill#Space_2')})**"] + [''] * 7 + [' ']
+ ]
+ for career, career_name in CAREER_ABBR.items():
+ row = [f"**{career_name}**"]
+ for i, unlock_state in enumerate(self._build['skill_unlocks'][career]):
+ if unlock_state is None:
+ row.append('')
+ else:
+ unlock_slot = self._cargo.skills['space_unlocks'][career][i]
+ if unlock_slot['points_required'] == 24:
+ skill_count = self._cargo.skills[f"space_points_{career}"]
+ link = wiki_url(unlock_slot['name'], 'Ability: ')
+ if unlock_state is None:
+ row += ['', '', '', ' ']
+ elif unlock_state == -1:
+ row += [f"[{unlock_slot['name']}]({link})", '', '', ' ']
+ elif unlock_state == 3:
+ row += [
+ f"[{unlock_slot['name']}]({link})",
+ unlock_slot['options'][0]['name'],
+ unlock_slot['options'][1]['name'],
+ unlock_slot['options'][2]['name'],
+ ]
+ elif skill_count == 25:
+ row += [
+ f"[{unlock_slot['name']}]({link})",
+ unlock_slot['options'][unlock_state]['name']
+ ]
+ elif skill_count == 26:
+ row.append(f"[{unlock_slot['name']}]({link})")
+ enhancements = [
+ unlock_slot['options'][0]['name'],
+ unlock_slot['options'][1]['name'],
+ unlock_slot['options'][2]['name'],
+ ' '
+ ]
+ enhancements.pop(unlock_state)
+ row += enhancements
+ else:
+ row.append(unlock_slot['nodes'][unlock_state]['name'])
+ if row[-1] == '':
+ row[-1] = ' '
+ unlock_table.append(row)
+ md += self.create_md_table(unlock_table)
+ return md
+ elif environment == 'ground' and type_ == 'skills':
+ md = '# Ground Skills\n\n'
+ skill_table = [['**Skill**', '**I**', '**II**']]
+ id_offset = 0
+ for skill in self._cargo.skills['ground']:
+ row = [f"[{skill['nodes'][0]['name']}]({skill['link']})"]
+ if self.build['ground_skills'][skill['tree']][id_offset]:
+ row.append('[X]')
+ else:
+ row.append('[ ]')
+ if self.build['ground_skills'][skill['tree']][id_offset + 1]:
+ row.append('[X]')
+ else:
+ row.append('[ ]')
+ skill_table.append(row)
+ if skill['tree'] < 2 and id_offset == 4 or skill['tree'] >= 2 and id_offset == 2:
+ id_offset = 0
+ else:
+ id_offset += 2
+ md += self.create_md_table(skill_table, alignment=[':--', ':-:', ':-:'])
+ md += '\n\n\n\n'
+
+ unlock_table = [['', f"**[Unlocks]({wiki_url('Skill#Ground_2')})**", '']]
+ for unlock, unlock_state in zip(
+ self._cargo.skills['ground_unlocks'], self._build['skill_unlocks']['ground']):
+ if unlock_state is not None:
+ unlock_table.append(['', unlock['nodes'][unlock_state]['name'], ''])
+ md += self.create_md_table(unlock_table, alignment=['', ':-:', ''])
+ return md
diff --git a/src/subwindows.py b/src/subwindows.py
index 9f80cd7..c40bb89 100644
--- a/src/subwindows.py
+++ b/src/subwindows.py
@@ -490,77 +490,3 @@ def edit_item(self, item: dict, modifiers: dict):
mod_combo.setCurrentText('')
self._item = self.empty_item
return self._result
-
-
-class ExportWindow(QDialog):
- """
- Holds Export Window
- """
- def __init__(self, sets, parent_window, data_getter: Callable):
- super().__init__(parent=parent_window)
- thick = sets.theme['app']['frame_thickness'] * sets.config.ui_scale
- dialog_layout = VBoxLayout(margins=thick)
- main_frame = create_frame(sets, size_policy=SMINMIN)
- dialog_layout.addWidget(main_frame)
- main_layout = VBoxLayout(margins=thick, spacing=thick)
- content_frame = create_frame(sets, size_policy=SMINMIN)
- content_layout = VBoxLayout(spacing=thick)
- content_layout.setAlignment(ATOP)
-
- header_label = create_label(sets, 'Markdown Export:', 'label_heading')
- content_layout.addWidget(header_label, alignment=ALEFT)
- md_textedit = QPlainTextEdit()
- button_def = {
- 'default': {'margin-top': 0},
- 'Space Build': {
- 'callback': lambda: md_textedit.setPlainText(data_getter('space', 'build'))
- },
- 'Ground Build': {
- 'callback': lambda: md_textedit.setPlainText(data_getter('ground', 'build'))
- },
- 'Space Skills': {
- 'callback': lambda: md_textedit.setPlainText(data_getter('space', 'skills'))
- },
- 'Ground Skills': {
- 'callback': lambda: md_textedit.setPlainText(data_getter('ground', 'skills'))
- },
- }
- top_buttons, (self._space_button, *_) = create_button_series(sets, button_def, ret=True)
- top_buttons.setAlignment(AHCENTER)
- content_layout.addLayout(top_buttons)
- md_textedit.setSizePolicy(SMINMIN)
- md_textedit.setStyleSheet(get_style_class(sets, 'QPlainTextEdit', 'textedit'))
- md_textedit.setFont(theme_font(sets, 'textedit'))
- md_textedit.setWordWrapMode(QTextOption.WrapMode.NoWrap)
- content_layout.addWidget(md_textedit, stretch=1)
- content_frame.setLayout(content_layout)
- main_layout.addWidget(content_frame, stretch=1)
-
- seperator = create_frame(sets, style='light_frame', size_policy=SMINMAX)
- seperator.setFixedHeight(1)
- main_layout.addWidget(seperator)
- footer_button_def = {
- 'Copy': {'callback': lambda: sets.app.clipboard().setText(md_textedit.toPlainText())},
- 'Close': {'callback': lambda: self.done(0)}
- }
- footer_buttons = create_button_series(sets, footer_button_def)
- footer_buttons.setAlignment(AHCENTER)
- main_layout.addLayout(footer_buttons)
- main_frame.setLayout(main_layout)
-
- self.setLayout(dialog_layout)
- self.setWindowTitle('SETS - Markdown Export')
- self.setStyleSheet(get_style(sets, 'dialog_window'))
-
- def invoke(self):
- """
- Shows Export Window.
- """
- window_rect = self.parent().geometry()
- self.setGeometry(
- window_rect.x() + window_rect.width() * 0.25,
- window_rect.y() + window_rect.height() * 0.25,
- window_rect.width() * 0.5,
- window_rect.height() * 0.5)
- self._space_button.click()
- self.exec()
diff --git a/src/widgetbuilder.py b/src/widgetbuilder.py
index 095c4d7..0d4d4d2 100644
--- a/src/widgetbuilder.py
+++ b/src/widgetbuilder.py
@@ -10,13 +10,127 @@
doff_variant_callback, picker, skill_callback_ground, skill_callback_space,
skill_unlock_callback)
from .constants import (
- ABOTTOM, AHCENTER, ALEFT, ATOP, AVCENTER, CALLABLE, CAREERS, GROUND_BOFF_SPECS, SMAXMAX,
- SMAXMIN, SMINMAX)
+ ABOTTOM, ACENTER, AHCENTER, ALEFT, ATOP, AVCENTER, CALLABLE, CAREERS, GROUND_BOFF_SPECS,
+ SMAXMAX, SMAXMIN, SMINMAX)
from .style import get_style, get_style_class, merge_style, theme_font
from .textedit import format_skill_tooltip
+from .theme import AppTheme
from .widgets import DoffCombobox, GridLayout, HBoxLayout, ItemButton, TooltipLabel, VBoxLayout
+def create_frame2(
+ theme: AppTheme, style: str = 'frame', style_override: dict = {},
+ size_policy: QSizePolicy | None = None) -> QFrame:
+ """
+ Creates a frame with default styling
+
+ Parameters:
+ - :param theme: reference to AppTheme
+ - :param style: style dict to override default style (optional)
+ - :param size_policy: size policy of the frame (optional)
+
+ :return: configured QFrame
+ """
+ frame = QFrame()
+ frame.setStyleSheet(theme.get_style(style, style_override))
+ frame.setSizePolicy(size_policy if size_policy is not None else SMAXMAX)
+ return frame
+
+
+def create_label2(theme: AppTheme, text: str, style: str = 'label', style_override={}) -> QLabel:
+ """
+ Creates a label according to style with parent.
+
+ Parameters:
+ - :param theme: reference to AppTheme
+ - :param text: text to be shown on the label
+ - :param style: name of the style as in self.theme
+ - :param style_override: style dict to override default style (optional)
+
+ :return: configured QLabel
+ """
+ label = QLabel()
+ label.setText(text)
+ label.setStyleSheet(theme.get_style_class('QLabel', style, style_override))
+ label.setSizePolicy(SMAXMAX)
+ if 'font' in style_override:
+ label.setFont(theme.get_font(style, style_override['font']))
+ else:
+ label.setFont(theme.get_font(style))
+ return label
+
+
+def create_button_series2(
+ theme: AppTheme, buttons: dict[str, dict], style: str = 'button', shape: str = 'row',
+ separator: str = '', ret: bool = False) -> (
+ VBoxLayout | HBoxLayout | tuple[VBoxLayout | HBoxLayout, list[QPushButton]]):
+ """
+ Creates a row / column of buttons.
+
+ Parameters:
+ - :param theme: reference to AppTheme
+ - :param buttons: dictionary containing button details
+ - key "default" contains style override for all buttons (optional)
+ - all other keys represent one button, key will be the text on the button; value for the
+ key contains dict with details for the specific button (all optional)
+ - "callback": callable that will be called on button click
+ - "style": individual style override dict
+ - "toggle": True or False when button should be a toggle button, None when it should
+ be a normal button; the bool value indicates the default state of the button
+ - "stretch": stretch value for the button
+ - "align": alignment flag for button
+ - :param style: key for AppTheme -> default style
+ - :param shape: row / column
+ - :param separator: string seperator displayed between buttons (optional)
+ - :param ret: set to true to return list of created buttons along with layout
+
+ :return: populated QVBoxlayout / QHBoxlayout
+ """
+ if 'default' in buttons:
+ defaults = theme.merge_style(theme[style], buttons.pop('default'))
+ else:
+ defaults = theme[style]
+
+ if shape == 'column':
+ layout = VBoxLayout()
+ else:
+ shape = 'row'
+ layout = HBoxLayout()
+
+ if separator != '':
+ sep_style = {
+ 'color': defaults['color'], 'margin': 0, 'padding': 0, 'background': '#00000000'}
+
+ button_list = []
+ for i, (name, detail) in enumerate(buttons.items()):
+ if 'style' in detail:
+ button_style = theme.merge_style(defaults, detail['style'])
+ else:
+ button_style = defaults
+ toggle_button = detail['toggle'] if 'toggle' in detail else None
+ bt = create_button(theme, name, style, button_style, toggle_button)
+ if 'callback' in detail and isinstance(detail['callback'], CALLABLE):
+ if toggle_button:
+ bt.clicked[bool].connect(detail['callback'])
+ else:
+ bt.clicked.connect(detail['callback'])
+ stretch = detail['stretch'] if 'stretch' in detail else 0
+ if 'align' in detail:
+ layout.addWidget(bt, stretch, detail['align'])
+ else:
+ layout.addWidget(bt, stretch)
+ button_list.append(bt)
+ if separator != '' and i < (len(buttons) - 1):
+ sep_label = create_label(theme, separator, 'label', sep_style)
+ sep_label.setSizePolicy(SMAXMIN)
+ layout.addWidget(sep_label, alignment=ACENTER)
+
+ if ret:
+ return layout, button_list
+ else:
+ return layout
+
+
def create_frame(self, style='frame', style_override={}, size_policy=None) -> QFrame:
"""
Creates a frame with default styling and parent
From 58b42bbec33ab03bc8d69d646ea40c94d12c762a Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Sun, 10 May 2026 08:26:32 +0200
Subject: [PATCH 09/44] renaming picker module
---
src/{subwindows.py => picker.py} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename src/{subwindows.py => picker.py} (100%)
diff --git a/src/subwindows.py b/src/picker.py
similarity index 100%
rename from src/subwindows.py
rename to src/picker.py
From 7d9070cd4521df914b6ef7c8c7dd1027e9412558 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Mon, 11 May 2026 09:48:50 +0200
Subject: [PATCH 10/44] Updating picker
- uses new theme
- uses signal to return results
- uses imagemanager
---
src/imagemanager.py | 20 +++-
src/picker.py | 269 +++++++++++++++++++++++++------------------
src/widgetbuilder.py | 119 +++++++++++++++++++
src/widgets.py | 4 +-
4 files changed, 295 insertions(+), 117 deletions(-)
diff --git a/src/imagemanager.py b/src/imagemanager.py
index e80ed21..17154dd 100644
--- a/src/imagemanager.py
+++ b/src/imagemanager.py
@@ -50,7 +50,7 @@ def __init__(
self._images: dict[str, QImage] = dict()
self.image_set: set[str] = set()
self.failed_images: dict[str, int] = dict()
-
+
def get(self, image_name: str) -> QImage:
"""
Returns image from cache if cached, loads and returns image if not cached.
@@ -63,6 +63,20 @@ def get(self, image_name: str) -> QImage:
image.load(self._images_dir / get_image_file_name(image_name))
return image
+ def get_alt(self, image_name: str, image_suffix: str = '') -> QImage:
+ """
+ Returns image from cache if cached, loads and returns image if not cached. Tries to get
+ alternate image first.
+
+ Parameters:
+ - :param image_name: name of the image
+ - :param image_suffix: suffix to check in self.cache.alt_images
+ """
+ if image_name + image_suffix in self._cargo_cache.alt_images:
+ return self.get(self._cargo_cache.alt_images[image_name + image_suffix])
+ else:
+ return self.get(image_name)
+
def get_downloaded_icons(self) -> set[str]:
"""
Returns set containing all images currently in the images folder.
@@ -137,7 +151,7 @@ def get_ship_image(self, image_name: str) -> QImage:
self._downloader.download_ship_image(image_name, {})
image = QImage(image_path)
return image
-
+
def load_base_images(self):
"""
Loads all images that are required for the app to start (skills, overlays)
@@ -169,7 +183,7 @@ def load_base_images(self):
self._images_dir / get_image_file_name('Probability Manipulation'))
self._images['EPS Corruption'] = QImage(
self._images_dir / get_image_file_name('EPS Corruption'))
-
+
def load_images(self):
"""
Loads images from drive.
diff --git a/src/picker.py b/src/picker.py
index c40bb89..627b73f 100644
--- a/src/picker.py
+++ b/src/picker.py
@@ -1,24 +1,39 @@
-from typing import Callable, Iterable, Iterator
+from typing import Iterable, Iterator
-from PySide6.QtCore import QPoint, QSortFilterProxyModel, QStringListModel, Qt
-from PySide6.QtGui import QMouseEvent, QTextOption
-from PySide6.QtWidgets import QAbstractItemView, QDialog, QListView, QPlainTextEdit
+from PySide6.QtCore import (
+ QModelIndex, QPoint, QSortFilterProxyModel, QStringListModel, Qt, Signal, Slot)
+from PySide6.QtGui import QMouseEvent
+from PySide6.QtWidgets import (
+ QAbstractItemView, QComboBox, QDialog, QFrame, QLabel, QListView, QWidget)
-from .constants import AHCENTER, ALEFT, ATOP, MARKS, RARITIES, SMAXMAX, SMINMAX, SMINMIN
-from .iofunc import alt_image
+from .config import SETSSettings
+from .constants import AHCENTER, ALEFT, MARKS, RARITIES, SMAXMAX, SMINMAX, SMINMIN
+from .imagemanager import ImageManager
+from .theme import AppTheme
from .widgetbuilder import (
- create_button, create_button_series, create_combo_box, create_entry, create_frame,
- create_item_button, create_label)
-from .widgets import GridLayout, HBoxLayout, VBoxLayout
-from .style import get_style, get_style_class, theme_font
+ create_button2, create_combo_box2, create_entry2, create_frame2, create_item_button2,
+ create_label2)
+from .widgets import GridLayout, HBoxLayout, ItemButton, ItemSlot, VBoxLayout
class BasePicker(QDialog):
"""
Base class of SETS item picker / editor housing shared methods.
"""
+
+ dialog_result: Signal = Signal(dict, ItemSlot)
+
+ def __init__(self, parent: QWidget):
+ super().__init__(parent=parent)
+ self._item: dict[str, str | list[str]] = self.empty_item
+ self._slot: ItemSlot | None = None
+ self._modifiers: dict[str, dict[str]] = {}
+ self._mod_combos: list[QComboBox | None] = [None] * 5
+ self._mark_combo: QComboBox
+ self._rarity_combo: QComboBox
+
@property
- def empty_item(self):
+ def empty_item(self) -> dict[str, str | list[str]]:
return {
'item': '',
'rarity': 'Common',
@@ -26,7 +41,7 @@ def empty_item(self):
'modifiers': [''] * 5
}
- def insert_modifiers(self, modifiers: dict = {}):
+ def insert_modifiers(self, modifiers: dict[str] = {}):
"""
Inserts the modifiers into the comboboxes
"""
@@ -42,7 +57,7 @@ def insert_modifiers(self, modifiers: dict = {}):
self._mod_combos[4].clear()
self._mod_combos[4].addItems(self.epic_mods(modifiers))
- def unique_mods(self, modifiers: dict = {}) -> Iterator[str]:
+ def unique_mods(self, modifiers: dict[str] = {}) -> Iterator[str]:
"""
yields mods for first mod slot from modifier dict
"""
@@ -51,7 +66,7 @@ def unique_mods(self, modifiers: dict = {}) -> Iterator[str]:
if not details['epic']:
yield mod
- def standard_mods(self, modifiers: dict = {}) -> Iterator[str]:
+ def standard_mods(self, modifiers: dict[str] = {}) -> Iterator[str]:
"""
yields mods for second to fourth mod slot from modifier list
"""
@@ -60,7 +75,7 @@ def standard_mods(self, modifiers: dict = {}) -> Iterator[str]:
if not details['epic'] and not details['isunique']:
yield mod
- def not_epic_mods(self, modifiers: dict = {}) -> Iterator[str]:
+ def not_epic_mods(self, modifiers: dict[str] = {}) -> Iterator[str]:
"""
yields mods for first to fourth mod slot from modifier list
"""
@@ -69,7 +84,7 @@ def not_epic_mods(self, modifiers: dict = {}) -> Iterator[str]:
if not details['epic']:
yield mod
- def epic_mods(self, modifiers: dict = {}) -> Iterator[str]:
+ def epic_mods(self, modifiers: dict[str] = {}) -> Iterator[str]:
"""
yields mods for fifth mod slot from modifier list
"""
@@ -112,47 +127,51 @@ class Picker(BasePicker):
Picker Window
"""
def __init__(
- self, sets, parent_window, style: str = 'picker',
- default_rarity_getter: Callable = lambda: 'Common',
- default_mark_getter: Callable = lambda: ''):
+ self, theme: AppTheme, parent_window: QWidget, settings: SETSSettings,
+ images: ImageManager, style: str = 'picker'):
super().__init__(parent=parent_window)
- self.start_pos = None
+ self._settings: SETSSettings = settings
+ self._images: ImageManager = images
+ self.start_pos: QPoint | None = None
+ self._image_suffix: str = ''
+ self._item_button: ItemButton
+ self._item_label: QLabel
+ self._prop_frame: QFrame
+ self._item_model: QStringListModel
+ self._sort_model: QSortFilterProxyModel
+ self._items_list: QListView
+
self.setWindowFlags(
- self.windowFlags() | Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
- self.setStyleSheet(get_style(sets, style))
+ self.windowFlags() | Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
+ self.setStyleSheet(theme.get_style(style))
self.setWindowModality(Qt.WindowModality.WindowModal)
self.setMinimumSize(10, 10)
self.setSizePolicy(SMAXMAX)
- self._sets = sets
- self._item = self.empty_item
- self._result = None
- self._modifiers = {}
- self._image_suffix = ''
- ui_scale = sets.config.ui_scale
- spacing = sets.theme['defaults']['isp'] * ui_scale
+ ui_scale = theme.scale
+ spacing = theme['defaults']['isp'] * ui_scale
layout = VBoxLayout(margins=(spacing, 0, spacing, spacing), spacing=0)
top_layout = HBoxLayout(spacing=spacing)
button_layout = VBoxLayout(margins=(0, spacing, 0, spacing))
- button_frame = create_frame(sets, style_override={'background': 'none'})
- self._item_button = create_item_button(sets)
+ button_frame = create_frame2(theme, style_override={'background': 'none'})
+ self._item_button = create_item_button2(theme)
button_layout.addWidget(self._item_button)
button_frame.setLayout(button_layout)
top_layout.addWidget(button_frame, alignment=ALEFT)
- self._item_label = create_label(
- sets, '- ', 'label_subhead', style_override={'margin-bottom': 0})
+ self._item_label = create_label2(
+ theme, '
- ', 'label_subhead', style_override={'margin-bottom': 0})
self._item_label.setWordWrap(True)
self._item_label.setSizePolicy(SMINMAX)
top_layout.addWidget(self._item_label, stretch=1)
layout.addLayout(top_layout)
- self._prop_frame = create_frame(sets, size_policy=SMINMAX)
- csp = sets.theme['defaults']['csp'] * ui_scale
+ self._prop_frame = create_frame2(theme, size_policy=SMINMAX)
+ csp = theme['defaults']['csp'] * ui_scale
prop_layout = VBoxLayout(spacing=csp)
rarity_layout = HBoxLayout(spacing=csp)
- self._mark_combo = create_combo_box(sets)
+ self._mark_combo = create_combo_box2(theme)
self._mark_combo.addItems(('', *MARKS))
self._mark_combo.currentTextChanged.connect(self.mark_callback)
rarity_layout.addWidget(self._mark_combo, 1)
- self._rarity_combo = create_combo_box(sets)
+ self._rarity_combo = create_combo_box2(theme)
self._rarity_combo.addItems(RARITIES.keys())
self._rarity_combo.currentTextChanged.connect(self.rarity_callback)
rarity_layout.addWidget(self._rarity_combo, 1)
@@ -160,66 +179,63 @@ def __init__(
mod_layout = GridLayout(spacing=csp)
self._mod_combos = [None] * 5
for i in range(4):
- mod_combo = create_combo_box(sets, style_override={'font': '@font'}, editable=True)
+ mod_combo = create_combo_box2(theme, style_override={'font': '@font'}, editable=True)
mod_combo.currentIndexChanged.connect(lambda mod, i=i: self.modifier_callback(mod, i))
self._mod_combos[i] = mod_combo
mod_layout.addWidget(mod_combo, i // 2, i % 2)
- mod_combo = create_combo_box(sets, style_override={'font': '@font'}, editable=True)
+ mod_combo = create_combo_box2(theme, style_override={'font': '@font'}, editable=True)
mod_combo.currentIndexChanged.connect(lambda mod: self.modifier_callback(mod, 4))
self._mod_combos[4] = mod_combo
mod_layout.addWidget(mod_combo, 2, 0, 1, 2)
prop_layout.addLayout(mod_layout)
- spacer_1 = create_frame(sets)
+ spacer_1 = create_frame2(theme)
spacer_1.setFixedHeight(spacing - csp)
prop_layout.addWidget(spacer_1)
self._prop_frame.setLayout(prop_layout)
layout.addWidget(self._prop_frame)
- seperator = create_frame(sets, size_policy=SMINMAX, style_override={
- 'background-color': '@lbg', 'margin': '@isp'})
- seperator.setFixedHeight(sets.theme['defaults']['sep'] * ui_scale)
+ seperator = create_frame2(theme, size_policy=SMINMAX, style_override={
+ 'background-color': '@lbg', 'margin': '@isp'})
+ seperator.setFixedHeight(theme['defaults']['sep'] * ui_scale)
layout.addWidget(seperator)
- spacer_2 = create_frame(sets)
+ spacer_2 = create_frame2(theme)
spacer_2.setFixedHeight(spacing)
layout.addWidget(spacer_2)
self._item_model = QStringListModel()
self._sort_model = QSortFilterProxyModel()
self._sort_model.setSourceModel(self._item_model)
self._sort_model.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
- self._search_bar = create_entry(sets, placeholder='Search')
+ self._search_bar = create_entry2(theme, placeholder='Search')
self._search_bar.textChanged.connect(
- lambda new_text: self._sort_model.setFilterFixedString(new_text))
+ lambda new_text: self._sort_model.setFilterFixedString(new_text))
self._search_bar.setSizePolicy(SMINMAX)
layout.addWidget(self._search_bar)
- spacer_3 = create_frame(sets)
+ spacer_3 = create_frame2(theme)
spacer_3.setFixedHeight(spacing)
layout.addWidget(spacer_3)
self._items_list = QListView()
- self._items_list.setStyleSheet(get_style_class(sets, 'QListView', 'picker_list'))
+ self._items_list.setStyleSheet(theme.get_style_class('QListView', 'picker_list'))
self._items_list.setSizePolicy(SMINMIN)
self._items_list.setModel(self._sort_model)
self._items_list.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self._items_list.clicked.connect(self.slot_item)
self._items_list.doubleClicked.connect(self.select_item)
layout.addWidget(self._items_list)
- spacer_4 = create_frame(sets)
+ spacer_4 = create_frame2(theme)
spacer_4.setFixedHeight(spacing)
layout.addWidget(spacer_4)
control_layout = HBoxLayout(spacing=csp)
- cancel_button = create_button(sets, 'Cancel')
+ cancel_button = create_button2(theme, 'Cancel')
cancel_button.setSizePolicy(SMINMAX)
cancel_button.clicked.connect(self.reject)
control_layout.addWidget(cancel_button)
- save_button = create_button(sets, 'Save')
+ save_button = create_button2(theme, 'Save')
save_button.clicked.connect(self.accept)
save_button.setSizePolicy(SMINMAX)
control_layout.addWidget(save_button)
layout.addLayout(control_layout)
self.setLayout(layout)
- self._get_default_rarity = default_rarity_getter
- self._get_default_mark = default_mark_getter
-
- def slot_item(self, new_index):
+ def slot_item(self, new_index: QModelIndex):
"""
called when item is clicked
"""
@@ -228,11 +244,11 @@ def slot_item(self, new_index):
self._item_label.setText(new_item)
if new_item.endswith('I'):
new_item, _, _ = new_item.rpartition(' ')
- self._item_button.set_item(alt_image(self._sets, new_item, self._image_suffix))
+ self._item_button.set_item(self._images.get_alt(new_item, self._image_suffix))
for i in range(5):
self._mod_combos[i].setCurrentText('')
- def select_item(self, new_index):
+ def select_item(self, new_index: QModelIndex):
"""
shortcut for selecting item and pressing ok
"""
@@ -241,10 +257,17 @@ def select_item(self, new_index):
self.accept()
def pick_item(
- self, items: Iterable, button_pos: QPoint | None, equipment: bool = False,
- modifiers: dict = {}, image_suffix: str = ''):
+ self, items: Iterable[str], button_pos: QPoint | None, slot: ItemSlot,
+ modifiers: dict[str, dict[str]] = {}, image_suffix: str = ''):
"""
- Executes picker, returns selected item. Returns None when picker is closed without saving.
+ Shows picker window. Returns immediately.
+
+ Parameters:
+ - :param items: collection of items to select from
+ - :param button_pos: positions picker next to this position if not `None`
+ - :param slot: information about the slot
+ - :param modifiers: collection of modifiers
+ - :param image_suffix: suffix containing environment and type to check for alternative icon
"""
window = self.parentWidget()
if button_pos is None:
@@ -259,29 +282,43 @@ def pick_item(
)
window_position = (button_pos.x() - window_size[0] * 1.05, button_pos.y())
self._result = None
+ self._slot = slot
self.setFixedSize(*window_size)
self.move(*window_position)
self._item_model.setStringList(items)
self._item_label.setMinimumWidth(window_size[0] * 0.75)
self._sort_model.sort(0, Qt.SortOrder.AscendingOrder)
self._items_list.scrollToTop()
- if equipment:
+ if slot.is_equipment:
self.insert_modifiers(modifiers)
- self._mark_combo.setCurrentText(self._get_default_mark())
- self._rarity_combo.setCurrentText(self._get_default_rarity())
+ self._mark_combo.setCurrentText(self._settings.default_mark)
+ self._rarity_combo.setCurrentText(self._settings.default_rarity)
self._prop_frame.show()
else:
self._prop_frame.hide()
self._image_suffix = image_suffix
self._search_bar.setFocus()
- action = self.exec()
+ self.open()
+
+ @Slot(int)
+ def finish_pick(self, action: int):
+ """
+ Completes the pick action, resets the dialog and emits the data using the `dialog_result`
+ signal.
+
+ Parameters:
+ - :param action: indicates whether the result should be saved (`1`) or not (`0`)
+ """
+ slot = self._slot
if action == 1 and self._item['item'] != '':
- self._result = {
+ picked_item = {
'item': self._item['item'],
'rarity': self._item['rarity'],
'mark': self._item['mark'],
'modifiers': [mod for mod in self._item['modifiers']]
}
+ else:
+ picked_item = self.empty_item
self._item_button.clear()
self._search_bar.clear()
self._item_label.setText('')
@@ -291,12 +328,14 @@ def pick_item(
for mod_combo in self._mod_combos:
mod_combo.setCurrentText('')
self._item = self.empty_item
- return self._result
+ self._slot = None
+ self.dialog_result.emit(picked_item, slot)
def mousePressEvent(self, event: QMouseEvent):
pr = self._prop_frame.rect()
pr.moveTopLeft(self._prop_frame.pos())
if pr.contains(event.pos()):
+ # allowing window move to start here can cause accidental clicks on comboboxes
self.start_pos = None
else:
self.start_pos = event.globalPosition().toPoint()
@@ -316,46 +355,45 @@ class ShipSelector(QDialog):
"""
Selection Window for ships
"""
- def __init__(self, sets, parent_window, style: str = 'picker'):
+ def __init__(self, theme: AppTheme, parent_window: QWidget, style: str = 'picker'):
super().__init__(parent=parent_window)
self.setWindowFlags(
- self.windowFlags() | Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
- self.setStyleSheet(get_style(sets, style))
+ self.windowFlags() | Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
+ self.setStyleSheet(theme.get_style(style))
self.setWindowModality(Qt.WindowModality.WindowModal)
self.setMinimumSize(10, 10)
self.setSizePolicy(SMAXMAX)
- ui_scale = sets.config.ui_scale
- spacing = sets.theme['defaults']['isp'] * ui_scale
+ ui_scale = theme.scale
+ spacing = theme['defaults']['isp'] * ui_scale
layout = VBoxLayout(margins=spacing, spacing=spacing)
self._ship_data_model = QStringListModel()
sort_model = QSortFilterProxyModel()
sort_model.setSourceModel(self._ship_data_model)
sort_model.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
- heading = create_label(sets, 'Select Ship', 'label_heading')
+ heading = create_label2(theme, 'Select Ship', 'label_heading')
layout.addWidget(heading, alignment=AHCENTER)
- self._search_bar = create_entry(sets, placeholder='Search')
+ self._search_bar = create_entry2(theme, placeholder='Search')
self._search_bar.textChanged.connect(
- lambda new_text: sort_model.setFilterFixedString(new_text))
+ lambda new_text: sort_model.setFilterFixedString(new_text))
self._search_bar.setSizePolicy(SMINMAX)
layout.addWidget(self._search_bar)
self._ship_list = QListView()
- self._ship_list.setStyleSheet(get_style_class(
- sets, 'QListView', 'picker_list',
- override={'::item:selected': {'border-color': '@sets'}}))
+ self._ship_list.setStyleSheet(theme.get_style_class(
+ 'QListView', 'picker_list', override={'::item:selected': {'border-color': '@sets'}}))
self._ship_list.setSizePolicy(SMINMIN)
self._ship_list.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self._ship_list.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self._ship_list.setModel(sort_model)
self._ship_list.doubleClicked.connect(self.accept)
layout.addWidget(self._ship_list)
- csp = sets.theme['defaults']['csp'] * ui_scale
+ csp = theme['defaults']['csp'] * ui_scale
control_layout = HBoxLayout(spacing=csp)
- cancel_button = create_button(sets, 'Cancel')
+ cancel_button = create_button2(theme, 'Cancel')
cancel_button.setSizePolicy(SMINMAX)
cancel_button.clicked.connect(self.reject)
control_layout.addWidget(cancel_button)
- save_button = create_button(sets, 'Save')
+ save_button = create_button2(theme, 'Save')
save_button.clicked.connect(self.accept)
save_button.setSizePolicy(SMINMAX)
control_layout.addWidget(save_button)
@@ -398,73 +436,66 @@ class ItemEditor(BasePicker):
"""
Dialog to edit mark, rarity and mods of equipment items.
"""
- def __init__(self, sets, parent_window, style: str = 'picker'):
- """
- Dialog to edit mark, rarity and mods of equipment items.
-
- Parameters:
- - :param sets: SETS object
- - :param parent_window: parent window of dialog
- - :param style: style key for sets.theme
- """
+ def __init__(self, theme: AppTheme, parent_window: QWidget, style: str = 'picker'):
super().__init__(parent=parent_window)
self.setWindowFlags(
- self.windowFlags() | Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
- self.setStyleSheet(get_style(sets, style))
+ self.windowFlags() | Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
+ self.setStyleSheet(theme.get_style(style))
self.setWindowModality(Qt.WindowModality.WindowModal)
self.setMinimumSize(10, 10)
self.setSizePolicy(SMAXMAX)
- self._item = self.empty_item
- self._result = None
- self._modifiers = {}
- ui_scale = sets.config.ui_scale
- csp = sets.theme['defaults']['csp'] * ui_scale
+ ui_scale = theme.scale
+ csp = theme['defaults']['csp'] * ui_scale
layout = VBoxLayout(spacing=csp)
rarity_layout = HBoxLayout(spacing=csp)
- self._mark_combo = create_combo_box(sets)
+ self._mark_combo = create_combo_box2(theme)
self._mark_combo.addItems(('', *MARKS))
self._mark_combo.currentTextChanged.connect(self.mark_callback)
rarity_layout.addWidget(self._mark_combo, 1)
- self._rarity_combo = create_combo_box(sets)
+ self._rarity_combo = create_combo_box2(theme)
self._rarity_combo.addItems(RARITIES.keys())
self._rarity_combo.currentTextChanged.connect(self.rarity_callback)
rarity_layout.addWidget(self._rarity_combo, 1)
layout.addLayout(rarity_layout)
mod_layout = GridLayout(spacing=csp)
- self._mod_combos = [None] * 5
for i in range(4):
- mod_combo = create_combo_box(
- sets, style_override={'font': '@font'}, editable=True, size_policy=SMINMAX)
+ mod_combo = create_combo_box2(
+ theme, style_override={'font': '@font'}, editable=True, size_policy=SMINMAX)
mod_combo.currentIndexChanged.connect(lambda mod, i=i: self.modifier_callback(mod, i))
self._mod_combos[i] = mod_combo
mod_layout.addWidget(mod_combo, i // 2, i % 2)
- mod_combo = create_combo_box(sets, style_override={'font': '@font'}, editable=True)
+ mod_combo = create_combo_box2(theme, style_override={'font': '@font'}, editable=True)
mod_combo.currentIndexChanged.connect(lambda mod: self.modifier_callback(mod, 4))
self._mod_combos[4] = mod_combo
mod_layout.addWidget(mod_combo, 2, 0, 1, 2)
layout.addLayout(mod_layout)
control_layout = HBoxLayout(spacing=csp)
- cancel_button = create_button(sets, 'Cancel')
+ cancel_button = create_button2(theme, 'Cancel')
cancel_button.setSizePolicy(SMINMAX)
cancel_button.clicked.connect(self.reject)
control_layout.addWidget(cancel_button)
- save_button = create_button(sets, 'Save')
+ save_button = create_button2(theme, 'Save')
save_button.clicked.connect(self.accept)
save_button.setSizePolicy(SMINMAX)
control_layout.addWidget(save_button)
layout.addLayout(control_layout)
- content_frame = create_frame(sets, size_policy=SMINMIN)
+ content_frame = create_frame2(theme, size_policy=SMINMIN)
content_frame.setLayout(layout)
- margin = sets.theme['defaults']['isp'] * ui_scale
+ margin = theme['defaults']['isp'] * ui_scale
main_layout = VBoxLayout(margins=margin)
main_layout.addWidget(content_frame)
self.setLayout(main_layout)
- def edit_item(self, item: dict, modifiers: dict):
+ def edit_item(self, item: dict[str], modifiers: dict[str, dict[str]], slot: ItemSlot):
"""
- Executes editor, returns edited item. Returns None when editor is closed without saving.
+ Shows editor window. Returns immediately.
+
+ Parameters:
+ - :param item: item data for the item that should be edited
+ - :param modifiers: collection of available modifiers
+ - :param slot: information about the slot
"""
- self._result = None
+ self._slot = slot
self.insert_modifiers(modifiers)
self._mark_combo.setCurrentText(item['mark'])
self._rarity_combo.setCurrentText(item['rarity'])
@@ -476,17 +507,31 @@ def edit_item(self, item: dict, modifiers: dict):
'mark': item['mark'],
'modifiers': [mod for mod in item['modifiers']]
}
- action = self.exec()
+ self.open()
+
+ @Slot(int)
+ def finish_edit(self, action: int):
+ """
+ Completes the edit action, resets the dialog and emits the data using the `dialog_result`
+ signal.
+
+ Parameters:
+ - :param action: indicates whether the result should be saved (`1`) or not (`0`)
+ """
+ slot = self._slot
if action == 1:
- self._result = {
+ edited_item = {
'item': self._item['item'],
'rarity': self._item['rarity'],
'mark': self._item['mark'],
'modifiers': [mod for mod in self._item['modifiers']]
}
+ else:
+ edited_item = self.empty_item
self._mark_combo.setCurrentText('')
self._rarity_combo.setCurrentText('Common')
for mod_combo in self._mod_combos:
mod_combo.setCurrentText('')
self._item = self.empty_item
- return self._result
+ self._slot = None
+ self.dialog_result.emit(edited_item, slot)
diff --git a/src/widgetbuilder.py b/src/widgetbuilder.py
index 0d4d4d2..c461a26 100644
--- a/src/widgetbuilder.py
+++ b/src/widgetbuilder.py
@@ -1,6 +1,7 @@
from typing import Callable
from PySide6.QtCore import Qt
+from PySide6.QtGui import QValidator
from PySide6.QtWidgets import (
QCheckBox, QComboBox, QCompleter, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QSizePolicy, QSlider, QVBoxLayout)
@@ -131,6 +132,124 @@ def create_button_series2(
return layout
+def create_button2(
+ theme: AppTheme, text: str, style: str = 'button', style_override: dict = {},
+ toggle: bool = None):
+ """
+ Creates a button according to style with parent.
+
+ Parameters:
+ - :param theme: reference to AppTheme
+ - :param text: text to be shown on the button
+ - :param style: name of the style as in self.theme or style dict
+ - :param style_override: style dict to override default style (optional)
+ - :param toggle: True or False when button should be a toggle button, None when it should be a \
+ normal button; the bool value indicates the default state of the button
+
+ :return: configured QPushButton
+ """
+ button = QPushButton(text)
+ button.setStyleSheet(theme.get_style_class('QPushButton', style, style_override))
+ if 'font' in style_override:
+ button.setFont(theme.get_font(style, style_override['font']))
+ else:
+ button.setFont(theme.get_font(style))
+ button.setCursor(Qt.CursorShape.PointingHandCursor)
+ button.setSizePolicy(SMAXMAX)
+ if isinstance(toggle, bool):
+ button.setCheckable(True)
+ button.setChecked(toggle)
+ return button
+
+
+def create_item_button2(theme: AppTheme) -> ItemButton:
+ """
+ Creates Item Button.
+
+ Parameters:
+ - :param theme: reference to AppTheme
+ """
+ label = create_label2(theme, '', 'infobox')
+ frame = create_frame2(theme, 'infobox_frame')
+ margin = theme['defaults']['csp'] * theme.scale
+ layout = VBoxLayout(margin)
+ layout.addWidget(label, alignment=ATOP)
+ frame.setLayout(layout)
+ button = ItemButton(
+ theme.opt.box_width, theme.opt.box_height, theme['item'], label, frame,
+ margin + theme['defaults']['bw'] * theme.scale)
+ return button
+
+
+def create_combo_box2(
+ theme: AppTheme, style: str = 'combobox', editable: bool = False,
+ size_policy: QSizePolicy = None, style_override: dict[str] = {},
+ class_: type[QComboBox] = QComboBox) -> QComboBox:
+ """
+ Creates a combobox with given style and returns it.
+
+ Parameters:
+ - :param theme: reference to AppTheme
+ - :param style: key for self.theme -> default style
+ - :param editable: set to True to make combobox editable
+ - :param size_policy: size policy for combobox
+ - :param style_override: style dict to override default style
+ - :param class_: custom constructor for combobox; must be QCombobox or subclass
+
+ :return: styled QCombobox
+ """
+ combo_box = class_()
+ combo_box.setStyleSheet(theme.get_style_class('QComboBox', style, style_override))
+ if 'font' in style_override:
+ font = theme.get_font(style, style_override['font'])
+ else:
+ font = theme.get_font(style)
+ combo_box.setFont(font)
+ combo_box.setSizePolicy(SMINMAX if size_policy is None else size_policy)
+ combo_box.setCursor(Qt.CursorShape.PointingHandCursor)
+ combo_box.view().setCursor(Qt.CursorShape.PointingHandCursor)
+ combo_box.setMinimumContentsLength(1)
+ combo_box.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents)
+ if editable:
+ combo_box.setEditable(True)
+ combo_box.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
+ combo_box.completer().setFilterMode(Qt.MatchFlag.MatchContains)
+ combo_box.completer().setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
+ combo_box.completer().popup().setStyleSheet(theme.get_style_class('QListView', 'popup'))
+ combo_box.completer().popup().setFont(font)
+ combo_box.lineEdit().setFont(font)
+ return combo_box
+
+
+def create_entry2(
+ theme: AppTheme, default_value='', validator: QValidator | None = None,
+ style: str = 'entry', style_override: dict = {}, placeholder: str = '') -> QLineEdit:
+ """
+ Creates an entry widget and styles it.
+
+ Parameters:
+ - :param theme: reference to AppTheme
+ - :param default_value: default value for the entry
+ - :param validator: validator to validate entered characters against
+ - :param style: key for self.theme -> default style
+ - :param style_override: style dict to override default style
+ - :param placeholder: placeholder shown when entry is empty
+
+ :return: styled QLineEdit
+ """
+ entry = QLineEdit(default_value)
+ entry.setValidator(validator)
+ entry.setPlaceholderText(placeholder)
+ entry.setStyleSheet(theme.get_style_class('QLineEdit', style, style_override))
+ if 'font' in style_override:
+ entry.setFont(theme.get_font(style, style_override['font']))
+ else:
+ entry.setFont(theme.get_font(style))
+ entry.setCursor(Qt.CursorShape.IBeamCursor)
+ entry.setSizePolicy(SMAXMAX)
+ return entry
+
+
def create_frame(self, style='frame', style_override={}, size_policy=None) -> QFrame:
"""
Creates a frame with default styling and parent
diff --git a/src/widgets.py b/src/widgets.py
index 7bb4992..8fa6dfb 100644
--- a/src/widgets.py
+++ b/src/widgets.py
@@ -478,7 +478,7 @@ def __init__(self, target: Callable, args: tuple = (), kwargs: dict[str] = {}):
self._target: Callable = target
self._args: tuple = args
self._kwargs: dict[str] = kwargs
-
+
def set_args(self, new_args: tuple) -> bool:
"""
Sets new arguments that should be passed to the target. Only works while thread is not
@@ -595,7 +595,7 @@ def mousePressEvent(self, ev: QMouseEvent):
TagStyles = namedtuple('TagStyles', ('ul', 'li', 'indent'))
-ItemSlot = namedtuple('ItemSlot', ('type', 'index', 'environment'))
+ItemSlot = namedtuple('ItemSlot', ('environment', 'type', 'index', 'boff_id', 'is_equipment'))
class ContextMenu(QMenu):
From 4f36c60b9a3fb21e2d10850a0417ea4aa615f1c1 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Mon, 11 May 2026 11:57:19 +0200
Subject: [PATCH 11/44] Refactoring context menu
---
src/buildmanager.py | 35 +++++++++++-
src/callbacks.py | 89 ------------------------------
src/contextmenu.py | 131 ++++++++++++++++++++++++++++++++++++++++++++
src/picker.py | 1 +
src/widgets.py | 37 -------------
5 files changed, 166 insertions(+), 127 deletions(-)
create mode 100644 src/contextmenu.py
diff --git a/src/buildmanager.py b/src/buildmanager.py
index 311350d..6ea0b2c 100644
--- a/src/buildmanager.py
+++ b/src/buildmanager.py
@@ -10,7 +10,7 @@
from .iofunc import store_json__new
from .textedit import add_equipment_tooltip_header__new, get_ultimate_skill_unlock_tooltip__new
from .theme import TooltipCSS
-from .widgets import ItemButton, ShipButton, ShipImage, Thread, TooltipLabel
+from .widgets import ItemButton, ItemSlot, ShipButton, ShipImage, Thread, TooltipLabel
class SpaceBuild():
@@ -519,6 +519,39 @@ def slot_trait_item(
tooltip = self._cache.ground_traits[build_key][item_name]['tooltip']
item_button.set_item_full(item_image, None, tooltip)
+ def unslot_item(self, environment: str, build_key: str, build_subkey: int, boff_id: int = -1):
+ """
+ Updates build and UI with item
+
+ Parameters:
+ - :param item: item to be slotted
+ - :param environment: space/ground
+ - :param build_key: key to self.build[environment]
+ - :param build_subkey: index of the item within its build_key (category)
+ - :param boff_id: id of the boff seat; assumes non-boff item when `-1` or not supplied
+ """
+ if boff_id == -1:
+ self._build_data[environment][build_key][build_subkey] = ''
+ item_button: ItemButton = getattr(getattr(self, environment), build_key)[build_subkey]
+ item_button.clear()
+ else:
+ self._build_data[environment][build_key][boff_id][build_subkey] = ''
+ item_button: ItemButton = getattr(
+ getattr(self, environment), build_key)[boff_id][build_subkey]
+ item_button.clear()
+
+ def finish_item_edit(self, new_item: dict[str], slot: ItemSlot):
+ """
+ Updates item after editing if editing was not cancelled. Autosaves.
+
+ Parameters:
+ - :param new_item: contains the new item
+ - :param slot: information about the slot
+ """
+ if new_item['item'] != '':
+ self.slot_equipment_item(new_item, slot.environment, slot.type, slot.index)
+ self.autosave()
+
def load_boff_stations(self, environment: str):
"""
Updates boff stations to show items from build
diff --git a/src/callbacks.py b/src/callbacks.py
index d926e6c..c5890e9 100644
--- a/src/callbacks.py
+++ b/src/callbacks.py
@@ -531,95 +531,6 @@ def ship_info_callback(self):
open_wiki_page(self.cache.ships[self.build['space']['ship']]['Page'])
-def open_wiki_context(self):
- """
- Opens wiki page of item in `self.context_menu.clicked_slot`.
- """
- slot = self.context_menu.clicked_slot
- if self.context_menu.clicked_boff_station != -1:
- boff_id = self.context_menu.clicked_boff_station
- item = self.build[slot.environment][slot.type][boff_id][slot.index]
- if item is not None and item != '':
- open_wiki_page(f"{item['item']}_(ability)")
- return
- item = self.build[slot.environment][slot.type][slot.index]
- if item is None or item == '':
- return
- if slot.type == 'starship_traits':
- open_wiki_page(f"{item['item']}_(starship_trait)")
- elif 'traits' in slot.type:
- open_wiki_page(f"{item['item']}_({slot.environment}_trait)")
- else:
- open_wiki_page(f"{self.cache.equipment[slot.type][item['item']]['Page']}#{item['item']}")
-
-
-def copy_equipment_item(self):
- """
- Copies equipment item clicked on.
- """
- slot = self.context_menu.clicked_slot
- item = self.build[slot.environment][slot.type][slot.index]
- if item is None or item == '':
- self.context_menu.copied_item = None
- self.context_menu.copied_item_type = None
- else:
- self.context_menu.copied_item = item
- item_type = EQUIPMENT_TYPES[self.cache.equipment[slot.type][item['item']]['type']]
- self.context_menu.copied_item_type = item_type
-
-
-def paste_equipment_item(self):
- """
- Pastes copied item into clicked slot if slot types are compatible
- """
- slot = self.context_menu.clicked_slot
- copied_type = self.context_menu.copied_item_type
- if slot.type == copied_type:
- slot_equipment_item(
- self, self.context_menu.copied_item, slot.environment, slot.type, slot.index)
- elif copied_type == 'ship_weapon' and (
- slot.type == 'fore_weapons' or slot.type == 'aft_weapons'):
- slot_equipment_item(
- self, self.context_menu.copied_item, slot.environment, slot.type, slot.index)
- elif (copied_type == 'uni_consoles' and 'consoles' in slot.type
- or slot.type == 'uni_consoles' and 'consoles' in copied_type):
- slot_equipment_item(
- self, self.context_menu.copied_item, slot.environment, slot.type, slot.index)
- self.autosave()
-
-
-def clear_slot(self):
- """
- Clears slot that was rightclicked on.
- """
- slot = self.context_menu.clicked_slot
- if self.context_menu.clicked_boff_station == -1:
- self.widgets.build[slot.environment][slot.type][slot.index].clear()
- self.build[slot.environment][slot.type][slot.index] = ''
- else:
- boff_id = self.context_menu.clicked_boff_station
- self.widgets.build[slot.environment][slot.type][boff_id][slot.index].clear()
- self.build[slot.environment][slot.type][boff_id][slot.index] = ''
- self.autosave()
-
-
-def edit_equipment_item(self):
- """
- Edit mark, modifiers and rarity of rightclicked item.
- """
- slot = self.context_menu.clicked_slot
- item = self.build[slot.environment][slot.type][slot.index]
- if slot.type == 'fore_weapons' or slot.type == 'aft_weapons':
- item_type = slot.type
- else:
- item_type = EQUIPMENT_TYPES[self.cache.equipment[slot.type][item['item']]['type']]
- modifiers = self.cache.modifiers[item_type]
- new_item = self.edit_window.edit_item(item, modifiers)
- if new_item is not None:
- slot_equipment_item(self, new_item, slot.environment, slot.type, slot.index)
- self.autosave()
-
-
def doff_spec_callback(self, new_spec: str, environment: str, doff_id: int):
"""
Callback for duty officer specialization combobox.
diff --git a/src/contextmenu.py b/src/contextmenu.py
new file mode 100644
index 0000000..ed57b33
--- /dev/null
+++ b/src/contextmenu.py
@@ -0,0 +1,131 @@
+from PySide6.QtCore import Signal
+from PySide6.QtGui import QMouseEvent
+from PySide6.QtWidgets import QMenu
+
+from .cargomanager import CargoManager
+from .constants import EQUIPMENT_TYPES
+from .buildmanager import BuildManager
+from .iofunc import open_wiki_page
+from .theme import AppTheme
+from .widgets import ItemSlot
+
+
+class ContextMenu(QMenu):
+ """
+ Custom context menu with data storage
+ """
+
+ edit_slot: Signal = Signal(dict, dict, ItemSlot)
+
+ def __init__(self, theme: AppTheme, build: BuildManager, cargo: CargoManager):
+ super().__init__()
+ self._build: BuildManager = build
+ self._cargo: CargoManager = cargo
+ self.clicked_slot: ItemSlot | None = None
+ self.clicked_modifiers: dict = {}
+ self.copied_item: dict = None
+ self.copied_item_type: str = None
+
+ self.setStyleSheet(theme.get_style_class('ContextMenu', 'context_menu'))
+ self.setFont(theme.get_font('context_menu'))
+ self.addAction(theme.icons['copy'], 'Copy Item', self.copy_equipment_item)
+ self.addAction(theme.icons['paste'], 'Paste Item', self.paste_equipment_item)
+ self.addAction(theme.icons['clear'], 'Clear Slot', self.clear_slot)
+ self.addAction(theme.icons['link'], 'Open Wiki', self.open_wiki)
+ self.addAction(theme.icons['edit'], 'Edit Slot', self.edit_equipment_item)
+
+ def invoke(self, event: QMouseEvent, key: str, subkey: int, environment: str, boff: int = -1):
+ """
+ Opens context menu for equipment
+
+ Parameters:
+ - :param event: event containing the clicked point
+ - :param key: slot type in self.build[environment]
+ - :param subkey: slot index
+ - :param environment: "space" / "ground"
+ - :param boff: id of the boff station
+ """
+ actions = self.actions()
+ if key in {'boffs', 'rep_traits', 'starship_traits', 'traits', 'active_rep_traits'}:
+ actions[0].setEnabled(False)
+ actions[1].setEnabled(False)
+ actions[4].setEnabled(False)
+ is_equipment = False
+ else:
+ actions[0].setEnabled(True)
+ actions[1].setEnabled(True)
+ actions[4].setEnabled(True)
+ is_equipment = True
+ self.clicked_slot = ItemSlot(environment, key, subkey, boff, is_equipment)
+ self.popup(event.globalPos())
+
+ def copy_equipment_item(self):
+ """
+ Copies equipment item clicked on.
+ """
+ slot = self.clicked_slot
+ item = self._build[slot.environment][slot.type][slot.index]
+ if item is None or item == '':
+ self.copied_item = None
+ self.copied_item_type = None
+ else:
+ # TODO check if dict must be deep-copied
+ self.copied_item = item
+ item_type = EQUIPMENT_TYPES[self._cargo.equipment[slot.type][item['item']]['type']]
+ self.copied_item_type = item_type
+
+ def paste_equipment_item(self):
+ """
+ Pastes copied item into clicked slot if slot types are compatible
+ """
+ slot = self.clicked_slot
+ if (self.copied_item_type == slot.type
+ or self.copied_item_type == 'ship_weapon' and (
+ slot.type == 'fore_weapons' or slot.type == 'aft_weapons')
+ or self.copied_item_type == 'uni_consoles' and 'consoles' in slot.type
+ or slot.type == 'uni_consoles' and 'consoles' in self.copied_item_type):
+ self._build.slot_equipment_item(
+ self.copied_item, slot.environment, slot.type, slot.index)
+ self._build.autosave()
+
+ def edit_equipment_item(self):
+ """
+ Edit mark, modifiers and rarity of rightclicked item.
+ """
+ slot = self.clicked_slot
+ item = self._build[slot.environment][slot.type][slot.index]
+ if slot.type == 'fore_weapons' or slot.type == 'aft_weapons':
+ item_type = slot.type
+ else:
+ item_type = EQUIPMENT_TYPES[self._cargo.equipment[slot.type][item['item']]['type']]
+ modifiers = self._cargo.modifiers[item_type]
+ self.edit_slot.emit(item, modifiers, slot)
+
+ def clear_slot(self):
+ """
+ Clears slot that was rightclicked on.
+ """
+ slot = self.clicked_slot
+ self._build.unslot_item(slot.environment, slot.type, slot.index, slot.boff_id)
+ self._build.autosave()
+
+ def open_wiki(self):
+ """
+ Opens wiki page of item that was rightclicked on.
+ """
+ slot = self.clicked_slot
+ if slot.boff_id != -1:
+ item = self._build[slot.environment][slot.type][slot.boff_id][slot.index]
+ if item is not None and item != '':
+ open_wiki_page(f"{item['item']}_(ability)")
+ return
+ item = self._build[slot.environment][slot.type][slot.index]
+ if item is None or item == '':
+ return
+ if slot.type == 'starship_traits':
+ open_wiki_page(f"{item['item']}_(starship_trait)")
+ elif 'traits' in slot.type:
+ open_wiki_page(f"{item['item']}_({slot.environment}_trait)")
+ else:
+ open_wiki_page(
+ f"{self._cargo.equipment[slot.type][item['item']]['Page']}#{item['item']}")
diff --git a/src/picker.py b/src/picker.py
index 627b73f..6bc3b4c 100644
--- a/src/picker.py
+++ b/src/picker.py
@@ -486,6 +486,7 @@ def __init__(self, theme: AppTheme, parent_window: QWidget, style: str = 'picker
main_layout.addWidget(content_frame)
self.setLayout(main_layout)
+ @Slot(dict, dict, ItemSlot)
def edit_item(self, item: dict[str], modifiers: dict[str, dict[str]], slot: ItemSlot):
"""
Shows editor window. Returns immediately.
diff --git a/src/widgets.py b/src/widgets.py
index 8fa6dfb..f6ea85c 100644
--- a/src/widgets.py
+++ b/src/widgets.py
@@ -598,43 +598,6 @@ def mousePressEvent(self, ev: QMouseEvent):
ItemSlot = namedtuple('ItemSlot', ('environment', 'type', 'index', 'boff_id', 'is_equipment'))
-class ContextMenu(QMenu):
- """
- Custom context menu with data storage
- """
- def __init__(self):
- super().__init__()
- self.clicked_slot: ItemSlot = None
- self.clicked_boff_station: int = -1
- self.clicked_modifiers: dict = {}
- self.copied_item: dict = None
- self.copied_item_type: str = None
-
- def invoke(self, event: QMouseEvent, key: str, subkey: int, environment: str, boff: int = -1):
- """
- Opens context menu for equipment
-
- Parameters:
- - :param event: event containing the clicked point
- - :param key: slot type in self.build[environment]
- - :param subkey: slot index
- - :param environment: "space" / "ground"
- - :param boff: id of the boff station
- """
- self.clicked_slot = ItemSlot(key, subkey, environment)
- self.clicked_boff_station = boff
- actions = self.actions()
- if key in {'boffs', 'rep_traits', 'starship_traits', 'traits', 'active_rep_traits'}:
- actions[0].setEnabled(False)
- actions[1].setEnabled(False)
- actions[4].setEnabled(False)
- else:
- actions[0].setEnabled(True)
- actions[1].setEnabled(True)
- actions[4].setEnabled(True)
- self.exec(event.globalPos())
-
-
class DoffCombobox(QComboBox):
def minimumSizeHint(self) -> QSize:
return QSize(100, super().minimumSizeHint().height())
From 66a892425b7ca587881f0a55f5e10696814f9bb8 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Mon, 11 May 2026 12:24:22 +0200
Subject: [PATCH 12/44] updating app initialization
---
src/app.py | 110 ++++++++++++++++++++------------------------------
src/iofunc.py | 9 +++--
2 files changed, 50 insertions(+), 69 deletions(-)
diff --git a/src/app.py b/src/app.py
index b4d0369..ff80028 100644
--- a/src/app.py
+++ b/src/app.py
@@ -2,25 +2,27 @@
from pathlib import Path
from PySide6.QtCore import QDir, Qt, QThread
-from PySide6.QtGui import QFontDatabase, QTextOption
+from PySide6.QtGui import QCloseEvent, QFontDatabase, QTextOption
from PySide6.QtWidgets import QApplication, QFrame, QPlainTextEdit, QScrollArea, QTabWidget, QWidget
+from .buildmanager import BuildManager
from .cargomanager import CargoManager
from .config import SETSConfig, SETSSettings
from .constants import (
- ABOTTOM, ACENTER, AHCENTER, ALEFT, ARIGHT, ATOP, AVCENTER, CAREERS, FACTIONS, MARKS,
+ ABOTTOM, AHCENTER, ALEFT, ARIGHT, ATOP, AVCENTER, CAREERS, FACTIONS, MARKS,
PRIMARY_SPECS, RARITIES, SCROLLOFF, SCROLLON, SECONDARY_SPECS, SMAXMAX, SMAXMIN, SMINMAX,
SMINMIN)
+from .contextmenu import ContextMenu
from .datafunctions import cache_skills
from .downloader import Downloader
+from .exportwindow import ExportWindow
from .imagemanager import ImageManager
from .iofunc import (
- create_folder, delete_folder_contents, get_asset_path, load_icon, load_json, open_url,
- store_json)
-from .subwindows import ExportWindow, ItemEditor, Picker, ShipSelector
+ delete_folder_contents, get_asset_path, load_icon, open_url, store_json)
+from .picker import ItemEditor, Picker, ShipSelector
from .theme import AppTheme
from .widgets import (
- Cache, ContextMenu, GridLayout, HBoxLayout, ImageLabel, ShipButton, ShipImage, TooltipLabel,
+ Cache, GridLayout, HBoxLayout, ImageLabel, ShipButton, ShipImage, TooltipLabel,
VBoxLayout, WidgetStorage)
# only for developing; allows to terminate the qt event loop with keyboard interrupt
@@ -31,16 +33,14 @@
class SETS():
from .callbacks import (
- clear_all, clear_slot, clear_build_callback, copy_equipment_item, edit_equipment_item,
- elite_callback, faction_combo_callback, load_build_callback, load_skills_callback,
- open_wiki_context, paste_equipment_item, save_build_callback, save_skills_callback,
+ clear_all, clear_build_callback, elite_callback, faction_combo_callback,
+ load_build_callback, load_skills_callback, save_build_callback, save_skills_callback,
select_ship, set_build_item, ship_info_callback,
skill_unlock_callback, spec_combo_callback, species_combo_callback, switch_main_tab,
tier_callback)
from .datafunctions import (
autosave, backup_cargo_data, empty_build,
init_backend, load_legacy_build_image)
- from .export import get_build_markdown
from .splash import enter_splash, exit_splash, splash_text
from .style import (
create_style_sheet, get_style, get_style_class, prepare_tooltip_css, theme_font)
@@ -87,7 +87,7 @@ def __init__(self, theme, args, path, config, versions):
self.theme = theme
self.args = args
self.app_dir = path
- self.config = config
+ self.app_dir2: Path = Path(path)
self.widgets = WidgetStorage()
self.cache = Cache()
self.config: SETSConfig = SETSConfig()
@@ -101,25 +101,27 @@ def __init__(self, theme, args, path, config, versions):
self.downloader = Downloader(
self.config.config_subfolders['images'],
self.config.config_subfolders['ship_images'])
- self.cargo: CargoManager = CargoManager(self.config.config_subfolders)
+ self.cargo: CargoManager = CargoManager(
+ self.config.config_subfolders, self.app_dir2, self.downloader, self.settings,
+ self.theme2)
self.images: ImageManager = ImageManager(
Path(self.config.config_subfolders['images']),
Path(self.config.config_subfolders['ship_images']),
- self.cargo, self.downloader)
+ self.app_dir2, self.cargo, self.downloader)
+ self.build2: BuildManager = BuildManager(
+ self.cargo, self.images, self.config.autosave_path, self.theme2.tooltips)
self.app, self.window = self.create_main_window()
self.cache_icons()
- self.cache_item_aliases()
self.building = True
self.build = self.empty_build()
- self.export_window = ExportWindow(self, self.window, self.get_build_markdown)
+ self.export_window = ExportWindow(self.theme2, self.window, self.build2, self.cargo)
self.setup_main_layout()
- self.picker_window = Picker(
- self, self.window,
- default_rarity_getter=lambda: self.settings.default_rarity,
- default_mark_getter=lambda: self.settings.default_mark)
- self.edit_window = ItemEditor(self, self.window)
- self.ship_selector_window = ShipSelector(self, self.window)
- self.context_menu = self.create_context_menu()
+ self.picker_window: Picker = Picker(self.theme2, self.window, self.settings, self.images)
+ self.edit_window: ItemEditor = ItemEditor(self.theme2, self.window)
+ self.edit_window.dialog_result.connect(self.build2.finish_item_edit)
+ self.ship_selector_window: ShipSelector = ShipSelector(self.theme2, self.window)
+ self.context_menu: ContextMenu = ContextMenu(self.theme2, self.build2, self.cargo)
+ self.context_menu.edit_slot.connect(self.edit_window.edit_item)
self.window.show()
self.init_backend()
@@ -200,33 +202,25 @@ def cache_icons(self):
"""
Loads static icons.
"""
- self.cache.icons['copy'] = load_icon('copy.png', self.app_dir)
- self.cache.icons['paste'] = load_icon('paste.png', self.app_dir)
- self.cache.icons['clear'] = load_icon('clear.png', self.app_dir)
- self.cache.icons['edit'] = load_icon('edit.png', self.app_dir)
- self.cache.icons['link'] = load_icon('external_link.png', self.app_dir)
- self.cache.icons['dual_cannons'] = load_icon('DC_icon.svg', self.app_dir).pixmap(16, 24.5)
- self.cache.icons['ground'] = load_icon('ground_icon.png', self.app_dir).pixmap(
- self.theme2.opt.box_width * 1.2, self.theme2.opt.box_width * 1.2)
- self.cache.icons['tac'] = load_icon('tac_icon.png', self.app_dir).pixmap(
- self.theme2.opt.box_width, self.theme2.opt.box_width)
- self.cache.icons['tac-small'] = load_icon('tac-small.svg', self.app_dir).pixmap(25, 25)
- self.cache.icons['sci'] = load_icon('sci_icon.png', self.app_dir).pixmap(
- self.theme2.opt.box_width, self.theme2.opt.box_width)
- self.cache.icons['sci-small'] = load_icon('sci-small.svg', self.app_dir).pixmap(25, 25)
- self.cache.icons['eng'] = load_icon('eng_icon.png', self.app_dir).pixmap(
- self.theme2.opt.box_width, self.theme2.opt.box_width)
- self.cache.icons['STOCD'] = load_icon('stocd.png', self.app_dir).pixmap(
- self.box_height, self.box_height * 182 / 106)
+ self.cache.icons['copy'] = load_icon('copy.png', self.app_dir2)
+ self.cache.icons['paste'] = load_icon('paste.png', self.app_dir2)
+ self.cache.icons['clear'] = load_icon('clear.png', self.app_dir2)
+ self.cache.icons['edit'] = load_icon('edit.png', self.app_dir2)
+ self.cache.icons['link'] = load_icon('external_link.png', self.app_dir2)
+ self.cache.icons['dual_cannons'] = load_icon('DC_icon.svg', self.app_dir2, size=(16, 24.5))
+ icon_size = (self.theme2.opt.box_width * 1.2, self.theme2.opt.box_width * 1.2)
+ self.cache.icons['ground'] = load_icon('ground_icon.png', self.app_dir2, icon_size)
+ icon_size = (self.theme2.opt.box_width, self.theme2.opt.box_width)
+ self.cache.icons['tac'] = load_icon('tac_icon.png', self.app_dir2, icon_size)
+ self.cache.icons['sci'] = load_icon('sci_icon.png', icon_size)
+ self.cache.icons['eng'] = load_icon('eng_icon.png', icon_size)
+ self.cache.icons['tac-small'] = load_icon('tac-small.svg', self.app_dir2, size=(25, 25))
+ self.cache.icons['sci-small'] = load_icon('sci-small.svg', self.app_dir2, size=(25, 25))
+ icon_size = (self.theme2.opt.box_height, self.theme2.opt.box_width * 182 / 106)
+ self.cache.icons['STOCD'] = load_icon('stocd.png', self.app_dir2, icon_size)
self.theme2.icons = self.cache.icons
- def cache_item_aliases(self):
- """
- Loads item aliases into cache (used for fixing renamed items).
- """
- self.cache.item_aliases = load_json(get_asset_path('aliases.json', self.app_dir))
-
- def main_window_close_callback(self, event):
+ def main_window_close_callback(self, event: QCloseEvent):
"""
Executed when application is closed.
"""
@@ -248,13 +242,11 @@ def create_main_window(self, argv=[]) -> tuple[QApplication, QWidget]:
"""
app = QApplication(argv)
font_database = QFontDatabase()
- font_database.addApplicationFont(
- get_asset_path('Overpass-VariableFont_wght.ttf', self.app_dir))
- font_database.addApplicationFont(
- get_asset_path('RobotoMono-Regular.ttf', self.app_dir))
+ font_database.addApplicationFont(self.app_dir2 / 'local' / 'Overpass-VariableFont_wght.ttf')
+ font_database.addApplicationFont(self.app_dir2 / 'local' / 'RobotoMono-Regular.ttf')
app.setStyleSheet(self.theme2.create_style_sheet(self.theme2['app']['style']))
window = QWidget()
- window.setWindowIcon(load_icon('SETS_icon_small.png', self.app_dir))
+ window.setWindowIcon(load_icon('SETS_icon_small.png', self.app_dir2))
window.setWindowTitle('STO Equipment and Trait Selector')
if self.settings.state__geometry:
window.restoreGeometry(self.settings.state__geometry)
@@ -989,20 +981,6 @@ def setup_splash(self, frame: QFrame):
layout.addWidget(loading_label, 2, 0, 1, 3, alignment=AHCENTER)
frame.setLayout(layout)
- def create_context_menu(self) -> ContextMenu:
- """
- Creates context menu for rightclick operations on equipment items
- """
- menu = ContextMenu()
- menu.setStyleSheet(self.get_style_class('ContextMenu', 'context_menu'))
- menu.setFont(self.theme2.get_font('context_menu'))
- menu.addAction(self.cache.icons['copy'], 'Copy Item', self.copy_equipment_item)
- menu.addAction(self.cache.icons['paste'], 'Paste Item', self.paste_equipment_item)
- menu.addAction(self.cache.icons['clear'], 'Clear Slot', self.clear_slot)
- menu.addAction(self.cache.icons['link'], 'Open Wiki', self.open_wiki_context)
- menu.addAction(self.cache.icons['edit'], 'Edit Slot', self.edit_equipment_item)
- return menu
-
def hide_tooltips(self):
"""
Hides tooltip windows when main window isn't the active window anymore.
diff --git a/src/iofunc.py b/src/iofunc.py
index 7659c3a..1637b4a 100644
--- a/src/iofunc.py
+++ b/src/iofunc.py
@@ -9,7 +9,7 @@
from urllib.parse import quote_plus, unquote_plus
from webbrowser import open as webbrowser_open
-from PySide6.QtGui import QIcon, QImage
+from PySide6.QtGui import QIcon, QImage, QPixmap
from PySide6.QtWidgets import QFileDialog
import requests
from requests.cookies import create_cookie as requests__create_cookie
@@ -325,7 +325,7 @@ def get_asset_path(asset_name: str, app_directory: str) -> str:
return ''
-def load_icon(filename: str, app_directory: str) -> QIcon:
+def load_icon(filename: str, app_directory: Path, size: tuple = tuple()) -> QIcon | QPixmap:
"""
Loads icon from path and returns it.
@@ -333,7 +333,10 @@ def load_icon(filename: str, app_directory: str) -> QIcon:
- :param path: path to icon
- :param app_directory: absolute path to the app directory
"""
- return QIcon(get_asset_path(filename, app_directory))
+ icon = QIcon(app_directory / 'local' / filename)
+ if len(size) == 2:
+ return icon.pixmap(*size)
+ return icon
def load_json__new(file_path: Path) -> dict | list | None:
From 0d16f7da034778351308fc494be09a6d8c141c1f Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Mon, 11 May 2026 16:54:12 +0200
Subject: [PATCH 13/44] Updating app ui creation and its helper functions
---
src/app.py | 878 +++++++++++++++++++++++++++++--------------
src/buildmanager.py | 22 +-
src/splash.py | 15 +
src/textedit.py | 24 +-
src/widgetbuilder.py | 632 ++-----------------------------
src/widgets.py | 28 +-
6 files changed, 708 insertions(+), 891 deletions(-)
diff --git a/src/app.py b/src/app.py
index ff80028..a2b1e54 100644
--- a/src/app.py
+++ b/src/app.py
@@ -3,31 +3,35 @@
from PySide6.QtCore import QDir, Qt, QThread
from PySide6.QtGui import QCloseEvent, QFontDatabase, QTextOption
-from PySide6.QtWidgets import QApplication, QFrame, QPlainTextEdit, QScrollArea, QTabWidget, QWidget
+from PySide6.QtWidgets import (
+ QApplication, QFrame, QPlainTextEdit, QPushButton, QScrollArea, QTabWidget, QWidget)
from .buildmanager import BuildManager
from .cargomanager import CargoManager
from .config import SETSConfig, SETSSettings
from .constants import (
- ABOTTOM, AHCENTER, ALEFT, ARIGHT, ATOP, AVCENTER, CAREERS, FACTIONS, MARKS,
+ ABOTTOM, AHCENTER, ALEFT, ARIGHT, ATOP, AVCENTER, CAREERS, FACTIONS, GROUND_BOFF_SPECS, MARKS,
PRIMARY_SPECS, RARITIES, SCROLLOFF, SCROLLON, SECONDARY_SPECS, SMAXMAX, SMAXMIN, SMINMAX,
SMINMIN)
from .contextmenu import ContextMenu
-from .datafunctions import cache_skills
from .downloader import Downloader
from .exportwindow import ExportWindow
from .imagemanager import ImageManager
-from .iofunc import (
- delete_folder_contents, get_asset_path, load_icon, open_url, store_json)
+from .iofunc import delete_folder_contents, load_icon, open_url, store_json
from .picker import ItemEditor, Picker, ShipSelector
+from .splash import SplashScreen
+from .textedit import format_skill_tooltip
from .theme import AppTheme
+from .widgetbuilder import (
+ create_annotated_slider2, create_button2, create_button_series2, create_checkbox2,
+ create_combo_box2, create_entry2, create_frame2, create_item_button2, create_label2)
from .widgets import (
- Cache, GridLayout, HBoxLayout, ImageLabel, ShipButton, ShipImage, TooltipLabel,
- VBoxLayout, WidgetStorage)
+ Cache, DoffCombobox, GridLayout, HBoxLayout, ImageLabel, ItemButton, ShipButton, ShipImage,
+ Tabbers, TooltipLabel, VBoxLayout, WidgetStorage)
# only for developing; allows to terminate the qt event loop with keyboard interrupt
-from signal import signal, SIGINT, SIG_DFL
-signal(SIGINT, SIG_DFL)
+# from signal import signal, SIGINT, SIG_DFL
+# signal(SIGINT, SIG_DFL)
class SETS():
@@ -42,15 +46,7 @@ class SETS():
autosave, backup_cargo_data, empty_build,
init_backend, load_legacy_build_image)
from .splash import enter_splash, exit_splash, splash_text
- from .style import (
- create_style_sheet, get_style, get_style_class, prepare_tooltip_css, theme_font)
- from .widgetbuilder import (
- create_annotated_slider, create_boff_station_ground, create_boff_station_space,
- create_bonus_bar_segment, create_bonus_bar_space, create_build_section, create_button,
- create_button_series, create_checkbox, create_combo_box, create_doff_section,
- create_entry, create_frame, create_item_button, create_label,
- create_personal_trait_section, create_skill_button_ground, create_skill_group_space,
- create_starship_trait_section)
+ from .style import prepare_tooltip_css
app_dir = None
# (release version, dev version)
@@ -110,12 +106,15 @@ def __init__(self, theme, args, path, config, versions):
self.app_dir2, self.cargo, self.downloader)
self.build2: BuildManager = BuildManager(
self.cargo, self.images, self.config.autosave_path, self.theme2.tooltips)
+ self.splash: SplashScreen = SplashScreen()
+ self.tabbers: Tabbers = Tabbers()
self.app, self.window = self.create_main_window()
self.cache_icons()
self.building = True
self.build = self.empty_build()
- self.export_window = ExportWindow(self.theme2, self.window, self.build2, self.cargo)
+ self.cargo.load_static_data()
self.setup_main_layout()
+ self.export_window = ExportWindow(self.theme2, self.window, self.build2, self.cargo)
self.picker_window: Picker = Picker(self.theme2, self.window, self.settings, self.images)
self.edit_window: ItemEditor = ItemEditor(self.theme2, self.window)
self.edit_window.dialog_result.connect(self.build2.finish_item_edit)
@@ -255,41 +254,57 @@ def create_main_window(self, argv=[]) -> tuple[QApplication, QWidget]:
QThread.currentThread().setPriority(QThread.Priority.TimeCriticalPriority)
return app, window
+ def picker(
+ self, environment: str, build_key: str, build_subkey: int, button,
+ equipment: bool = False, boff_id: int | None = None):
+ """
+ opens dialog to select item, stores it to build and updates item button
+
+ Parameters:
+ - :param items: iterable of items available to pick from
+ - :param environment: space or ground
+ - :param build_key: key to self.build[environment]; for storing picked item
+ - :param build_subkey: index of the item within its build_key (category)
+ - :param button: reference to the button clicked
+ - :param equipment: set to True to show rarity, mark, and modifier selector (optional)
+ - :param boff_id: id of the boff; only set when picking boff abilities! (optional)
+ """
+
def setup_main_layout(self):
"""
Creates the main layout and places it into the main window.
"""
# master layout: banner, borders and splash screen
- layout = VBoxLayout(margins=0, spacing=0)
- background_frame = self.create_frame(
- style_override={'background-color': '@sets'}, size_policy=SMINMIN)
+ layout = VBoxLayout()
+ background_frame = create_frame2(
+ self.theme2, style_override={'background-color': '@sets'}, size_policy=SMINMIN)
layout.addWidget(background_frame)
self.window.setLayout(layout)
- main_layout = VBoxLayout(margins=0, spacing=0)
- banner = ImageLabel(get_asset_path('sets_banner.png', self.app_dir), (2880, 126))
+ main_layout = VBoxLayout()
+ banner = ImageLabel(self.app_dir / 'local' / 'sets_banner.png', (2880, 126))
main_layout.addWidget(banner)
frame_width = 8 * self.theme2.scale
- tabber_layout = VBoxLayout(margins=frame_width, spacing=0)
+ tabber_layout = VBoxLayout(margins=frame_width)
splash_tabber = QTabWidget()
- splash_tabber.setStyleSheet(self.get_style_class('QTabWidget', 'tabber'))
- splash_tabber.tabBar().setStyleSheet(self.get_style_class('QTabBar', 'tabber_tab'))
+ splash_tabber.setStyleSheet(self.theme2.get_style_class('QTabWidget', 'tabber'))
+ splash_tabber.tabBar().setStyleSheet(self.theme2.get_style_class('QTabBar', 'tabber_tab'))
splash_tabber.setSizePolicy(SMINMIN)
- self.widgets.splash_tabber = splash_tabber
+ self.splash.tabber = splash_tabber
tabber_layout.addWidget(splash_tabber)
main_layout.addLayout(tabber_layout)
background_frame.setLayout(main_layout)
- content_frame = self.create_frame()
- splash_frame = self.create_frame()
+ content_frame = create_frame2(self.theme2)
+ splash_frame = create_frame2(self.theme2)
splash_tabber.addTab(content_frame, 'Main')
splash_tabber.addTab(splash_frame, 'Splash')
self.setup_splash(splash_frame)
- content_layout = GridLayout(margins=0, spacing=0)
+ content_layout = GridLayout()
content_layout.setColumnStretch(0, 1)
content_layout.setColumnStretch(1, 4)
margin = 3 * self.theme2.scale
- menu_layout = GridLayout(margins=(margin, margin, margin, 0), spacing=0)
+ menu_layout = GridLayout(margins=(margin, margin, margin, 0))
menu_layout.setColumnStretch(0, 2)
menu_layout.setColumnStretch(1, 5)
menu_layout.setColumnStretch(2, 2)
@@ -299,7 +314,8 @@ def setup_main_layout(self):
'Clear Current Tab': {'callback': self.clear_build_callback},
'Clear All Tabs': {'callback': self.clear_all}
}
- menu_layout.addLayout(self.create_button_series(left_button_group), 0, 0, ALEFT | ATOP)
+ menu_layout.addLayout(
+ create_button_series2(self.theme2, left_button_group), 0, 0, alignment=ALEFT | ATOP)
center_button_group = {
'default': {'font': ('Overpass', 16, 'medium')},
'SPACE': {'callback': lambda: self.switch_main_tab(0), 'stretch': 1, 'size': SMINMAX},
@@ -315,51 +331,50 @@ def setup_main_layout(self):
'size': SMINMAX
}
}
- center_buttons = self.create_button_series(center_button_group, 'heavy_button')
+ center_buttons = create_button_series2(self.theme2, center_button_group, 'heavy_button')
menu_layout.addLayout(center_buttons, 0, 1)
right_button_group = {
'Export': {'callback': self.export_window.invoke},
'Settings': {'callback': lambda: self.switch_main_tab(5)},
}
- menu_layout.addLayout(self.create_button_series(right_button_group), 0, 2, ARIGHT | ATOP)
+ menu_layout.addLayout(
+ create_button_series2(self.theme2, right_button_group), 0, 2, alignment=ARIGHT | ATOP)
content_layout.addLayout(menu_layout, 0, 0, 1, 2)
# sidebar
- sidebar = self.create_frame(size_policy=SMINMIN)
- self.widgets.sidebar = sidebar
- sidebar_layout = GridLayout(margins=0, spacing=0)
+ sidebar = create_frame2(self.theme2, size_policy=SMINMIN)
+ sidebar_layout = GridLayout()
sidebar_tabber = QTabWidget()
- sidebar_tabber.setStyleSheet(self.get_style_class('QTabWidget', 'tabber'))
- sidebar_tabber.tabBar().setStyleSheet(self.get_style_class('QTabBar', 'tabber_tab'))
+ sidebar_tabber.setStyleSheet(self.theme2.get_style_class('QTabWidget', 'tabber'))
+ sidebar_tabber.tabBar().setStyleSheet(self.theme2.get_style_class('QTabBar', 'tabber_tab'))
sidebar_tabber.setSizePolicy(SMINMIN)
- self.widgets.sidebar_tabber = sidebar_tabber
- sidebar_tab_names = (
- 'space', 'ground', 'space_skills', 'ground_skills', 'empty', 'settings')
- for tab_name in sidebar_tab_names:
- tab_frame = self.create_frame()
+ self.tabbers.sidebar_tabber = sidebar_tabber
+ for tab_name in ('space', 'ground', 'space_skills', 'ground_skills', 'empty', 'settings'):
+ tab_frame = create_frame2(self.theme2)
sidebar_tabber.addTab(tab_frame, tab_name)
- self.widgets.sidebar_frames.append(tab_frame)
+ self.tabbers.sidebar_frames.append(tab_frame)
self.setup_ship_frame()
sidebar_layout.addWidget(sidebar_tabber, 0, 0)
character_tabber = QTabWidget()
- character_tabber.setStyleSheet(self.get_style_class('QTabWidget', 'tabber'))
- character_tabber.tabBar().setStyleSheet(self.get_style_class('QTabBar', 'tabber_tab'))
+ character_tabber.setStyleSheet(self.theme2.get_style_class('QTabWidget', 'tabber'))
+ character_tabber.tabBar().setStyleSheet(
+ self.theme2.get_style_class('QTabBar', 'tabber_tab'))
character_tabber.setSizePolicy(SMINMAX)
- self.widgets.character_tabber = character_tabber
- char_frame = self.create_frame()
+ self.tabbers.character_tabber = character_tabber
+ char_frame = create_frame2(self.theme2)
self.setup_character_frame(char_frame)
character_tabber.addTab(char_frame, 'char')
- empty_frame = self.create_frame()
+ empty_frame = create_frame2(self.theme2)
character_tabber.addTab(empty_frame, 'empty')
- settings_frame = self.create_frame()
+ settings_frame = create_frame2(self.theme2)
character_tabber.addTab(settings_frame, 'settings')
- self.widgets.character_frames = [char_frame, empty_frame, settings_frame]
+ self.tabbers.character_frames = [char_frame, empty_frame, settings_frame]
sidebar_layout.addWidget(character_tabber, 1, 0)
- seperator = self.create_frame(size_policy=SMAXMIN, style_override={
- 'background-color': '@sets', 'margin-top': '@isp', 'margin-bottom': '@isp'})
+ seperator = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ 'background-color': '@sets', 'margin-top': '@isp', 'margin-bottom': '@isp'})
seperator.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
sidebar_layout.addWidget(seperator, 0, 1, 2, 1)
sidebar.setLayout(sidebar_layout)
@@ -367,19 +382,21 @@ def setup_main_layout(self):
# build section
build_tabber = QTabWidget()
- build_tabber.setStyleSheet(self.get_style_class('QTabWidget', 'tabber'))
- build_tabber.tabBar().setStyleSheet(self.get_style_class('QTabBar', 'tabber_tab'))
+ build_tabber.setStyleSheet(self.theme2.get_style_class('QTabWidget', 'tabber'))
+ build_tabber.tabBar().setStyleSheet(self.theme2.get_style_class('QTabBar', 'tabber_tab'))
build_tabber.setSizePolicy(SMINMIN)
- self.widgets.build_tabber = build_tabber
+ self.tabbers.build_tabber = build_tabber
build_tab_names = (
- 'space_build', 'ground_build', 'space_skills', 'ground_skills', 'library',
- 'settings')
+ 'space_build', 'ground_build', 'space_skills', 'ground_skills', 'library', 'settings')
for tab_name in build_tab_names:
- tab_frame = self.create_frame()
+ tab_frame = create_frame2(self.theme2)
build_tabber.addTab(tab_frame, tab_name)
- self.widgets.build_frames.append(tab_frame)
+ self.tabbers.build_frames.append(tab_frame)
content_layout.addWidget(build_tabber, 1, 1)
- self.setup_build_frames()
+ self.setup_space_build_frame()
+ self.setup_ground_build_frame()
+ self.setup_space_skill_frame()
+ self.setup_ground_skill_frame()
self.setup_settings_frame()
content_frame.setLayout(content_layout)
@@ -392,83 +409,394 @@ def setup_ship_frame(self):
csp = self.theme2['defaults']['csp'] * self.theme2.scale
layout = VBoxLayout(margins=csp, spacing=csp)
- image_frame = self.create_frame(size_policy=SMINMIN)
- image_layout = GridLayout(margins=0, spacing=0)
+ image_frame = create_frame2(self.theme2, size_policy=SMINMIN)
+ image_layout = GridLayout()
ship_image = ShipImage()
ship_image.setSizePolicy(SMINMIN)
- self.widgets.ship['image'] = ship_image
+ self.build2.ship.image = ship_image
image_layout.addWidget(ship_image, 0, 0)
image_frame.setLayout(image_layout)
layout.addWidget(image_frame, stretch=1)
- ship_frame = self.create_frame(size_policy=SMINMIN)
- ship_layout = GridLayout(margins=0, spacing=csp)
+ ship_frame = create_frame2(self.theme2, size_policy=SMINMIN)
+ ship_layout = GridLayout(spacing=csp)
ship_layout.setRowStretch(4, 1)
ship_layout.setColumnStretch(2, 1)
ship_selector = ShipButton('')
ship_selector.setSizePolicy(SMINMAX)
ship_selector.setStyleSheet(
- self.get_style_class('ShipButton', 'button', override={'margin': 0}))
+ self.theme2.get_style_class('ShipButton', 'button', override={'margin': 0}))
ship_selector.setFont(self.theme2.get_font(font_spec='@subhead'))
ship_selector.clicked.connect(self.select_ship)
- self.widgets.ship['button'] = ship_selector
+ self.build2.ship.button = ship_selector
ship_layout.addWidget(ship_selector, 0, 0, 1, 4, alignment=ATOP)
- tier_label = self.create_label('Ship Tier:')
+ tier_label = create_label2(self.theme2, 'Ship Tier:')
ship_layout.addWidget(tier_label, 1, 0)
- tier_combo = self.create_combo_box()
+ tier_combo = create_combo_box2(self.theme2)
tier_combo.currentTextChanged.connect(self.tier_callback)
tier_combo.setSizePolicy(SMAXMAX)
- self.widgets.ship['tier'] = tier_combo
+ self.build2.ship.tier = tier_combo
ship_layout.addWidget(tier_combo, 1, 1, alignment=ALEFT)
- dc_tooltip = self.create_label('Can equip Dual Cannons', 'label_tooltip')
+ dc_tooltip = create_label2(self.theme2, 'Can equip Dual Cannons', 'label_tooltip')
dc_label = TooltipLabel('', dc_tooltip)
- dc_label.setPixmap(self.cache.icons['dual_cannons'])
+ dc_label.setPixmap(self.theme2.icons['dual_cannons'])
dc_label_size_policy = dc_label.sizePolicy()
dc_label_size_policy.setRetainSizeWhenHidden(True)
dc_label.setSizePolicy(dc_label_size_policy)
- self.widgets.ship['dc'] = dc_label
+ self.build2.ship.dc = dc_label
ship_layout.addWidget(dc_label, 1, 2, alignment=ARIGHT)
info_button = self.create_button('Ship Info', style_override={'margin': 0})
info_button.clicked.connect(self.ship_info_callback)
ship_layout.addWidget(info_button, 1, 3, alignment=ARIGHT)
- name_label = self.create_label('Ship Name:')
+ name_label = create_label2(self.theme2, 'Ship Name:')
ship_layout.addWidget(name_label, 2, 0)
- name_entry = self.create_entry()
+ name_entry = create_entry2(self.theme2)
name_entry.editingFinished.connect(
- lambda: self.set_build_item(self.build['space'], 'ship_name', name_entry.text()))
- self.widgets.ship['name'] = name_entry
+ lambda: self.set_build_item(self.build['space'], 'ship_name', name_entry.text()))
+ self.build2.ship.name = name_entry
name_entry.setSizePolicy(SMINMAX)
ship_layout.addWidget(name_entry, 2, 1, 1, 3)
- desc_label = self.create_label('Build Description:')
+ desc_label = create_label2(self.theme2, 'Build Description:')
ship_layout.addWidget(desc_label, 3, 0, 1, 4)
desc_edit = QPlainTextEdit()
desc_edit.setSizePolicy(SMINMIN)
- desc_edit.setStyleSheet(self.get_style_class('QPlainTextEdit', 'textedit'))
+ desc_edit.setStyleSheet(self.theme2.get_style_class('QPlainTextEdit', 'textedit'))
desc_edit.setFont(self.theme2.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
desc_edit.textChanged.connect(lambda: self.set_build_item(
- self.build['space'], 'ship_desc', desc_edit.toPlainText(), autosave=False))
- self.widgets.ship['desc'] = desc_edit
+ self.build['space'], 'ship_desc', desc_edit.toPlainText(), autosave=False))
+ self.build2.ship.desc = desc_edit
ship_layout.addWidget(desc_edit, 4, 0, 1, 4)
ship_frame.setLayout(ship_layout)
layout.addWidget(ship_frame, stretch=2)
frame.setLayout(layout)
- def setup_build_frames(self):
+ def create_build_section(
+ self, label_text: str, button_count: int, environment: str, build_key: str,
+ is_equipment: bool = False, label_store: str = '') -> GridLayout:
"""
- Creates build areas
+ Creates a block of item buttons below a label.
+
+ Parameters:
+ - :param label_text: text to be displayed above the buttons
+ - :param button_count: number of buttons to be created
+ - :param environment: "space" or "ground"
+ - :param build_key: key for self.build['space'/'ground']
+ - :param is_equipment: True when items are equipment, False if items are abilities or traits
+ - :param label_store: stores category label in self.widgets.build[`label_store`] if set
"""
- self.setup_space_build_frame()
- self.setup_ground_build_frame()
- cache_skills(self.cache.skills, self.app_dir)
- self.setup_space_skill_frame()
- self.setup_ground_skill_frame()
+ layout = GridLayout(spacing=self.theme2['defaults']['margin'] * self.theme2.scale)
+ label = create_label2(self.theme2, label_text, style_override={'margin': (0, 0, 6, 0)})
+ label_size_policy = label.sizePolicy()
+ label_size_policy.setRetainSizeWhenHidden(True)
+ label.setSizePolicy(label_size_policy)
+ layout.addWidget(label, 0, 0, 1, button_count, alignment=ALEFT)
+ widget_storage = self.build2.space if environment == 'space' else self.build2.ground
+ if label_store != '':
+ setattr(widget_storage, label_store, label)
+ for i in range(button_count):
+ button = create_item_button2(self.theme2)
+ button.clicked.connect(lambda subkey=i, bt=button: self.picker(
+ environment, build_key, subkey, bt, is_equipment))
+ button.rightclicked.connect(lambda event, subkey=i: self.context_menu.invoke(
+ event, build_key, subkey, environment))
+ getattr(widget_storage, build_key)[i] = button
+ layout.addWidget(button, 1, i, alignment=ALEFT)
+ return layout
+
+ def create_boff_station_space(
+ self, profession: str, specialization: str = '', boff_id: int = 0) -> GridLayout:
+ """
+ Creates a block of item buttons with label / Combobox representing boff station.
+
+ Parameters:
+ - :param profession: "Tactical", "Science", "Engineering" or "Universal"
+ - :param specialization: specialization of the seat; empty if it has no specialization
+ - :param boff_id: identifies the boff station
+ """
+ layout = GridLayout(spacing=self.theme2['defaults']['margin'] * self.theme2.scale)
+ layout.setColumnStretch(3, 1)
+ if specialization != '':
+ specialization = f' / {specialization}'
+ if profession == 'Universal':
+ label_options = (
+ f'Tactical{specialization}',
+ f'Science{specialization}',
+ f'Engineering{specialization}'
+ )
+ else:
+ label_options = (profession + specialization,)
+ widget_storage = self.build2.space
+ label_layout = HBoxLayout(spacing=self.config.ui_scale * 3)
+ icon_label = TooltipLabel('', create_label2(self.theme2, '', 'label_tooltip'))
+ widget_storage.boff_label_icons[boff_id] = icon_label
+ label_layout.addWidget(icon_label, alignment=ALEFT)
+ icon_label.hide()
+ label = create_combo_box2(
+ self.theme2, size_policy=SMAXMAX, style_override=self.theme['boff_combo'])
+ # label.currentTextChanged.connect(
+ # lambda new: boff_profession_callback_space(self, boff_id, new))
+ label.addItems(label_options)
+ label_size_policy = label.sizePolicy()
+ label_size_policy.setRetainSizeWhenHidden(True)
+ label.setSizePolicy(label_size_policy)
+ widget_storage.boff_labels[boff_id] = label
+ label_layout.addWidget(label, alignment=ALEFT)
+ layout.addLayout(label_layout, 0, 0, 1, 4, alignment=ALEFT)
+ for i in range(4):
+ button = create_item_button2(self.theme2)
+ button.sizePolicy().setRetainSizeWhenHidden(True)
+ button.clicked.connect(lambda subkey=i, bt=button: self.picker(
+ 'space', 'boffs', subkey, bt, boff_id=boff_id))
+ button.rightclicked.connect(lambda event, subkey=i: self.context_menu.invoke(
+ event, 'boffs', subkey, 'space', boff_id))
+ layout.addWidget(button, 1, i, alignment=ALEFT)
+ widget_storage.boffs[boff_id][i] = button
+ return layout
+
+ def create_boff_station_ground(self, boff_id: int) -> VBoxLayout:
+ """
+ Creates a block of item buttons with label / Combobox representing boff station.
+
+ Parameters:
+ - :param boff_id: identifies the boff station
+ """
+ widget_storage = self.build2.ground
+ m = self.theme2['defaults']['margin'] * self.theme2.scale
+ layout = VBoxLayout(spacing=m)
+ label_layout = HBoxLayout(spacing=m)
+ label_layout.setAlignment(ALEFT)
+ prof_label = create_combo_box2(self.theme2, style_override=self.theme['boff_combo'])
+ # prof_label.currentTextChanged.connect(
+ # lambda new: boff_label_callback_ground(self, boff_id, 'boff_profs', new))
+ prof_label.addItems(CAREERS)
+ widget_storage.boff_profs[boff_id] = prof_label
+ label_layout.addWidget(prof_label)
+ spec_label = create_combo_box2(self.theme2, style_override=self.theme['boff_combo'])
+ # spec_label.currentTextChanged.connect(
+ # lambda new: boff_label_callback_ground(self, boff_id, 'boff_specs', new))
+ spec_label.addItems(GROUND_BOFF_SPECS)
+ widget_storage['boff_specs'][boff_id] = spec_label
+ label_layout.addWidget(spec_label)
+ layout.addLayout(label_layout)
+ button_layout = HBoxLayout(spacing=m)
+ button_layout.setAlignment(ALEFT)
+ for i in range(4):
+ button = create_item_button2(self.theme2)
+ button.clicked.connect(lambda subkey=i, bt=button: self.picker(
+ 'ground', 'boffs', subkey, bt, boff_id=boff_id))
+ button.rightclicked.connect(lambda event, subkey=i: self.context_menu.invoke(
+ event, 'boffs', subkey, 'ground', boff_id))
+ button_layout.addWidget(button)
+ widget_storage.boffs[boff_id][i] = button
+ layout.addLayout(button_layout)
+ return layout
+
+ def create_personal_trait_section(self, environment: str) -> GridLayout:
+ """
+ Creates build section for personal traits
+
+ Parameters:
+ - :param environment: "space" / "ground"
+ """
+ layout = GridLayout(spacing=self.theme2['defaults']['margin'] * self.theme2.scale)
+ label = create_label2(
+ self.theme2, 'Personal Traits', style_override={'margin': (0, 0, 6, 0)})
+ layout.addWidget(label, 0, 0, 1, 4, alignment=ALEFT)
+ widget_storage = self.build2.space if environment == 'space' else self.build2.ground
+ for row in range(3):
+ for col in range(4):
+ i = row * 4 + col
+ button = create_item_button2(self)
+ button.clicked.connect(
+ lambda subkey=i, bt=button: self.picker(environment, 'traits', subkey, bt))
+ button.rightclicked.connect(lambda event, subkey=i: self.context_menu.invoke(
+ event, 'traits', subkey, environment))
+ layout.addWidget(button, row + 1, col, alignment=ALEFT)
+ widget_storage.traits[i] = button
+ # Last button is for innate trait and should not be clickable
+ button.setEnabled(False)
+ button.set_style(self.theme2['item_dark'])
+ return layout
+
+ def create_starship_trait_section(self) -> GridLayout:
+ """
+ Creates build section for starship traits
+ """
+ layout = GridLayout(spacing=self.theme2['defaults']['margin'] * self.theme2.scale)
+ label = create_label2(
+ self.theme2, 'Starship Traits', style_override={'margin': (0, 0, 6, 0)})
+ label.sizePolicy().setRetainSizeWhenHidden(True)
+ layout.addWidget(label, 0, 0, 1, 4, alignment=ALEFT)
+ widget_storage = self.build2.space
+ for col in range(5):
+ button = create_item_button2(self.theme2)
+ button.sizePolicy().setRetainSizeWhenHidden(True)
+ button.clicked.connect(
+ lambda subkey=col, bt=button: self.picker('space', 'starship_traits', subkey, bt))
+ button.rightclicked.connect(lambda event, subkey=col: self.context_menu.invoke(
+ event, 'starship_traits', subkey, 'space'))
+ layout.addWidget(button, 1, col, alignment=ALEFT)
+ widget_storage.starship_traits[col] = button
+ for col in range(2):
+ button = create_item_button2(self.theme2)
+ button.sizePolicy().setRetainSizeWhenHidden(True)
+ button.clicked.connect(lambda subkey=col + 5, bt=button: self.picker(
+ 'space', 'starship_traits', subkey, bt))
+ button.rightclicked.connect(lambda event, subkey=col + 5: self.context_menu.invoke(
+ event, 'starship_traits', subkey, 'space'))
+ layout.addWidget(button, 2, col, alignment=ALEFT)
+ widget_storage.starship_traits[col + 5] = button
+ return layout
+
+ def create_doff_section(self, environment: str) -> GridLayout:
+ """
+ Creates duty officer section
+
+ Parameters:
+ - :param environment: "space" / "ground"
+ """
+ doff_layout = GridLayout(spacing=self.theme2['defaults']['bw'] * self.theme2.scale)
+ doff_layout.setColumnStretch(1, 1)
+ widget_storage = self.build2.space if environment == 'space' else self.build2.ground
+ for i in range(6):
+ spec_combo = create_combo_box2(self.theme2, style_override=self.theme['doff_combo'])
+ # spec_combo.currentTextChanged.connect(
+ # lambda spec, i=i: doff_spec_callback(self, spec, environment, i))
+ doff_layout.addWidget(spec_combo, i, 0)
+ widget_storage.doffs_spec[i] = spec_combo
+ variant_combo = create_combo_box2(
+ self.theme2, style_override=self.theme['doff_combo'], class_=DoffCombobox)
+ # variant_combo.currentTextChanged.connect(
+ # lambda variant, i=i: doff_variant_callback(self, variant, environment, i))
+ doff_layout.addWidget(variant_combo, i, 1)
+ widget_storage.doffs_variant[i] = variant_combo
+ return doff_layout
+
+ def create_skill_group_space(self, group_data: dict, id_offset: int) -> GridLayout:
+ """
+ Creates a skill group (3 related skill nodes) in appropriate shape
+
+ Parameters:
+ - :param group_data: skill group data
+ - :param id_offset: index of the first skill node in self.widgets and self.build
+ """
+ layout = GridLayout(spacing=self.theme['defaults']['csp'] * self.config.ui_scale)
+ # one skill with 3 ranks
+ if group_data['grouping'] == 'column':
+ for index, node in enumerate(group_data['nodes']):
+ button = create_item_button2(self.theme2)
+ # button.clicked.connect(lambda id=id_offset + index: skill_callback_space(
+ # self, group_data['career'], id, 'column'))
+ button.skill_image_name = node['image']
+ button.tooltip = format_skill_tooltip(
+ group_data['skill'], group_data, index, 'space', self.theme2.tooltips)
+ self.build2.skills.space[group_data['career']][id_offset + index] = button
+ layout.addWidget(button, index, 0)
+ # == 'pair+1': one skill with 2 ranks and one sub-skill with 1 rank
+ # == 'separate': 3 separate skills
+ else:
+ button = create_item_button2(self.theme2)
+ # button.clicked.connect(lambda id=id_offset: skill_callback_space(
+ # self, group_data['career'], id, group_data['grouping']))
+ button.skill_image_name = group_data['nodes'][0]['image']
+ button.tooltip = format_skill_tooltip(
+ group_data['skill'][0], group_data, 0, 'space', self.theme2.tooltips)
+ layout.addWidget(button, 0, 0, 1, 2, alignment=AHCENTER | ABOTTOM)
+ self.build2.skills.space[group_data['career']][id_offset] = button
+ button = create_item_button2(self.theme2)
+ # button.clicked.connect(lambda id=id_offset + 1: skill_callback_space(
+ # self, group_data['career'], id, group_data['grouping']))
+ button.skill_image_name = group_data['nodes'][1]['image']
+ button.tooltip = format_skill_tooltip(
+ group_data['skill'][1], group_data, 1, 'space', self.theme2.tooltips)
+ layout.addWidget(button, 1, 0, alignment=ATOP)
+ self.build2.skills.space[group_data['career']][id_offset + 1] = button
+ button = create_item_button2(self.theme2)
+ # button.clicked.connect(lambda id=id_offset + 2: skill_callback_space(
+ # self, group_data['career'], id, group_data['grouping']))
+ button.skill_image_name = group_data['nodes'][2]['image']
+ button.tooltip = format_skill_tooltip(
+ group_data['skill'][2], group_data, 2, 'space', self.theme2.tooltips)
+ layout.addWidget(button, 1, 1, alignment=ATOP)
+ self.build2.skills.space[group_data['career']][id_offset + 2] = button
+ return layout
+
+ def create_bonus_bar_segment(
+ self, bar: str, index: int, style: str = 'bonus_bar',
+ style_override: dict = {}) -> QPushButton:
+ """
+ Creates segment of bar showing the spent skill points.
+
+ Parameters:
+ - :param bar: identifies the bar ("tac" / "sci" / "eng" / "ground")
+ - :param index: index of the segment within the bar
+ - :param style: style key
+ - :param style_override: overrides style specified by self.theme
+ """
+ seg = QPushButton()
+ seg.setEnabled(False)
+ seg.setCheckable(True)
+ seg.setStyleSheet(self.theme2.get_style_class('QPushButton', style, style_override))
+ seg.setFixedSize(7 * self.theme2.scale, 17 * self.theme2.scale)
+ self.build2.skills.bonus_bars[bar][index] = seg
+ return seg
+
+ def create_bonus_bar_space(self, career: str, layout: GridLayout, column: int):
+ """
+ Creates bonus bar for space career and inserts it into the given layout.
+
+ Parameters:
+ - :param career: "tac" / "eng" / "sci"
+ - :param layout: layout to insert the bar into
+ - :param column: column of the layout to use
+ """
+ segment_index = 0
+ button_index = 0
+ for row in range(29, 5, -1):
+ if row % 6 == 0:
+ button = create_item_button2(self.theme2)
+ # button.clicked.connect(
+ # lambda i=button_index: skill_unlock_callback(self, career, i))
+ layout.addWidget(button, row, column, alignment=AHCENTER)
+ self.build2.skills.unlocks[career][button_index] = button
+ button_index += 1
+ else:
+ segment = self.create_bonus_bar_segment(career, segment_index)
+ layout.addWidget(segment, row, column, alignment=AHCENTER)
+ segment_index += 1
+ for row in range(5, 1, -1):
+ segment = self.create_bonus_bar_segment(career, segment_index)
+ layout.addWidget(segment, row, column, alignment=AHCENTER)
+ segment_index += 1
+ button = create_item_button2(self.theme2)
+ # button.clicked.connect(lambda: skill_unlock_callback(self, career, 4))
+ layout.addWidget(button, 1, column, alignment=AHCENTER)
+ self.build2.skills.unlocks[career][4] = button
+
+ def create_skill_button_ground(self, group_data: dict, id: int, node_id: int) -> ItemButton:
+ """
+ Creates ground skill button and returns it
+
+ Parameters:
+ - :param group_data: skill group data
+ - :param id: index of the skill node in self.widgets and self.build
+ - :param node_id: 0 or 1 for first or second node
+ """
+ button = create_item_button2(self.theme2)
+ # button.clicked.connect(lambda: skill_callback_ground(self, group_data['tree'], id))
+ button.skill_image_name = group_data['nodes'][node_id]['image']
+ button.tooltip = format_skill_tooltip(
+ group_data['nodes'][node_id]['name'], group_data, node_id, 'ground',
+ self.theme2.tooltips)
+ self.build2.skills.ground[group_data['tree']][id] = button
+ return button
def setup_space_build_frame(self):
"""
Creates space build layout
"""
- frame = self.widgets.build_frames[0]
+ frame = self.tabbers.build_frames[0]
isp = self.theme2['defaults']['isp'] * 2 * self.theme2.scale
layout = GridLayout(margins=isp, spacing=isp)
layout.setColumnStretch(0, 1)
@@ -479,17 +807,17 @@ def setup_space_build_frame(self):
fore_layout = self.create_build_section('Fore Weapons', 5, 'space', 'fore_weapons', True)
layout.addLayout(fore_layout, 0, 1, alignment=ALEFT)
aft_layout = self.create_build_section(
- 'Aft Weapons', 5, 'space', 'aft_weapons', True, 'aft_weapons_label')
+ 'Aft Weapons', 5, 'space', 'aft_weapons', True, 'aft_weapons_label')
layout.addLayout(aft_layout, 1, 1, alignment=ALEFT)
exp_layout = self.create_build_section(
- 'Experimental Weapon', 1, 'space', 'experimental', True, 'experimental_label')
+ 'Experimental Weapon', 1, 'space', 'experimental', True, 'experimental_label')
layout.addLayout(exp_layout, 2, 1, alignment=ALEFT)
device_layout = self.create_build_section('Devices', 6, 'space', 'devices', True)
layout.addLayout(device_layout, 3, 1, alignment=ALEFT)
hangar_layout = self.create_build_section(
- 'Hangars', 2, 'space', 'hangars', True, 'hangars_label')
+ 'Hangars', 2, 'space', 'hangars', True, 'hangars_label')
layout.addLayout(hangar_layout, 4, 1, alignment=ALEFT)
- sep1 = self.create_frame(size_policy=SMAXMIN, style_override={
+ sep1 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
sep1.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep1, 0, 2, 5, 1)
@@ -497,7 +825,7 @@ def setup_space_build_frame(self):
deflector_layout = self.create_build_section('Deflector', 1, 'space', 'deflector', True)
layout.addLayout(deflector_layout, 0, 3, alignment=ALEFT)
secdef_layout = self.create_build_section(
- 'Sec-Def', 1, 'space', 'sec_def', True, 'sec_def_label')
+ 'Sec-Def', 1, 'space', 'sec_def', True, 'sec_def_label')
layout.addLayout(secdef_layout, 1, 3, alignment=ALEFT)
engine_layout = self.create_build_section('Engines', 1, 'space', 'engines', True)
layout.addLayout(engine_layout, 2, 3, alignment=ALEFT)
@@ -505,24 +833,24 @@ def setup_space_build_frame(self):
layout.addLayout(warp_layout, 3, 3, alignment=ALEFT)
shield_layout = self.create_build_section('Shield', 1, 'space', 'shield', True)
layout.addLayout(shield_layout, 4, 3, alignment=ALEFT)
- sep2 = self.create_frame(size_policy=SMAXMIN, style_override={
+ sep2 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
sep2.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep2, 0, 4, 5, 1)
uni_layout = self.create_build_section(
- 'Universal Consoles', 3, 'space', 'uni_consoles', True, 'uni_consoles_label')
+ 'Universal Consoles', 3, 'space', 'uni_consoles', True, 'uni_consoles_label')
layout.addLayout(uni_layout, 0, 5, alignment=ALEFT)
eng_layout = self.create_build_section(
- 'Engineering Consoles', 5, 'space', 'eng_consoles', True, 'eng_consoles_label')
+ 'Engineering Consoles', 5, 'space', 'eng_consoles', True, 'eng_consoles_label')
layout.addLayout(eng_layout, 1, 5, alignment=ALEFT)
sci_layout = self.create_build_section(
- 'Science Consoles', 5, 'space', 'sci_consoles', True, 'sci_consoles_label')
+ 'Science Consoles', 5, 'space', 'sci_consoles', True, 'sci_consoles_label')
layout.addLayout(sci_layout, 2, 5, alignment=ALEFT)
tac_layout = self.create_build_section(
- 'Tactical Consoles', 5, 'space', 'tac_consoles', True, 'tac_consoles_label')
+ 'Tactical Consoles', 5, 'space', 'tac_consoles', True, 'tac_consoles_label')
layout.addLayout(tac_layout, 3, 5, alignment=ALEFT)
- sep3 = self.create_frame(size_policy=SMAXMIN, style_override={
+ sep3 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
sep3.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep3, 0, 6, 5, 1)
@@ -539,7 +867,7 @@ def setup_space_build_frame(self):
boff_5_layout = self.create_boff_station_space('Universal', 'Temporal', boff_id=4)
layout.addLayout(boff_5_layout, 4, 7, alignment=ALEFT)
boff_6_layout = self.create_boff_station_space('Universal', boff_id=5)
- width_placeholder = self.create_combo_box(size_policy=SMAXMAX)
+ width_placeholder = create_combo_box2(self.theme2, size_policy=SMAXMAX)
width_placeholder.addItem('Engineering / Miracle Worker')
width_placeholder_sizepolicy = width_placeholder.sizePolicy()
width_placeholder_sizepolicy.setRetainSizeWhenHidden(True)
@@ -559,19 +887,19 @@ def setup_space_build_frame(self):
rep_trait_layout = self.create_build_section('Reputation Traits', 5, 'space', 'rep_traits')
trait_layout.addLayout(rep_trait_layout, 2, 0)
active_trait_layout = self.create_build_section(
- 'Active Reputation Traits', 5, 'space', 'active_rep_traits')
+ 'Active Reputation Traits', 5, 'space', 'active_rep_traits')
trait_layout.addLayout(active_trait_layout, 3, 0)
layout.addLayout(trait_layout, 0, 9, 6, 1, alignment=ATOP)
# Doffs
spacing = self.theme2['defaults']['bw'] * self.theme2.scale
- doff_container = self.create_frame(size_policy=SMINMAX)
+ doff_container = create_frame2(self.theme2, size_policy=SMINMAX)
doff_container_layout = VBoxLayout(spacing=spacing * 2)
- doff_label = self.create_label('Space Duty Officers')
+ doff_label = create_label2(self.theme2, 'Space Duty Officers')
doff_container_layout.addWidget(doff_label, alignment=ALEFT)
- doff_frame = self.create_frame('doff_frame', size_policy=SMINMAX)
+ doff_frame = create_frame2(self.theme2, 'doff_frame', size_policy=SMINMAX)
doff_frame_layout = VBoxLayout()
- doff_style_nullifier = self.create_frame(size_policy=SMINMAX)
+ doff_style_nullifier = create_frame2(self.theme2, size_policy=SMINMAX)
doff_frame_layout.addWidget(doff_style_nullifier)
doff_layout = self.create_doff_section('space')
doff_style_nullifier.setLayout(doff_layout)
@@ -594,25 +922,25 @@ def setup_ground_build_frame(self):
layout.setRowStretch(5, 1)
# Equipment
- modules_layout = self.create_build_section('Kit Modules:', 6, 'ground', 'kit_modules', True)
+ modules_layout = self.create_build_section('Kit Modules', 6, 'ground', 'kit_modules', True)
layout.addLayout(modules_layout, 0, 1, alignment=ALEFT)
- weapons_layout = self.create_build_section('Weapons:', 2, 'ground', 'weapons', True)
+ weapons_layout = self.create_build_section('Weapons', 2, 'ground', 'weapons', True)
layout.addLayout(weapons_layout, 1, 1, alignment=ALEFT)
- devices_layout = self.create_build_section('Devices:', 5, 'ground', 'ground_devices', True)
+ devices_layout = self.create_build_section('Devices', 5, 'ground', 'ground_devices', True)
layout.addLayout(devices_layout, 2, 1, alignment=ALEFT)
- sep1 = self.create_frame(size_policy=SMAXMIN, style_override={
+ sep1 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
sep1.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep1, 0, 2)
- kit_layout = self.create_build_section('Kit Frame:', 1, 'ground', 'kit', True)
+ kit_layout = self.create_build_section('Kit Frame', 1, 'ground', 'kit', True)
layout.addLayout(kit_layout, 0, 3, alignment=ALEFT)
- armor_layout = self.create_build_section('Armor:', 1, 'ground', 'armor', True)
+ armor_layout = self.create_build_section('Armor', 1, 'ground', 'armor', True)
layout.addLayout(armor_layout, 1, 3, alignment=ALEFT)
- ev_layout = self.create_build_section('EV Suit:', 1, 'ground', 'ev_suit', True)
+ ev_layout = self.create_build_section('EV Suit', 1, 'ground', 'ev_suit', True)
layout.addLayout(ev_layout, 2, 3, alignment=ALEFT)
- shield_layout = self.create_build_section('Shield:', 1, 'ground', 'personal_shield', True)
+ shield_layout = self.create_build_section('Shield', 1, 'ground', 'personal_shield', True)
layout.addLayout(shield_layout, 3, 3, alignment=ALEFT)
- sep2 = self.create_frame(size_policy=SMAXMIN, style_override={
+ sep2 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
sep2.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep2, 0, 4)
@@ -626,7 +954,7 @@ def setup_ground_build_frame(self):
layout.addLayout(boff_3_layout, 2, 5, alignment=ALEFT)
boff_4_layout = self.create_boff_station_ground(boff_id=3)
layout.addLayout(boff_4_layout, 3, 5, alignment=ALEFT)
- sep3 = self.create_frame(size_policy=SMAXMIN, style_override={
+ sep3 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
sep3.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(sep3, 0, 6)
@@ -638,19 +966,19 @@ def setup_ground_build_frame(self):
rep_trait_layout = self.create_build_section('Reputation Traits', 5, 'ground', 'rep_traits')
trait_layout.addLayout(rep_trait_layout, 1, 0)
active_trait_layout = self.create_build_section(
- 'Active Reputation Traits', 5, 'ground', 'active_rep_traits')
+ 'Active Reputation Traits', 5, 'ground', 'active_rep_traits')
trait_layout.addLayout(active_trait_layout, 2, 0)
layout.addLayout(trait_layout, 0, 7, 4, 1, alignment=ATOP)
# Doffs
spacing = self.theme2['defaults']['bw'] * self.theme2.scale
- doff_container = self.create_frame(size_policy=SMINMAX)
+ doff_container = create_frame2(self.theme2, size_policy=SMINMAX)
doff_container_layout = VBoxLayout(spacing=spacing * 2)
- doff_label = self.create_label('Ground Duty Officers')
+ doff_label = create_label2(self.theme2, 'Ground Duty Officers')
doff_container_layout.addWidget(doff_label, alignment=ALEFT)
- doff_frame = self.create_frame('doff_frame', size_policy=SMINMAX)
+ doff_frame = create_frame2(self.theme2, 'doff_frame', size_policy=SMINMAX)
doff_frame_layout = VBoxLayout()
- doff_style_nullifier = self.create_frame(size_policy=SMINMAX)
+ doff_style_nullifier = create_frame2(self.theme2, size_policy=SMINMAX)
doff_frame_layout.addWidget(doff_style_nullifier)
doff_layout = self.create_doff_section('ground')
doff_style_nullifier.setLayout(doff_layout)
@@ -662,19 +990,19 @@ def setup_ground_build_frame(self):
frame.setLayout(layout)
# sidebar
- sidebar_frame = self.widgets.sidebar_frames[1]
+ sidebar_frame = self.tabbers.sidebar_frames[1]
csp = self.theme2['defaults']['csp'] * self.theme2.scale
sidebar_layout = GridLayout(margins=(csp, isp, csp, csp), spacing=csp)
sidebar_layout.setColumnStretch(0, 1)
- desc_label = self.create_label('Build Description:')
+ desc_label = create_label2(self.theme2, 'Build Description:')
sidebar_layout.addWidget(desc_label, 0, 0)
desc_edit = QPlainTextEdit()
- desc_edit.setStyleSheet(self.get_style_class('QPlainTextEdit', 'textedit'))
+ desc_edit.setStyleSheet(self.theme2.get_style_class('QPlainTextEdit', 'textedit'))
desc_edit.setFont(self.theme2.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
- desc_edit.textChanged.connect(lambda: self.set_build_item(
- self.build['ground'], 'ground_desc', desc_edit.toPlainText(), autosave=False))
- self.widgets.ground_desc = desc_edit
+ desc_edit.textChanged.connect(lambda: self.build2.set(
+ 'ground', 'ground_desc', value=desc_edit.toPlainText(), autosave=False))
+ self.build2.ground.desc = desc_edit
sidebar_layout.addWidget(desc_edit, 1, 0)
sidebar_frame.setLayout(sidebar_layout)
@@ -685,76 +1013,74 @@ def setup_character_frame(self, frame: QFrame):
csp = self.theme2['defaults']['csp'] * self.theme2.scale
layout = GridLayout(margins=csp, spacing=csp)
layout.setColumnStretch(1, 1)
- seperator = self.create_frame(size_policy=SMINMAX, style_override={
- 'background-color': '@sets', 'margin': '@isp'})
- sep = self.theme2['defaults']['sep'] * self.theme2.scale
- seperator.setFixedHeight(sep)
+ seperator = create_frame2(self.theme2, size_policy=SMINMAX, style_override={
+ 'background-color': '@sets', 'margin': '@isp'})
+ seperator.setFixedHeight(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(seperator, 0, 0, 1, 2, alignment=ATOP) # ATOP makes it respect the margin?
char_name = self.create_entry(placeholder='NAME')
char_name.setAlignment(AHCENTER)
char_name.setSizePolicy(SMINMAX)
char_name.editingFinished.connect(
- lambda: self.set_build_item(self.build['captain'], 'name', char_name.text()))
+ lambda: self.set_build_item(self.build['captain'], 'name', char_name.text()))
layout.addWidget(char_name, 1, 0, 1, 2)
- elite_label = self.create_label('Elite Captain')
+ self.build2.character.name = char_name
+ elite_label = create_label2(self.theme2, 'Elite Captain')
layout.addWidget(elite_label, 2, 0, alignment=ARIGHT)
- elite_checkbox = self.create_checkbox()
+ elite_checkbox = create_checkbox2(self.theme2)
elite_checkbox.checkStateChanged.connect(self.elite_callback)
layout.addWidget(elite_checkbox, 2, 1, alignment=ALEFT)
- career_label = self.create_label('Captain Career')
+ self.build2.character.elite = elite_checkbox
+ career_label = create_label2(self.theme2, 'Captain Career')
layout.addWidget(career_label, 3, 0, alignment=ARIGHT)
- career_combo = self.create_combo_box()
+ career_combo = create_combo_box2(self.theme2)
career_combo.addItems({''} | CAREERS)
career_combo.currentTextChanged.connect(
- lambda t: self.set_build_item(self.build['captain'], 'career', t))
+ lambda t: self.set_build_item(self.build['captain'], 'career', t))
layout.addWidget(career_combo, 3, 1)
- faction_label = self.create_label('Faction')
+ self.build2.character.career = career_combo
+ faction_label = create_label2(self.theme2, 'Faction')
layout.addWidget(faction_label, 4, 0, alignment=ARIGHT)
- faction_combo = self.create_combo_box()
+ faction_combo = create_combo_box2(self.theme2)
faction_combo.addItems({''} | FACTIONS)
faction_combo.currentTextChanged.connect(self.faction_combo_callback)
layout.addWidget(faction_combo, 4, 1)
- species_label = self.create_label('Species')
+ self.build2.character.faction = faction_combo
+ species_label = create_label2(self.theme2, 'Species')
layout.addWidget(species_label, 5, 0, alignment=ARIGHT)
- species_combo = self.create_combo_box()
+ species_combo = create_combo_box2(self.theme2)
species_combo.addItems({''})
species_combo.currentTextChanged.connect(lambda t: self.species_combo_callback(t))
layout.addWidget(species_combo, 5, 1)
- primary_label = self.create_label('Primary Spec')
+ self.build2.character.species = species_combo
+ primary_label = create_label2(self.theme2, 'Primary Spec')
layout.addWidget(primary_label, 6, 0, alignment=ARIGHT)
- primary_combo = self.create_combo_box()
+ primary_combo = create_combo_box2(self.theme2)
primary_combo.addItems({''} | PRIMARY_SPECS)
primary_combo.currentTextChanged.connect(lambda t: self.spec_combo_callback(True, t))
layout.addWidget(primary_combo, 6, 1)
- secondary_label = self.create_label('Secondary Spec', style_override={'margin-bottom': 0})
+ self.build2.character.primary = primary_combo
+ secondary_label = create_label2(
+ self.theme2, 'Secondary Spec', style_override={'margin-bottom': 0})
layout.addWidget(secondary_label, 7, 0, alignment=ARIGHT)
- secondary_combo = self.create_combo_box()
+ secondary_combo = create_combo_box2(self.theme2)
secondary_combo.addItems({''} | PRIMARY_SPECS | SECONDARY_SPECS)
secondary_combo.currentTextChanged.connect(lambda t: self.spec_combo_callback(False, t))
layout.addWidget(secondary_combo, 7, 1)
+ self.build2.character.secondary = secondary_combo
frame.setLayout(layout)
- self.widgets.character = {
- 'name': char_name,
- 'elite': elite_checkbox,
- 'career': career_combo,
- 'faction': faction_combo,
- 'species': species_combo,
- 'primary': primary_combo,
- 'secondary': secondary_combo,
- }
def setup_space_skill_frame(self):
"""
Creates Space skill GUI
"""
- frame = self.widgets.build_frames[2]
+ frame = self.tabbers.build_frames[2]
isp = self.theme2['defaults']['isp'] * self.theme2.scale
csp = self.theme2['defaults']['csp'] * self.theme2.scale
col_layout = GridLayout(margins=isp, spacing=csp)
col_layout.setRowStretch(0, 1)
col_layout.setColumnStretch(0, 3)
col_layout.setColumnStretch(2, 1)
- scroll_frame = self.create_frame()
+ scroll_frame = create_frame2(self.theme2)
scroll_area = QScrollArea()
scroll_area.setSizePolicy(SMINMIN)
scroll_area.setHorizontalScrollBarPolicy(SCROLLOFF)
@@ -769,6 +1095,7 @@ def setup_space_skill_frame(self):
scroll_layout.setColumnStretch(3, 1)
scroll_layout.setColumnStretch(4, 1)
scroll_layout.setColumnStretch(5, 1)
+
# skill tree
rank_texts = (
'Lieutenant
(0 points required)',
@@ -778,15 +1105,15 @@ def setup_space_skill_frame(self):
'Admiral
(35 points required)'
)
sep_height = self.theme2['hr']['height'] * self.theme2.scale
- for rank, skill_groups in enumerate(self.cache.skills['space']):
+ for rank, skill_groups in enumerate(self.cargo.skills['space']):
header_layout = GridLayout(spacing=isp)
- left_sep = self.create_frame('hr', size_policy=SMINMAX)
+ left_sep = create_frame2(self.theme2, 'hr', size_policy=SMINMAX)
left_sep.setFixedHeight(sep_height)
header_layout.addWidget(left_sep, 0, 0, alignment=AVCENTER)
- rank_label = self.create_label(rank_texts[rank], 'label_subhead')
+ rank_label = create_label2(self.theme2, rank_texts[rank], 'label_subhead')
rank_label.setAlignment(AHCENTER)
header_layout.addWidget(rank_label, 0, 1)
- right_sep = self.create_frame('hr', size_policy=SMINMAX)
+ right_sep = create_frame2(self.theme2, 'hr', size_policy=SMINMAX)
right_sep.setFixedHeight(sep_height)
header_layout.addWidget(right_sep, 0, 2, alignment=AVCENTER)
scroll_layout.addLayout(header_layout, rank * 3, 0, 1, 6)
@@ -794,64 +1121,64 @@ def setup_space_skill_frame(self):
id_offset = rank * 6 + (group_id % 2) * 3
group_layout = self.create_skill_group_space(group_data, id_offset)
scroll_layout.addLayout(group_layout, rank * 3 + 1, group_id)
- spacer = self.create_frame()
+ spacer = create_frame2(self.theme2)
spacer.setFixedHeight(isp)
scroll_layout.addWidget(spacer, rank * 3 + 2, 0)
VBoxLayout().addWidget(spacer)
-
scroll_frame.setLayout(scroll_layout)
scroll_area.setWidget(scroll_frame)
- seperator = self.create_frame(size_policy=SMAXMIN, style_override={
- 'background-color': '@sets'})
+ seperator = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ 'background-color': '@sets'})
seperator.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
col_layout.addWidget(seperator, 0, 1)
- bonus_bar_container = self.create_frame(size_policy=SMINMIN)
+ bonus_bar_container = create_frame2(self.theme2, size_policy=SMINMIN)
+
# bonus bars
bonus_bar_layout = GridLayout(margins=isp)
bonus_bar_layout.setRowStretch(0, 1)
bonus_bar_layout.setRowStretch(32, 1)
self.create_bonus_bar_space('eng', bonus_bar_layout, 1)
- eng_label = self.create_label('', style='unlock_label')
- eng_label.setPixmap(self.cache.icons['eng'])
+ eng_label = create_label2(self.theme2, '', style='unlock_label')
+ eng_label.setPixmap(self.theme2.icons['eng'])
bonus_bar_layout.addWidget(eng_label, 30, 1, alignment=AHCENTER)
- eng_count = self.create_label('0', 'label_subhead')
+ eng_count = create_label2(self.theme2, '0', 'label_subhead')
bonus_bar_layout.addWidget(eng_count, 31, 1, alignment=AHCENTER)
- self.widgets.skill_counts_space['eng'] = eng_count
+ self.build2.skills.count_labels['eng'] = eng_count
self.create_bonus_bar_space('sci', bonus_bar_layout, 2)
- sci_label = self.create_label('', style='unlock_label')
- sci_label.setPixmap(self.cache.icons['sci'])
+ sci_label = create_label2(self.theme2, '', style='unlock_label')
+ sci_label.setPixmap(self.theme2.icons['sci'])
bonus_bar_layout.addWidget(sci_label, 30, 2, alignment=AHCENTER)
- sci_count = self.create_label('0', 'label_subhead')
+ sci_count = create_label2(self.theme2, '0', 'label_subhead')
bonus_bar_layout.addWidget(sci_count, 31, 2, alignment=AHCENTER)
- self.widgets.skill_counts_space['sci'] = sci_count
+ self.build2.skills.count_labels['sci'] = sci_count
self.create_bonus_bar_space('tac', bonus_bar_layout, 3)
- tac_label = self.create_label('', style='unlock_label')
- tac_label.setPixmap(self.cache.icons['tac'])
+ tac_label = create_label2(self.theme2, '', style='unlock_label')
+ tac_label.setPixmap(self.theme2.icons['tac'])
bonus_bar_layout.addWidget(tac_label, 30, 3, alignment=AHCENTER)
- tac_count = self.create_label('0', 'label_subhead')
+ tac_count = create_label2(self.theme2, '0', 'label_subhead')
bonus_bar_layout.addWidget(tac_count, 31, 3, alignment=AHCENTER)
- self.widgets.skill_counts_space['tac'] = tac_count
+ self.build2.skills.count_labels['tac'] = tac_count
bonus_bar_container.setLayout(bonus_bar_layout)
col_layout.addWidget(bonus_bar_container, 0, 2)
frame.setLayout(col_layout)
# sidebar
- sidebar_frame = self.widgets.sidebar_frames[2]
+ sidebar_frame = self.tabbers.sidebar_frames[2]
sidebar_layout = GridLayout(margins=(csp, isp * 2, csp, csp), spacing=csp)
- desc_label = self.create_label('Space Skill Notes:')
+ desc_label = create_label2(self.theme2, 'Space Skill Notes:')
sidebar_layout.addWidget(desc_label, 0, 0, 1, 2)
desc_edit = QPlainTextEdit()
- desc_edit.setStyleSheet(self.get_style_class('QPlainTextEdit', 'textedit'))
+ desc_edit.setStyleSheet(self.theme2.get_style_class('QPlainTextEdit', 'textedit'))
desc_edit.setFont(self.theme2.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
- desc_edit.textChanged.connect(lambda: self.set_build_item(
- self.build['skill_desc'], 'space', desc_edit.toPlainText(), autosave=False))
- self.widgets.build['skill_desc']['space'] = desc_edit
+ desc_edit.textChanged.connect(lambda: self.build2.set(
+ 'space', 'skill_desc', value=desc_edit.toPlainText(), autosave=False))
+ self.build2.skills.space_desc = desc_edit
sidebar_layout.addWidget(desc_edit, 1, 0, 1, 2)
- load_skills_button = self.create_button('Load Skills')
+ load_skills_button = create_button2(self.theme2, 'Load Skills')
load_skills_button.clicked.connect(self.load_skills_callback)
sidebar_layout.addWidget(load_skills_button, 2, 0, alignment=AHCENTER)
- save_skills_button = self.create_button('Save Skills')
+ save_skills_button = create_button2(self.theme2, 'Save Skills')
save_skills_button.clicked.connect(self.save_skills_callback)
sidebar_layout.addWidget(save_skills_button, 2, 1, alignment=AHCENTER)
sidebar_frame.setLayout(sidebar_layout)
@@ -860,22 +1187,23 @@ def setup_ground_skill_frame(self):
"""
Creates Ground skill GUI
"""
- frame = self.widgets.build_frames[3]
+ frame = self.tabbers.build_frames[3]
isp = self.theme2['defaults']['isp'] * self.theme2.scale
csp = self.theme2['defaults']['csp'] * self.theme2.scale
col_layout = GridLayout(margins=isp, spacing=csp)
col_layout.setRowStretch(0, 1)
col_layout.setColumnStretch(0, 3)
col_layout.setColumnStretch(2, 1)
- tree_frame = self.create_frame(size_policy=SMINMIN)
+ tree_frame = create_frame2(self.theme2, size_policy=SMINMIN)
col_layout.addWidget(tree_frame, 0, 0)
+
# skill tree
tree_layout = GridLayout(spacing=5 * isp)
tree_layout.setColumnStretch(0, 1)
tree_layout.setColumnStretch(3, 1)
tree_layout.setRowStretch(0, 1)
tree_layout.setRowStretch(3, 1)
- skills = self.cache.skills['ground']
+ skills = self.cargo.skills['ground']
group_layout = GridLayout(spacing=csp)
group_layout.addWidget(self.create_skill_button_ground(skills[0], 0, 0), 0, 1)
group_layout.addWidget(self.create_skill_button_ground(skills[0], 1, 1), 1, 1)
@@ -894,31 +1222,31 @@ def setup_ground_skill_frame(self):
tree_layout.addLayout(group_layout, 1, 2)
group_layout = GridLayout(spacing=csp)
group_layout.addWidget(
- self.create_skill_button_ground(skills[6], 0, 0), 0, 0, 1, 2, alignment=AHCENTER)
+ self.create_skill_button_ground(skills[6], 0, 0), 0, 0, 1, 2, alignment=AHCENTER)
group_layout.addWidget(
- self.create_skill_button_ground(skills[6], 1, 1), 1, 0, alignment=ARIGHT)
+ self.create_skill_button_ground(skills[6], 1, 1), 1, 0, alignment=ARIGHT)
group_layout.addWidget(
- self.create_skill_button_ground(skills[7], 2, 0), 1, 1, alignment=ALEFT)
+ self.create_skill_button_ground(skills[7], 2, 0), 1, 1, alignment=ALEFT)
group_layout.addWidget(
- self.create_skill_button_ground(skills[7], 3, 1), 2, 1, alignment=ALEFT)
+ self.create_skill_button_ground(skills[7], 3, 1), 2, 1, alignment=ALEFT)
tree_layout.addLayout(group_layout, 2, 1)
group_layout = GridLayout(spacing=csp)
group_layout.addWidget(
- self.create_skill_button_ground(skills[8], 0, 0), 0, 0, 1, 2, alignment=AHCENTER)
+ self.create_skill_button_ground(skills[8], 0, 0), 0, 0, 1, 2, alignment=AHCENTER)
group_layout.addWidget(
- self.create_skill_button_ground(skills[8], 1, 1), 1, 1, alignment=ALEFT)
+ self.create_skill_button_ground(skills[8], 1, 1), 1, 1, alignment=ALEFT)
group_layout.addWidget(
- self.create_skill_button_ground(skills[9], 2, 0), 1, 0, alignment=ARIGHT)
+ self.create_skill_button_ground(skills[9], 2, 0), 1, 0, alignment=ARIGHT)
group_layout.addWidget(
- self.create_skill_button_ground(skills[9], 3, 1), 2, 0, alignment=ARIGHT)
+ self.create_skill_button_ground(skills[9], 3, 1), 2, 0, alignment=ARIGHT)
tree_layout.addLayout(group_layout, 2, 2)
-
tree_frame.setLayout(tree_layout)
- seperator = self.create_frame(size_policy=SMAXMIN, style_override={
- 'background-color': '@sets'})
+ seperator = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ 'background-color': '@sets'})
seperator.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
col_layout.addWidget(seperator, 0, 1)
- bonus_bar_container = self.create_frame(size_policy=SMINMIN)
+ bonus_bar_container = create_frame2(self.theme2, size_policy=SMINMIN)
+
# bonus bars
bonus_bar_layout = GridLayout(margins=isp)
bonus_bar_layout.setRowStretch(0, 1)
@@ -929,37 +1257,37 @@ def setup_ground_skill_frame(self):
bonus_bar_layout.addWidget(seg1, row, 1, alignment=AHCENTER)
seg2 = self.create_bonus_bar_segment('ground', i * 2 + 1)
bonus_bar_layout.addWidget(seg2, row - 1, 1, alignment=AHCENTER)
- button = self.create_item_button()
- button.clicked.connect(lambda i=i: self.skill_unlock_callback('ground', i))
+ button = create_item_button2(self.theme2)
+ # button.clicked.connect(lambda i=i: self.skill_unlock_callback('ground', i))
bonus_bar_layout.addWidget(button, row - 2, 1, alignment=AHCENTER)
- self.widgets.build['skill_unlocks']['ground'][i] = button
+ self.build2.skills.unlocks['ground'][i] = button
row -= 3
- icon_label = self.create_label('', style='unlock_label')
+ icon_label = create_label2(self.theme2, '', style='unlock_label')
icon_label.setPixmap(self.cache.icons['ground'])
bonus_bar_layout.addWidget(icon_label, 16, 1, alignment=AHCENTER)
- self.widgets.skill_count_ground = self.create_label('0', 'label_subhead')
+ self.build2.skills.count_labels['ground'] = create_label2(self.theme2, '0', 'label_subhead')
bonus_bar_layout.addWidget(self.widgets.skill_count_ground, 17, 1, alignment=AHCENTER)
bonus_bar_container.setLayout(bonus_bar_layout)
col_layout.addWidget(bonus_bar_container, 0, 2)
frame.setLayout(col_layout)
# sidebar
- sidebar_frame = self.widgets.sidebar_frames[3]
+ sidebar_frame = self.tabbers.sidebar_frames[3]
sidebar_layout = GridLayout(margins=(csp, isp * 2, csp, csp), spacing=csp)
- desc_label = self.create_label('Ground Skill Notes:')
+ desc_label = create_label2(self.theme2, 'Ground Skill Notes:')
sidebar_layout.addWidget(desc_label, 0, 0, 1, 2)
desc_edit = QPlainTextEdit()
- desc_edit.setStyleSheet(self.get_style_class('QPlainTextEdit', 'textedit'))
+ desc_edit.setStyleSheet(self.theme2.get_style_class('QPlainTextEdit', 'textedit'))
desc_edit.setFont(self.theme2.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
- desc_edit.textChanged.connect(lambda: self.set_build_item(
- self.build['skill_desc'], 'ground', desc_edit.toPlainText(), autosave=False))
+ desc_edit.textChanged.connect(lambda: self.build2.set(
+ 'ground', 'skill_desc', value=desc_edit.toPlainText(), autosave=False))
self.widgets.build['skill_desc']['ground'] = desc_edit
sidebar_layout.addWidget(desc_edit, 1, 0, 1, 2)
- load_skills_button = self.create_button('Load Skills')
+ load_skills_button = create_button2(self.theme2, 'Load Skills')
load_skills_button.clicked.connect(self.load_skills_callback)
sidebar_layout.addWidget(load_skills_button, 2, 0, alignment=AHCENTER)
- save_skills_button = self.create_button('Save Skills')
+ save_skills_button = create_button2(self.theme2, 'Save Skills')
save_skills_button.clicked.connect(self.save_skills_callback)
sidebar_layout.addWidget(save_skills_button, 2, 1, alignment=AHCENTER)
sidebar_frame.setLayout(sidebar_layout)
@@ -968,16 +1296,16 @@ def setup_splash(self, frame: QFrame):
"""
Creates Splash screen.
"""
- layout = GridLayout(margins=0, spacing=0)
+ layout = GridLayout()
layout.setRowStretch(0, 1)
layout.setRowStretch(3, 1)
layout.setColumnStretch(0, 3)
layout.setColumnStretch(1, 2)
layout.setColumnStretch(2, 3)
- loading_image = ImageLabel(get_asset_path('sets_loading.png', self.app_dir), (1, 1))
+ loading_image = ImageLabel(self.app_dir2 / 'local' / 'sets_loading.png', (1, 1))
layout.addWidget(loading_image, 1, 1)
- loading_label = self.create_label('Loading: ...', 'label_subhead')
- self.widgets.loading_label = loading_label
+ loading_label = create_label2(self.theme2, 'Loading: ...', 'label_subhead')
+ self.splash.loading_label = loading_label
layout.addWidget(loading_label, 2, 0, 1, 3, alignment=AHCENTER)
frame.setLayout(layout)
@@ -994,124 +1322,123 @@ def setup_settings_frame(self):
"""
Populates the settings frame.
"""
- settings_frame = self.widgets.build_frames[5]
+ settings_frame = self.tabbers.build_frames[5]
isp = self.theme2['defaults']['isp'] * self.theme2.scale
settings_layout = HBoxLayout(margins=(2 * isp, isp, isp, isp), spacing=isp)
scroll_layout = VBoxLayout(margins=(0, isp, 0, 0), spacing=isp)
scroll_layout.setSpacing(isp)
- scroll_frame = self.create_frame()
+ scroll_frame = create_frame2(self.theme2)
scroll_area = QScrollArea()
scroll_area.setSizePolicy(SMINMIN)
scroll_area.setHorizontalScrollBarPolicy(SCROLLOFF)
scroll_area.setVerticalScrollBarPolicy(SCROLLON)
- # scroll_area.setAlignment(AHCENTER)
settings_layout.addWidget(scroll_area)
settings_frame.setLayout(settings_layout)
# first section
- settings_header = self.create_label('Settings:', 'label_heading')
+ settings_header = create_label2(self.theme2, 'Settings:', 'label_heading')
scroll_layout.addWidget(settings_header, alignment=ALEFT)
sec_1 = GridLayout(spacing=isp)
sec_1.setColumnMinimumWidth(1, 3 * isp)
sec_1.setColumnMinimumWidth(2, 12 * isp)
sec_1.setColumnMinimumWidth(3, 3 * isp)
sec_1.setColumnStretch(5, 1)
- ui_scale_label = self.create_label('UI Scale')
+ ui_scale_label = create_label2(self.theme2, 'UI Scale')
sec_1.addWidget(ui_scale_label, 0, 0, alignment=ALEFT)
- ui_scale_slider = self.create_annotated_slider(
- default_value=round(self.settings.ui_scale * 50, 0),
- min=25, max=75, callback=self.settings.set_ui_scale)
+ ui_scale_slider = create_annotated_slider2(
+ self.theme2, default_value=round(self.settings.ui_scale * 50, 0), min=25, max=75,
+ callback=self.settings.set_ui_scale)
sec_1.addLayout(ui_scale_slider, 0, 2, alignment=ALEFT)
- ui_scale_desc = self.create_label('Requires restart.', 'hint_label')
+ ui_scale_desc = create_label2(self.theme2, 'Requires restart.', 'hint_label')
sec_1.addWidget(ui_scale_desc, 0, 4, alignment=ALEFT)
- mark_label = self.create_label('Default Mark')
+ mark_label = create_label2(self.theme2, 'Default Mark')
sec_1.addWidget(mark_label, 1, 0, alignment=ALEFT)
- mark_combo = self.create_combo_box(style_override={'font': '@small_text'})
+ mark_combo = create_combo_box2(self.theme2, style_override={'font': '@small_text'})
mark_combo.addItems(('',) + MARKS)
mark_combo.setCurrentText(self.settings.default_mark)
mark_combo.currentTextChanged.connect(
- lambda new_mark: self.settings.set('default_mark', new_mark))
+ lambda new_mark: self.settings.set('default_mark', new_mark))
sec_1.addWidget(mark_combo, 1, 2, alignment=ALEFT)
- rarity_label = self.create_label('Default Rarity')
+ rarity_label = create_label2(self.theme2, 'Default Rarity')
sec_1.addWidget(rarity_label, 2, 0, alignment=ALEFT)
- rarity_combo = self.create_combo_box(style_override={'font': '@small_text'})
+ rarity_combo = create_combo_box2(self.theme2, style_override={'font': '@small_text'})
rarity_combo.addItems(RARITIES.keys())
rarity_combo.setCurrentText(self.settings.default_rarity)
rarity_combo.currentTextChanged.connect(
- lambda new_rarity: self.settings.set('default_rarity', new_rarity))
+ lambda new_rarity: self.settings.set('default_rarity', new_rarity))
sec_1.addWidget(rarity_combo, 2, 2, alignment=ALEFT | AVCENTER)
- picker_rel_label = self.create_label('Picker Position')
+ picker_rel_label = create_label2(self.theme2, 'Picker Position')
sec_1.addWidget(picker_rel_label, 3, 0, alignment=ALEFT)
- picker_rel_combo = self.create_combo_box(style_override={'font': '@small_text'})
+ picker_rel_combo = create_combo_box2(self.theme2, style_override={'font': '@small_text'})
picker_rel_combo.addItems(('Absolute', 'Relative'))
picker_rel_combo.setCurrentIndex(self.settings.picker_relative)
picker_rel_combo.currentIndexChanged.connect(
- lambda new_i: self.settings.set('picker_relative', new_i))
+ lambda new_i: self.settings.set('picker_relative', new_i))
sec_1.addWidget(picker_rel_combo, 3, 2, alignment=ALEFT | AVCENTER)
- picker_rel_label = self.create_label('Default Save Format')
+ picker_rel_label = create_label2(self.theme2, 'Default Save Format')
sec_1.addWidget(picker_rel_label, 4, 0, alignment=ALEFT)
- picker_rel_combo = self.create_combo_box(style_override={'font': '@small_text'})
+ picker_rel_combo = create_combo_box2(self.theme2, style_override={'font': '@small_text'})
picker_rel_combo.addItems(('JSON', 'PNG'))
picker_rel_combo.setCurrentText(self.settings.default_save_format)
picker_rel_combo.currentTextChanged.connect(
- lambda new_t: self.settings.set('default_save_format', new_t))
+ lambda new_t: self.settings.set('default_save_format', new_t))
sec_1.addWidget(picker_rel_combo, 4, 2, alignment=ALEFT | AVCENTER)
- backup_label = self.create_label('Preferred Backup')
+ backup_label = create_label2(self.theme2, 'Preferred Backup')
sec_1.addWidget(backup_label, 5, 0, alignment=ALEFT)
- backup_combo = self.create_combo_box(style_override={'font': '@small_text'})
+ backup_combo = create_combo_box2(self.theme2, style_override={'font': '@small_text'})
backup_combo.addItems(('Auto', 'Manual'))
backup_combo.setCurrentIndex(self.settings.pref_backup)
backup_combo.currentIndexChanged.connect(
- lambda new_i: self.settings.set('pref_backup', new_i))
+ lambda new_i: self.settings.set('pref_backup', new_i))
sec_1.addWidget(backup_combo, 5, 2, alignment=ALEFT | AVCENTER)
scroll_layout.addLayout(sec_1)
# second section
- sep = self.create_frame()
+ sep = create_frame2(self.theme2)
sep.setFixedHeight(isp)
scroll_layout.addWidget(sep)
- maintenance_header = self.create_label('Maintenance:', 'label_heading')
+ maintenance_header = create_label2(self.theme2, 'Maintenance:', 'label_heading')
scroll_layout.addWidget(maintenance_header, alignment=ALEFT)
sec_2 = GridLayout(spacing=isp)
sec_2.setColumnMinimumWidth(1, 3 * isp)
sec_2.setColumnStretch(3, 1)
- cargo_clear_button = self.create_button('Clear Cargo Data')
+ cargo_clear_button = create_button2(self.theme2, 'Clear Cargo Data')
cargo_clear_button.clicked.connect(
- lambda: delete_folder_contents(self.config.config_subfolders['cargo']))
+ lambda: delete_folder_contents(self.config.config_subfolders['cargo']))
sec_2.addWidget(cargo_clear_button, 0, 0, alignment=ALEFT)
- cargo_clear_label = self.create_label(
- 'Clears cargo data. Restart to refresh data.', 'hint_label')
+ cargo_clear_label = create_label2(
+ self.theme2, 'Clears cargo data. Restart to refresh data.', 'hint_label')
sec_2.addWidget(cargo_clear_label, 0, 2, alignment=ALEFT)
- cache_clear_button = self.create_button('Clear Cache')
+ cache_clear_button = create_button2(self.theme2, 'Clear Cache')
cache_clear_button.clicked.connect(
- lambda: delete_folder_contents(self.config.config_subfolders['cache']))
+ lambda: delete_folder_contents(self.config.config_subfolders['cache']))
sec_2.addWidget(cache_clear_button, 1, 0, alignment=ALEFT)
- cache_clear_label = self.create_label(
- 'Clears cache. Restart to rebuild cache.', 'hint_label')
+ cache_clear_label = create_label2(
+ self.theme2, 'Clears cache. Restart to rebuild cache.', 'hint_label')
sec_2.addWidget(cache_clear_label, 1, 2, alignment=ALEFT)
- backup_cargo_button = self.create_button('Backup Cargo Data')
+ backup_cargo_button = create_button2(self.theme2, 'Backup Cargo Data')
backup_cargo_button.clicked.connect(self.backup_cargo_data)
sec_2.addWidget(backup_cargo_button, 2, 0, alignment=ALEFT)
- backup_cargo_label = self.create_label(
- 'Creates cargo backup to protect against download failures.', 'hint_label')
+ backup_cargo_label = create_label2(
+ self.theme2, 'Creates cargo backup to protect against download failures.', 'hint_label')
sec_2.addWidget(backup_cargo_label, 2, 2, alignment=ALEFT)
scroll_layout.addLayout(sec_2)
# third section
- sep = self.create_frame()
+ sep = create_frame2(self.theme2)
sep.setFixedHeight(isp)
scroll_layout.addWidget(sep)
- compatibility_header = self.create_label('Compatibility:', 'label_heading')
+ compatibility_header = create_label2(self.theme2, 'Compatibility:', 'label_heading')
scroll_layout.addWidget(compatibility_header, alignment=ALEFT)
sec_3 = GridLayout(spacing=isp)
sec_3.setColumnMinimumWidth(1, 3 * isp)
sec_3.setColumnStretch(3, 1)
- build_image_button = self.create_button('Convert Legacy Build Image')
+ build_image_button = create_button2(self.theme2, 'Convert Legacy Build Image')
build_image_button.clicked.connect(self.load_legacy_build_image)
sec_3.addWidget(build_image_button, 0, 0, alignment=ALEFT)
- build_image_label = self.create_label(
- 'Loads build from legacy build image. Use the "Load" button to load legacy '
- 'JSON build files.', 'hint_label')
+ build_image_label = create_label2(
+ self.theme2, 'Loads build from legacy build image. Use the "Load" button to load '
+ 'legacy JSON build files.', 'hint_label')
sec_3.addWidget(build_image_label, 0, 2, alignment=ALEFT)
scroll_layout.addLayout(sec_3)
@@ -1119,15 +1446,16 @@ def setup_settings_frame(self):
scroll_area.setWidget(scroll_frame)
# sidebar
- sidebar_frame = self.widgets.sidebar_frames[5]
+ sidebar_frame = self.tabbers.sidebar_frames[5]
csp = self.theme2['defaults']['csp'] * self.theme2.scale
sidebar_layout = VBoxLayout(margins=csp, spacing=isp)
sidebar_layout.setAlignment(ATOP)
- sidebar_layout.addWidget(self.create_label('About SETS:', 'label_heading'), alignment=ALEFT)
- about_label = self.create_label(
- 'Thank you for using the STO Equipment and Trait Selector (SETS)! Make sure to '
- 'check out other projects of the STO Community Developers on our Github page and '
- 'contact us on Discord for support.')
+ sidebar_layout.addWidget(
+ create_label2(self.theme2, 'About SETS:', 'label_heading'), alignment=ALEFT)
+ about_label = create_label2(
+ self.theme2, 'Thank you for using the STO Equipment and Trait Selector (SETS)! Make '
+ 'sure to check out other projects of the STO Community Developers on our Github page '
+ 'and contact us on Discord for support.')
about_label.setWordWrap(True)
about_label.setMinimumWidth(50) # to fix the word wrap
about_label.setSizePolicy(SMINMAX)
@@ -1142,23 +1470,23 @@ def setup_settings_frame(self):
'Downloads': {
'callback': lambda: open_url(self.config.link_downloads), 'align': AHCENTER}
}
- button_layout, buttons = self.create_button_series(
- link_button_style, 'button', shape='column', ret=True)
+ button_layout, buttons = create_button_series2(
+ self.theme2, link_button_style, 'button', shape='column', ret=True)
buttons[0].setToolTip(self.config.link_website)
buttons[1].setToolTip(self.config.link_github)
buttons[2].setToolTip(self.config.link_discord)
buttons[3].setToolTip(self.config.link_downloads)
- link_button_frame = self.create_frame()
+ link_button_frame = create_frame2(self.theme2)
link_button_frame.setLayout(button_layout)
sidebar_layout.addWidget(link_button_frame, alignment=AHCENTER)
sidebar_frame.setLayout(sidebar_layout)
- footer_frame = self.widgets.character_frames[2]
+ footer_frame = self.tabbers.character_frames[2]
footer_layout = GridLayout(margins=csp, spacing=isp)
- version_label = self.create_label(
- f"Version: {self.versions[0]}\n({self.versions[1]})", 'hint_label')
+ version_label = create_label2(
+ self.theme2, f"Version: {self.versions[0]}\n({self.versions[1]})", 'hint_label')
footer_layout.addWidget(version_label, 0, 0, alignment=ALEFT | ABOTTOM)
- stocd_label = self.create_label('')
+ stocd_label = create_label2(self.theme2, '')
stocd_label.setPixmap(self.cache.icons['STOCD'])
footer_layout.addWidget(stocd_label, 0, 1, alignment=ARIGHT | ABOTTOM)
footer_frame.setLayout(footer_layout)
diff --git a/src/buildmanager.py b/src/buildmanager.py
index 6ea0b2c..64509c0 100644
--- a/src/buildmanager.py
+++ b/src/buildmanager.py
@@ -40,7 +40,7 @@ def __init__(self):
self.sci_consoles: list[ItemButton] = [None] * 5
self.sci_consoles_label: QLabel = None
self.sec_def: list[ItemButton] = [None]
- self.sec_def_label: list[ItemButton] = [None]
+ self.sec_def_label: QLabel = [None]
self.shield: list[ItemButton] = [None]
self.starship_traits: list[ItemButton] = [None]
self.tac_consoles: list[ItemButton] = [None] * 5
@@ -168,6 +168,26 @@ def autosave(self):
def __getitem__(self, key: str):
return self._build_data[key]
+ def set(
+ self, category: str, key: str, subkey: int = -1, value: str | dict = '',
+ autosave: bool = True):
+ """
+ Sets build data for situations in which assignment is not possible.
+
+ Parameters:
+ - :param category: build category, e.g. `space`, `ground`, ...
+ - :param key: build key, e.g. `aft_weapons`, `armor`, ...
+ - :param subkey: build subkey, i.e. index of item under build key
+ - :param value: data to be set
+ - :param autosave: set to `False` to prevent autosaving
+ """
+ if subkey == -1:
+ self._build_data[category][key] = value
+ else:
+ self._build_data[category][key][subkey] = value
+ if autosave:
+ self.autosave()
+
def load_build(self):
"""
Updates UI to show the build currently in self._build_data
diff --git a/src/splash.py b/src/splash.py
index 1cf087e..86e279a 100644
--- a/src/splash.py
+++ b/src/splash.py
@@ -1,3 +1,18 @@
+from PySide6.QtCore import QObject, Signal
+from PySide6.QtWidgets import QLabel, QTabWidget
+
+
+class SplashScreen(QObject):
+ """Manages splash screen"""
+
+ show_splash: Signal = Signal(bool)
+
+ def __init__(self):
+ super().__init__()
+ self.loading_label: QLabel
+ self.tabber: QTabWidget
+
+
def enter_splash(self):
"""
Shows splash screen
diff --git a/src/textedit.py b/src/textedit.py
index 47cd7c1..858662d 100644
--- a/src/textedit.py
+++ b/src/textedit.py
@@ -71,7 +71,8 @@ def add_equipment_tooltip_header__new(
def format_skill_tooltip(
- self, skill_name: str, skill_data: dict, node_index: int, environment: str) -> str:
+ skill_name: str, skill_data: dict, node_index: int, environment: str,
+ tooltip_styles: TooltipCSS) -> str:
"""
Formats skill tooltip
@@ -80,7 +81,10 @@ def format_skill_tooltip(
- :param skill_data: contains skill details
- :param node_index: index of the node within the skill group
- :param environment: "space" / "ground"
+ - :param tooltip_styles: used to style the tooltip
"""
+ head_style = f"{tooltip_styles.equipment_name}color:#ffd700;"
+ subhead_style = f"{tooltip_styles.equipment_type_subheader}color:#ffd700;"
if environment == 'space':
if skill_data['grouping'] == 'column':
prefix = SKILL_PREFIXES[node_index]
@@ -94,19 +98,15 @@ def format_skill_tooltip(
else:
prefix = ''
global_description = skill_data['gdesc'][node_index]
- head_style = f"{self.theme['tooltip']['equipment_name']}color:#ffd700;"
- subhead_style = f"{self.theme['tooltip']['equipment_type_subheader']}color:#ffd700;"
return (
- f"{prefix}{skill_name}
"
- f"{CAREER_ABBR[skill_data['career']]} {environment.capitalize()} Skill
"
- f"{global_description}
{skill_data['nodes'][node_index]['desc']}
")
+ f"{prefix}{skill_name}
"
+ f"{CAREER_ABBR[skill_data['career']]} {environment.capitalize()} Skill
"
+ f"{global_description}
{skill_data['nodes'][node_index]['desc']}
")
elif environment == 'ground':
- head_style = f"{self.theme['tooltip']['equipment_name']}color:#ffd700;"
- subhead_style = f"{self.theme['tooltip']['equipment_type_subheader']}color:#ffd700;"
return (
- f"{skill_name}
"
- f"Ground Skill
"
- f"{skill_data['gdesc']}
{skill_data['nodes'][node_index]['desc']}
")
+ f"{skill_name}
"
+ f"Ground Skill
"
+ f"{skill_data['gdesc']}
{skill_data['nodes'][node_index]['desc']}
")
def get_skill_unlock_tooltip_ground(self, unlock_id: int, unlock_choice: int):
@@ -245,6 +245,7 @@ def create_equipment_tooltip(
f"{parse_wikitext(dewikify(item[f'text{i}']), tags)}")
return tooltip
+
def create_equipment_tooltip__new(item: dict, tooltip_style: TooltipCSS) -> str:
"""
Creates tooltip for equipment from raw item data.
@@ -306,6 +307,7 @@ def create_trait_tooltip(
tooltip = ''
return tooltip
+
def create_trait_tooltip__new(
name: str, description: str, type_: str, environment: str,
styles: TooltipCSS) -> str:
diff --git a/src/widgetbuilder.py b/src/widgetbuilder.py
index c461a26..d92bccd 100644
--- a/src/widgetbuilder.py
+++ b/src/widgetbuilder.py
@@ -3,20 +3,10 @@
from PySide6.QtCore import Qt
from PySide6.QtGui import QValidator
from PySide6.QtWidgets import (
- QCheckBox, QComboBox, QCompleter, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit,
- QPushButton, QSizePolicy, QSlider, QVBoxLayout)
-
-from .callbacks import (
- boff_label_callback_ground, boff_profession_callback_space, doff_spec_callback,
- doff_variant_callback, picker, skill_callback_ground, skill_callback_space,
- skill_unlock_callback)
-from .constants import (
- ABOTTOM, ACENTER, AHCENTER, ALEFT, ATOP, AVCENTER, CALLABLE, CAREERS, GROUND_BOFF_SPECS,
- SMAXMAX, SMAXMIN, SMINMAX)
-from .style import get_style, get_style_class, merge_style, theme_font
-from .textedit import format_skill_tooltip
+ QCheckBox, QComboBox, QCompleter, QFrame, QLabel, QLineEdit, QPushButton, QSizePolicy, QSlider)
+from .constants import ACENTER, ATOP, AVCENTER, CALLABLE, SMAXMAX, SMAXMIN, SMINMAX
from .theme import AppTheme
-from .widgets import DoffCombobox, GridLayout, HBoxLayout, ItemButton, TooltipLabel, VBoxLayout
+from .widgets import HBoxLayout, ItemButton, VBoxLayout
def create_frame2(
@@ -27,8 +17,9 @@ def create_frame2(
Parameters:
- :param theme: reference to AppTheme
- - :param style: style dict to override default style (optional)
- - :param size_policy: size policy of the frame (optional)
+ - :param style: key for theme, determines style preset
+ - :param style_override: style dict to override preset style
+ - :param size_policy: size policy of the frame
:return: configured QFrame
"""
@@ -45,8 +36,8 @@ def create_label2(theme: AppTheme, text: str, style: str = 'label', style_overri
Parameters:
- :param theme: reference to AppTheme
- :param text: text to be shown on the label
- - :param style: name of the style as in self.theme
- - :param style_override: style dict to override default style (optional)
+ - :param style: key for theme, determines style preset
+ - :param style_override: style dict to override preset style
:return: configured QLabel
"""
@@ -80,12 +71,12 @@ def create_button_series2(
be a normal button; the bool value indicates the default state of the button
- "stretch": stretch value for the button
- "align": alignment flag for button
- - :param style: key for AppTheme -> default style
+ - :param style: key for theme, determines style preset
- :param shape: row / column
- :param separator: string seperator displayed between buttons (optional)
- :param ret: set to true to return list of created buttons along with layout
- :return: populated QVBoxlayout / QHBoxlayout
+ :return: populated VBoxlayout / HBoxlayout
"""
if 'default' in buttons:
defaults = theme.merge_style(theme[style], buttons.pop('default'))
@@ -109,7 +100,7 @@ def create_button_series2(
else:
button_style = defaults
toggle_button = detail['toggle'] if 'toggle' in detail else None
- bt = create_button(theme, name, style, button_style, toggle_button)
+ bt = create_button2(theme, name, style, button_style, toggle_button)
if 'callback' in detail and isinstance(detail['callback'], CALLABLE):
if toggle_button:
bt.clicked[bool].connect(detail['callback'])
@@ -122,7 +113,7 @@ def create_button_series2(
layout.addWidget(bt, stretch)
button_list.append(bt)
if separator != '' and i < (len(buttons) - 1):
- sep_label = create_label(theme, separator, 'label', sep_style)
+ sep_label = create_label2(theme, separator, 'label', sep_style)
sep_label.setSizePolicy(SMAXMIN)
layout.addWidget(sep_label, alignment=ACENTER)
@@ -141,8 +132,8 @@ def create_button2(
Parameters:
- :param theme: reference to AppTheme
- :param text: text to be shown on the button
- - :param style: name of the style as in self.theme or style dict
- - :param style_override: style dict to override default style (optional)
+ - :param style: key for theme, determines style preset
+ - :param style_override: style dict to override preset style
- :param toggle: True or False when button should be a toggle button, None when it should be a \
normal button; the bool value indicates the default state of the button
@@ -190,10 +181,10 @@ def create_combo_box2(
Parameters:
- :param theme: reference to AppTheme
- - :param style: key for self.theme -> default style
+ - :param style: key for theme, determines style preset
- :param editable: set to True to make combobox editable
- :param size_policy: size policy for combobox
- - :param style_override: style dict to override default style
+ - :param style_override: style dict to override preset style
- :param class_: custom constructor for combobox; must be QCombobox or subclass
:return: styled QCombobox
@@ -231,8 +222,8 @@ def create_entry2(
- :param theme: reference to AppTheme
- :param default_value: default value for the entry
- :param validator: validator to validate entered characters against
- - :param style: key for self.theme -> default style
- - :param style_override: style dict to override default style
+ - :param style: key for theme, determines style preset
+ - :param style_override: style dict to override preset style
- :param placeholder: placeholder shown when entry is empty
:return: styled QLineEdit
@@ -250,587 +241,36 @@ def create_entry2(
return entry
-def create_frame(self, style='frame', style_override={}, size_policy=None) -> QFrame:
- """
- Creates a frame with default styling and parent
-
- Parameters:
- - :param style: style dict to override default style (optional)
- - :param size_policy: size policy of the frame (optional)
-
- :return: configured QFrame
- """
- frame = QFrame()
- frame.setStyleSheet(get_style(self, style, style_override))
- frame.setSizePolicy(size_policy if isinstance(size_policy, QSizePolicy) else SMAXMAX)
- return frame
-
-
-def create_label(self, text, style: str = 'label', style_override={}):
- """
- Creates a label according to style with parent.
-
- Parameters:
- - :param text: text to be shown on the label
- - :param style: name of the style as in self.theme
- - :param style_override: style dict to override default style (optional)
-
- :return: configured QLabel
- """
- label = QLabel()
- label.setText(text)
- label.setStyleSheet(get_style(self, style, style_override))
- label.setSizePolicy(SMAXMAX)
- if 'font' in style_override:
- label.setFont(theme_font(self, style, style_override['font']))
- else:
- label.setFont(theme_font(self, style))
- return label
-
-
-def create_button(self, text: str, style: str = 'button', style_override={}, toggle=None):
- """
- Creates a button according to style with parent.
-
- Parameters:
- - :param text: text to be shown on the button
- - :param style: name of the style as in self.theme or style dict
- - :param style_override: style dict to override default style (optional)
- - :param toggle: True or False when button should be a toggle button, None when it should be a
- normal button; the bool value indicates the default state of the button
-
- :return: configured QPushButton
- """
- button = QPushButton(text)
- button.setStyleSheet(get_style_class(self, 'QPushButton', style, style_override))
- if 'font' in style_override:
- button.setFont(theme_font(self, style, style_override['font']))
- else:
- button.setFont(theme_font(self, style))
- button.setCursor(Qt.CursorShape.PointingHandCursor)
- button.setSizePolicy(SMAXMAX)
- if isinstance(toggle, bool):
- button.setCheckable(True)
- button.setChecked(toggle)
- return button
-
-
-def create_button_series(
- self, buttons: dict, style: str = 'button', shape: str = 'row', seperator: str = '',
- ret=False): # QVBoxLayout | QHBoxLayout
- """
- Creates a row / column of buttons.
-
- Parameters:
- - :param buttons: dictionary containing button details
- - key "default" contains style override for all buttons (optional)
- - all other keys represent one button, key will be the text on the button; value for the
- key contains dict with details for the specific button (all optional)
- - "callback": callable that will be called on button click
- - "style": individual style override dict
- - "toggle": True or False when button should be a toggle button, None when it should be
- a normal button; the bool value indicates the default state of the button
- - "stretch": stretch value for the button
- - "align": alignment flag for button
- - "size": SizePolicy for button
- - :param style: key for self.theme -> default style
- - :param shape: row / column
- - :param seperator: string seperator displayed between buttons (optional)
-
- :return: populated QVBoxlayout / QHBoxlayout
- """
- if 'default' in buttons:
- defaults = merge_style(self, self.theme[style], buttons.pop('default'))
- else:
- defaults = self.theme[style]
-
- if shape == 'column':
- layout = QVBoxLayout()
- else:
- shape = 'row'
- layout = QHBoxLayout()
-
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
-
- button_list = []
-
- if seperator != '':
- sep_style = {
- 'color': defaults['color'], 'margin': 0, 'padding': 0, 'background': '#00000000'}
-
- for i, (name, detail) in enumerate(buttons.items()):
- if 'style' in detail:
- button_style = merge_style(self, defaults, detail['style'])
- else:
- button_style = defaults
- toggle_button = detail['toggle'] if 'toggle' in detail else None
- bt = create_button(self, name, style, button_style, toggle_button)
- if 'size' in detail:
- bt.setSizePolicy(detail['size'])
- if 'callback' in detail and isinstance(detail['callback'], CALLABLE):
- if toggle_button:
- bt.clicked[bool].connect(detail['callback'])
- else:
- bt.clicked.connect(detail['callback'])
- stretch = detail['stretch'] if 'stretch' in detail else 0
- if 'align' in detail:
- layout.addWidget(bt, stretch, detail['align'])
- else:
- layout.addWidget(bt, stretch)
- button_list.append(bt)
- if seperator != '' and i < (len(buttons) - 1):
- sep_label = create_label(self, seperator, 'label', sep_style)
- sep_label.setSizePolicy(SMAXMIN)
- layout.addWidget(sep_label)
-
- if ret:
- return layout, button_list
- else:
- return layout
-
-
-def create_combo_box(
- self, style: str = 'combobox', editable: bool = False, size_policy: QSizePolicy = None,
- style_override: dict = {}, class_=QComboBox) -> QComboBox:
- """
- Creates a combobox with given style and returns it.
-
- Parameters:
- - :param style: key for self.theme -> default style
- - :param editable: set to True to make combobox editable
- - :param size_policy: size policy for combobox
- - :param style_override: style dict to override default style
- - :param class_: custom constructor for combobox; must be QCombobox or subclass
-
- :return: styled QCombobox
- """
- combo_box = class_()
- combo_box.setStyleSheet(get_style_class(self, 'QComboBox', style, style_override))
- if 'font' in style_override:
- font = theme_font(self, style, style_override['font'])
- else:
- font = theme_font(self, style)
- combo_box.setFont(font)
- combo_box.setSizePolicy(SMINMAX if size_policy is None else size_policy)
- combo_box.setCursor(Qt.CursorShape.PointingHandCursor)
- combo_box.view().setCursor(Qt.CursorShape.PointingHandCursor)
- combo_box.setMinimumContentsLength(1)
- combo_box.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents)
- if editable:
- combo_box.setEditable(True)
- combo_box.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
- combo_box.completer().setFilterMode(Qt.MatchFlag.MatchContains)
- combo_box.completer().setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
- combo_box.completer().popup().setStyleSheet(get_style_class(self, 'QListView', 'popup'))
- combo_box.completer().popup().setFont(font)
- combo_box.lineEdit().setFont(font)
- return combo_box
-
-
-def create_entry(
- self, default_value='', validator=None, style: str = 'entry',
- style_override: dict = {}, placeholder='') -> QLineEdit:
- """
- Creates an entry widget and styles it.
-
- Parameters:
- - :param default_value: default value for the entry
- - :param validator: validator to validate entered characters against
- - :param style: key for self.theme -> default style
- - :param style_override: style dict to override default style
- - :param placeholder: placeholder shown when entry is empty
-
- :return: styled QLineEdit
- """
- entry = QLineEdit(default_value)
- entry.setValidator(validator)
- entry.setPlaceholderText(placeholder)
- entry.setStyleSheet(get_style_class(self, 'QLineEdit', style, style_override))
- if 'font' in style_override:
- entry.setFont(theme_font(self, style, style_override['font']))
- else:
- entry.setFont(theme_font(self, style))
- entry.setCursor(Qt.CursorShape.IBeamCursor)
- entry.setSizePolicy(SMAXMAX)
- return entry
-
-
-def create_checkbox(self, style: str = 'checkbox', style_override: dict = {}) -> QCheckBox:
+def create_checkbox2(
+ theme: AppTheme, style: str = 'checkbox', style_override: dict = {}) -> QCheckBox:
"""
Creates checkbox and styles it.
Parameters:
- - :param style: key for self.theme -> default style
- - :param style_override: style dict to override default style
+ - :param theme: reference to AppTheme
+ - :param style: key for theme, determines style preset
+ - :param style_override: style dict to override preset style
"""
checkbox = QCheckBox()
- checkbox.setStyleSheet(get_style_class(self, 'QCheckBox', style, style_override))
+ checkbox.setStyleSheet(theme.get_style_class('QCheckBox', style, style_override))
return checkbox
-def create_item_button(self, style_override: dict = {}) -> ItemButton:
- """
- Creates Item Button.
- """
- label = create_label(self, '', 'infobox')
- frame = create_frame(self, 'infobox_frame')
- margin = self.theme['defaults']['csp'] * self.config.ui_scale
- layout = VBoxLayout(margin)
- layout.addWidget(label, alignment=ATOP)
- frame.setLayout(layout)
- button = ItemButton(
- self.box_width, self.box_height, self.theme['item'], label, frame,
- margin + self.theme['defaults']['bw'] * self.config.ui_scale)
- return button
-
-
-def create_build_section(
- self, label_text: str, button_count: int, environment: bool, build_key: str,
- is_equipment: bool = False, label_store: str = '') -> QGridLayout:
- """
- Creates a block of item buttons below a label.
-
- Parameters:
- - :param label_text: text to be displayed above the buttons
- - :param button_count: number of buttons to be created
- - :param environment: "space" or "ground"
- - :param build_key: key for self.build['space'/'ground']
- - :param is_equipment: True when items are equipment, False if items are abilities or traits
- - :param label_store: stores category label in self.widgets.build[`label_store`] if set
- """
- layout = QGridLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(self.theme['defaults']['margin'] * self.config.ui_scale)
- label = create_label(self, label_text, style_override={'margin': (0, 0, 6, 0)})
- label_size_policy = label.sizePolicy()
- label_size_policy.setRetainSizeWhenHidden(True)
- label.setSizePolicy(label_size_policy)
- layout.addWidget(label, 0, 0, 1, button_count, alignment=ALEFT)
- widget_storage = self.widgets.build[environment]
- if label_store != '':
- widget_storage[label_store] = label
- for i in range(button_count):
- button = create_item_button(self)
- button.clicked.connect(lambda subkey=i, bt=button: picker(
- self, environment, build_key, subkey, bt, is_equipment))
- button.rightclicked.connect(
- lambda e, i=i: self.context_menu.invoke(e, build_key, i, environment))
- widget_storage[build_key][i] = button
- layout.addWidget(button, 1, i, alignment=ALEFT)
- return layout
-
-
-def create_boff_station_space(
- self, profession: str, specialization: str = '', boff_id: int = 0) -> QGridLayout:
- """
- Creates a block of item buttons with label / Combobox representing boff station.
-
- Parameters:
- - :param profession: "Tactical", "Science", "Engineering" or "Universal"
- - :param specialization: specialization of the seat; None if it has no specialization
- - :param boff_id: identifies the boff station
- """
- layout = QGridLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(self.theme['defaults']['margin'] * self.config.ui_scale)
- layout.setColumnStretch(3, 1)
- if specialization != '':
- specialization = f' / {specialization}'
- if profession == 'Universal':
- label_options = (
- f'Tactical{specialization}',
- f'Science{specialization}',
- f'Engineering{specialization}'
- )
- else:
- label_options = (profession + specialization,)
- widget_storage = self.widgets.build['space']
- label_layout = HBoxLayout(spacing=self.config.ui_scale * 3)
- icon_label = TooltipLabel('', create_label(self, '', 'label_tooltip'))
- widget_storage['boff_label_icons'][boff_id] = icon_label
- label_layout.addWidget(icon_label, alignment=ALEFT)
- icon_label.hide()
- label = create_combo_box(self, size_policy=SMAXMAX, style_override=self.theme['boff_combo'])
- label.currentTextChanged.connect(lambda new: boff_profession_callback_space(self, boff_id, new))
- label.addItems(label_options)
- label_size_policy = label.sizePolicy()
- label_size_policy.setRetainSizeWhenHidden(True)
- label.setSizePolicy(label_size_policy)
- widget_storage['boff_labels'][boff_id] = label
- label_layout.addWidget(label, alignment=ALEFT)
- layout.addLayout(label_layout, 0, 0, 1, 4, alignment=ALEFT)
- for i in range(4):
- button = create_item_button(self)
- button.sizePolicy().setRetainSizeWhenHidden(True)
- button.clicked.connect(lambda subkey=i, bt=button: picker(
- self, 'space', 'boffs', subkey, bt, boff_id=boff_id))
- button.rightclicked.connect(
- lambda e, i=i: self.context_menu.invoke(e, 'boffs', i, 'space', boff_id))
- layout.addWidget(button, 1, i, alignment=ALEFT)
- widget_storage['boffs'][boff_id][i] = button
- return layout
-
-
-def create_boff_station_ground(self, boff_id: int) -> VBoxLayout:
- """
- Creates a block of item buttons with label / Combobox representing boff station.
-
- Parameters:
- - :param boff_id: identifies the boff station
- """
- widget_storage = self.widgets.build['ground']
- m = self.theme['defaults']['margin'] * self.config.ui_scale
- layout = VBoxLayout(spacing=m)
- label_layout = HBoxLayout(spacing=m)
- label_layout.setAlignment(ALEFT)
- prof_label = create_combo_box(self, style_override=self.theme['boff_combo'])
- prof_label.currentTextChanged.connect(
- lambda new: boff_label_callback_ground(self, boff_id, 'boff_profs', new))
- prof_label.addItems(CAREERS)
- widget_storage['boff_profs'][boff_id] = prof_label
- label_layout.addWidget(prof_label)
- spec_label = create_combo_box(self, style_override=self.theme['boff_combo'])
- spec_label.currentTextChanged.connect(
- lambda new: boff_label_callback_ground(self, boff_id, 'boff_specs', new))
- spec_label.addItems(GROUND_BOFF_SPECS)
- widget_storage['boff_specs'][boff_id] = spec_label
- label_layout.addWidget(spec_label)
- layout.addLayout(label_layout)
- button_layout = HBoxLayout(spacing=m)
- button_layout.setAlignment(ALEFT)
- for i in range(4):
- button = create_item_button(self)
- button.clicked.connect(lambda subkey=i, bt=button: picker(
- self, 'ground', 'boffs', subkey, bt, boff_id=boff_id))
- button.rightclicked.connect(
- lambda e, i=i: self.context_menu.invoke(e, 'boffs', i, 'ground', boff_id))
- button_layout.addWidget(button)
- widget_storage['boffs'][boff_id][i] = button
- layout.addLayout(button_layout)
- return layout
-
-
-def create_personal_trait_section(self, environment: str) -> QGridLayout:
- """
- Creates build section for personal traits
- """
- layout = QGridLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(self.theme['defaults']['margin'] * self.config.ui_scale)
- label = create_label(self, 'Personal Traits', style_override={'margin': (0, 0, 6, 0)})
- layout.addWidget(label, 0, 0, 1, 4, alignment=ALEFT)
- widget_storage = self.widgets.build[environment]
- for row in range(3):
- for col in range(4):
- i = row * 4 + col
- button = create_item_button(self)
- button.clicked.connect(
- lambda subkey=i, bt=button: picker(self, environment, 'traits', subkey, bt))
- button.rightclicked.connect(
- lambda e, i=i: self.context_menu.invoke(e, 'traits', i, environment))
- layout.addWidget(button, row + 1, col, alignment=ALEFT)
- widget_storage['traits'][i] = button
- # Last button is for innate trait and should not be clickable
- button.setEnabled(False)
- button.set_style(self.theme['item_dark'])
- return layout
-
-
-def create_starship_trait_section(self) -> QGridLayout:
- """
- Creates build section for starship traits
- """
- layout = QGridLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(self.theme['defaults']['margin'] * self.config.ui_scale)
- label = create_label(self, 'Starship Traits', style_override={'margin': (0, 0, 6, 0)})
- label.sizePolicy().setRetainSizeWhenHidden(True)
- layout.addWidget(label, 0, 0, 1, 4, alignment=ALEFT)
- widget_storage = self.widgets.build['space']
- for col in range(5):
- button = create_item_button(self)
- button.sizePolicy().setRetainSizeWhenHidden(True)
- button.clicked.connect(lambda subkey=col, bt=button: picker(
- self, 'space', 'starship_traits', subkey, bt))
- button.rightclicked.connect(
- lambda e, i=col: self.context_menu.invoke(e, 'starship_traits', i, 'space'))
- layout.addWidget(button, 1, col, alignment=ALEFT)
- widget_storage['starship_traits'][col] = button
- for col in range(2):
- button = create_item_button(self)
- button.sizePolicy().setRetainSizeWhenHidden(True)
- button.clicked.connect(lambda subkey=col + 5, bt=button: picker(
- self, 'space', 'starship_traits', subkey, bt))
- button.rightclicked.connect(
- lambda e, i=col + 5: self.context_menu.invoke(e, 'starship_traits', i, 'space'))
- layout.addWidget(button, 2, col, alignment=ALEFT)
- widget_storage['starship_traits'][col + 5] = button
- return layout
-
-
-def create_doff_section(self, environment: str) -> GridLayout:
- """
- Creates duty officer section
- """
- spacing = self.theme['defaults']['bw'] * self.config.ui_scale
- doff_layout = GridLayout(spacing=spacing)
- doff_layout.setColumnStretch(1, 1)
- for i in range(6):
- spec_combo = create_combo_box(self, style_override=self.theme['doff_combo'])
- spec_combo.currentTextChanged.connect(
- lambda spec, i=i: doff_spec_callback(self, spec, environment, i))
- doff_layout.addWidget(spec_combo, i, 0)
- self.widgets.build[environment]['doffs_spec'][i] = spec_combo
- variant_combo = create_combo_box(
- self, style_override=self.theme['doff_combo'], class_=DoffCombobox)
- variant_combo.currentTextChanged.connect(
- lambda variant, i=i: doff_variant_callback(self, variant, environment, i))
- doff_layout.addWidget(variant_combo, i, 1)
- self.widgets.build[environment]['doffs_variant'][i] = variant_combo
- return doff_layout
-
-
-def create_skill_group_space(self, group_data: dict, id_offset: int) -> GridLayout:
- """
- Creates a skill group (3 related skill nodes) in appropriate shape
-
- Parameters:
- - :param group_data: skill group data
- - :param id_offset: index of the first skill node in self.widgets and self.build
- """
- layout = GridLayout(spacing=self.theme['defaults']['csp'] * self.config.ui_scale)
- # one skill with 3 ranks
- if group_data['grouping'] == 'column':
- for index, node in enumerate(group_data['nodes']):
- button = create_item_button(self)
- button.clicked.connect(lambda id=id_offset + index: skill_callback_space(
- self, group_data['career'], id, 'column'))
- # button.rightclicked.connect(lambda e: None)
- button.skill_image_name = node['image']
- button.tooltip = format_skill_tooltip(
- self, group_data['skill'], group_data, index, 'space')
- self.widgets.build['space_skills'][group_data['career']][id_offset + index] = button
- layout.addWidget(button, index, 0)
- # == 'pair+1': one skill with 2 ranks and one sub-skill with 1 rank
- # == 'separate': 3 separate skills
- else:
- button = create_item_button(self)
- button.clicked.connect(lambda id=id_offset: skill_callback_space(
- self, group_data['career'], id, group_data['grouping']))
- # button.rightclicked.connect(lambda e: None)
- button.skill_image_name = group_data['nodes'][0]['image']
- button.tooltip = format_skill_tooltip(
- self, group_data['skill'][0], group_data, 0, 'space')
- layout.addWidget(button, 0, 0, 1, 2, alignment=AHCENTER | ABOTTOM)
- self.widgets.build['space_skills'][group_data['career']][id_offset] = button
- button = create_item_button(self)
- button.clicked.connect(lambda id=id_offset + 1: skill_callback_space(
- self, group_data['career'], id, group_data['grouping']))
- # button.rightclicked.connect(lambda e: None)
- button.skill_image_name = group_data['nodes'][1]['image']
- button.tooltip = format_skill_tooltip(
- self, group_data['skill'][1], group_data, 1, 'space')
- layout.addWidget(button, 1, 0, alignment=ATOP)
- self.widgets.build['space_skills'][group_data['career']][id_offset + 1] = button
- button = create_item_button(self)
- button.clicked.connect(lambda id=id_offset + 2: skill_callback_space(
- self, group_data['career'], id, group_data['grouping']))
- # button.rightclicked.connect(lambda e: None)
- button.skill_image_name = group_data['nodes'][2]['image']
- button.tooltip = format_skill_tooltip(
- self, group_data['skill'][2], group_data, 2, 'space')
- layout.addWidget(button, 1, 1, alignment=ATOP)
- self.widgets.build['space_skills'][group_data['career']][id_offset + 2] = button
- return layout
-
-
-def create_skill_button_ground(self, group_data: dict, id: int, node_id: int) -> ItemButton:
- """
- Creates ground skill button and returns it
-
- Parameters:
- - :param group_data: skill group data
- - :param id: index of the skill node in self.widgets and self.build
- - :param node_id: 0 or 1 for first or second node
- """
- button = create_item_button(self)
- button.clicked.connect(lambda: skill_callback_ground(self, group_data['tree'], id))
- # button.rightclicked.connect(lambda e: None)
- button.skill_image_name = group_data['nodes'][node_id]['image']
- button.tooltip = format_skill_tooltip(
- self, group_data['nodes'][node_id]['name'], group_data, node_id, 'ground')
- self.widgets.build['ground_skills'][group_data['tree']][id] = button
- return button
-
-
-def create_bonus_bar_segment(
- self, bar: str, index: int, style: str = 'bonus_bar',
- style_override: dict = {}) -> QPushButton:
- """
- Creates segment of bar showing the spent skill points.
-
- Parameters:
- - :param bar: identifies the bar ("tac" / "sci" / "eng" / "ground")
- - :param index: index of the segment within the bar
- - :param style: style key
- - :param style_override: overrides style specified by self.theme
- """
- seg = QPushButton()
- seg.setEnabled(False)
- seg.setCheckable(True)
- seg.setStyleSheet(get_style_class(self, 'QPushButton', style, style_override))
- seg.setFixedSize(7 * self.config.ui_scale, 17 * self.config.ui_scale)
- self.widgets.skill_bonus_bars[bar][index] = seg
- return seg
-
-
-def create_bonus_bar_space(self, career: str, layout: GridLayout, column: int):
- """
- Creates bonus bar for space career and inserts it into the given layout.
-
- Parameters:
- - :param career: "tac" / "eng" / "sci"
- - :param layout: layout to insert the bar into
- - :param column: column of the layout to use
- """
- segment_index = 0
- button_index = 0
- for row in range(29, 5, -1):
- if row % 6 == 0:
- button = create_item_button(self)
- button.clicked.connect(lambda i=button_index: skill_unlock_callback(self, career, i))
- layout.addWidget(button, row, column, alignment=AHCENTER)
- self.widgets.build['skill_unlocks'][career][button_index] = button
- button_index += 1
- else:
- segment = create_bonus_bar_segment(self, career, segment_index)
- layout.addWidget(segment, row, column, alignment=AHCENTER)
- segment_index += 1
- for row in range(5, 1, -1):
- segment = create_bonus_bar_segment(self, career, segment_index)
- layout.addWidget(segment, row, column, alignment=AHCENTER)
- segment_index += 1
- button = create_item_button(self)
- button.clicked.connect(lambda: skill_unlock_callback(self, career, 4))
- layout.addWidget(button, 1, column, alignment=AHCENTER)
- self.widgets.build['skill_unlocks'][career][4] = button
-
-
-def create_annotated_slider(
- self, default_value: int = 1, min: int = 0, max: int = 3,
+def create_annotated_slider2(
+ theme: AppTheme, default_value: int = 1, min: int = 0, max: int = 3,
style: str = 'slider', style_override_slider: dict = {}, style_override_label: dict = {},
- callback: Callable = lambda v: v) -> QHBoxLayout:
+ callback: Callable = lambda v: v) -> HBoxLayout:
"""
Creates Slider with label to display the current value.
Parameters:
+ - :param theme: reference to AppTheme
- :param default_value: start value for the slider
- :param min: lowest value of the slider
- :param max: highest value of the slider
- - :param style: key for self.theme -> default style
- - :param style_override_slider: style dict to override default style
- - :param style_override_label: style dict to override default style
+ - :param style: key for theme, determines style preset
+ - :param style_override_slider: style dict to override preset style
+ - :param style_override_label: style dict to override preset style
- :param callback: callable to be attached to the valueChanged signal of the slider; will be \
passed value the slider was moved to; must return value that the label should be set to
@@ -841,11 +281,8 @@ def label_updater(new_value):
new_text = callback(new_value)
slider_label.setText(str(new_text))
- layout = QHBoxLayout()
- layout.setContentsMargins(0, 0, 0, 3)
- layout.setSpacing(self.theme['defaults']['margin'])
- slider_label = create_label(
- self, '', style, style_override=style_override_label)
+ layout = HBoxLayout(margins=(0, 0, 0, 3), spacing=theme['defaults']['margin'])
+ slider_label = create_label2(theme, '', style, style_override=style_override_label)
layout.addWidget(slider_label, alignment=AVCENTER)
slider = QSlider(Qt.Orientation.Horizontal)
slider.setRange(min, max)
@@ -855,7 +292,8 @@ def label_updater(new_value):
slider.setTickPosition(QSlider.TickPosition.NoTicks)
slider.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
slider.setSizePolicy(SMINMAX)
- slider.setStyleSheet(get_style_class(self, 'QSlider', style, style_override_slider))
+ slider.setStyleSheet(theme.get_style_class('QSlider', style, style_override_slider))
+ slider.setFixedHeight(22)
slider.valueChanged.connect(label_updater)
layout.addWidget(slider, stretch=1, alignment=AVCENTER)
label_updater(default_value)
diff --git a/src/widgets.py b/src/widgets.py
index f6ea85c..b9be2f2 100644
--- a/src/widgets.py
+++ b/src/widgets.py
@@ -1,15 +1,29 @@
from collections import namedtuple
+from pathlib import Path
from typing import Callable
from PySide6.QtCore import QEvent, QObject, QPoint, QRect, QSize, Qt, QThread, Signal, Slot
-from PySide6.QtGui import QBrush, QColor, QCursor, QEnterEvent, QImage, QMouseEvent, QPainter, QPen
+from PySide6.QtGui import (
+ QBrush, QColor, QCursor, QEnterEvent, QImage, QMouseEvent, QPainter, QPaintEvent, QPen)
from PySide6.QtWidgets import (
- QCheckBox, QComboBox, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu,
- QPlainTextEdit, QSizePolicy, QTabWidget, QVBoxLayout, QWidget)
+ QCheckBox, QComboBox, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPlainTextEdit,
+ QSizePolicy, QTabWidget, QVBoxLayout, QWidget)
from .constants import AHCENTER, ATOP, EQUIPMENT_TYPES, SMINMIN
+class Tabbers():
+ """Manages tabbers"""
+
+ def __init__(self):
+ self.build_tabber: QTabWidget
+ self.build_frames: list[QFrame] = list()
+ self.sidebar_tabber: QTabWidget
+ self.sidebar_frames: list[QFrame] = list()
+ self.character_tabber: QTabWidget
+ self.character_frames: list[QFrame] = list()
+
+
class WidgetStorage():
"""
Stores Widgets
@@ -227,10 +241,10 @@ class ImageLabel(QWidget):
Label displaying image that resizes according to its parents width while preserving aspect
ratio.
"""
- def __init__(self, path: str = '', aspect_ratio: tuple[int, int] = (0, 0), *args, **kwargs):
- super().__init__(*args, **kwargs)
+ def __init__(self, path: Path | None = None, aspect_ratio: tuple[int, int] = (0, 0)):
+ super().__init__()
self._w, self._h = aspect_ratio
- if path == '':
+ if path is None:
self.p = QImage()
else:
self.p = QImage(path)
@@ -244,7 +258,7 @@ def set_image(self, p: QImage):
self._h = p.height()
self.update()
- def paintEvent(self, event):
+ def paintEvent(self, event: QPaintEvent):
if not self.p.isNull():
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)
From d943f746956f46d678e6ee6b3d2fae450f702a6c Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Mon, 11 May 2026 16:56:23 +0200
Subject: [PATCH 14/44] using custom search dir for local assets
---
src/theme.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/theme.py b/src/theme.py
index 1c2084c..2e6c34e 100644
--- a/src/theme.py
+++ b/src/theme.py
@@ -54,7 +54,7 @@ def __init__(self, tooltips: dict[str, dict[str]], scale: float):
self.trait_header: str = self.get_tooltip_css(tooltips['trait_header'], scale)
self.trait_subheader: str = self.get_tooltip_css(tooltips['trait_subheader'], scale)
self.ul: str = self.get_tooltip_css(tooltips['ul'], scale)
-
+
def get_tooltip_css(self, style_data: dict[str], scale: float):
"""
Converts dictionary containing tooltip style to css
@@ -89,7 +89,7 @@ def __init__(self, scale: float, theme_tree: dict[str] = {}, theme_options: dict
if len(theme_tree) > 0:
self._theme_data: dict[str, dict] = theme_tree
else:
- self._theme_data: dict[str, dict] = self.get_default_theme()
+ self._theme_data: dict[str, dict] = self.get_default_theme()
self.tooltips: TooltipCSS = TooltipCSS(self._theme_data['tooltip_def'], scale)
def __getitem__(self, key: str):
@@ -430,10 +430,10 @@ def get_default_theme(self) -> dict[str, dict]:
'border-color': '@sets'
},
'::indicator:checked': {
- 'image': 'url(local/check.svg)'
+ 'image': 'url(local_folder:check.svg)'
},
'::indicator:unchecked': {
- 'image': 'url(local/uncheck.svg)',
+ 'image': 'url(local_folder:uncheck.svg)',
}
},
# holds sub-pages
@@ -463,7 +463,7 @@ def get_default_theme(self) -> dict[str, dict]:
'color': '@fg',
'font': '@subhead',
'::down-arrow': {
- 'image': 'url(local/thick-chevron-down.svg)',
+ 'image': 'url(local_folder:thick-chevron-down.svg)',
'width': '@margin',
},
'::drop-down': {
From cbd3633f6aaf0ee3318e0349c6c3f3657b1b95a5 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Mon, 11 May 2026 17:08:29 +0200
Subject: [PATCH 15/44] moving tab logic to Tabbers class
---
src/app.py | 12 ++++++------
src/callbacks.py | 29 +----------------------------
src/widgets.py | 21 +++++++++++++++++++++
3 files changed, 28 insertions(+), 34 deletions(-)
diff --git a/src/app.py b/src/app.py
index a2b1e54..b0e73a9 100644
--- a/src/app.py
+++ b/src/app.py
@@ -40,7 +40,7 @@ class SETS():
clear_all, clear_build_callback, elite_callback, faction_combo_callback,
load_build_callback, load_skills_callback, save_build_callback, save_skills_callback,
select_ship, set_build_item, ship_info_callback,
- skill_unlock_callback, spec_combo_callback, species_combo_callback, switch_main_tab,
+ skill_unlock_callback, spec_combo_callback, species_combo_callback,
tier_callback)
from .datafunctions import (
autosave, backup_cargo_data, empty_build,
@@ -318,15 +318,15 @@ def setup_main_layout(self):
create_button_series2(self.theme2, left_button_group), 0, 0, alignment=ALEFT | ATOP)
center_button_group = {
'default': {'font': ('Overpass', 16, 'medium')},
- 'SPACE': {'callback': lambda: self.switch_main_tab(0), 'stretch': 1, 'size': SMINMAX},
- 'GROUND': {'callback': lambda: self.switch_main_tab(1), 'stretch': 1, 'size': SMINMAX},
+ 'SPACE': {'callback': lambda: self.tabbers.switch(0), 'stretch': 1, 'size': SMINMAX},
+ 'GROUND': {'callback': lambda: self.tabbers.switch(1), 'stretch': 1, 'size': SMINMAX},
'SPACE SKILLS': {
- 'callback': lambda: self.switch_main_tab(2),
+ 'callback': lambda: self.tabbers.switch(2),
'stretch': 1,
'size': SMINMAX
},
'GROUND SKILLS': {
- 'callback': lambda: self.switch_main_tab(3),
+ 'callback': lambda: self.tabbers.switch(3),
'stretch': 1,
'size': SMINMAX
}
@@ -335,7 +335,7 @@ def setup_main_layout(self):
menu_layout.addLayout(center_buttons, 0, 1)
right_button_group = {
'Export': {'callback': self.export_window.invoke},
- 'Settings': {'callback': lambda: self.switch_main_tab(5)},
+ 'Settings': {'callback': lambda: self.tabbers.switch(5)},
}
menu_layout.addLayout(
create_button_series2(self.theme2, right_button_group), 0, 2, alignment=ARIGHT | ATOP)
diff --git a/src/callbacks.py b/src/callbacks.py
index c5890e9..91adf66 100644
--- a/src/callbacks.py
+++ b/src/callbacks.py
@@ -1,5 +1,3 @@
-import os
-
from .buildupdater import (
align_space_frame, clear_captain, clear_doffs, clear_ground_build, clear_ship, clear_traits,
get_variable_slot_counts, set_skill_unlock_ground, set_skill_unlock_space,
@@ -9,37 +7,12 @@
SPECIES, SPECIES_TRAITS)
from .datafunctions import (
load_build_file, load_skill_tree_file, save_build_file, save_skill_tree_file)
-from .iofunc import browse_path, get_ship_image, image, open_wiki_page
+from .iofunc import browse_path, image, open_wiki_page
from .widgets import exec_in_thread
from PySide6.QtCore import Qt
-def switch_main_tab(self, index):
- """
- Callback to switch between tabs. Switches build and both sidebar tabs.
-
- Parameters:
- - :param index: index to switch to (0: space build, 1: ground build, 2: space skills,
- 3: ground skills, 4: library, 5: settings)
- """
- CHAR_TAB_MAP = {
- 0: 0,
- 1: 0,
- 2: 0,
- 3: 0,
- 4: 1,
- 5: 2
- }
- self.widgets.build_tabber.setCurrentIndex(index)
- self.widgets.sidebar_tabber.setCurrentIndex(index)
- self.widgets.character_tabber.setCurrentIndex(CHAR_TAB_MAP[index])
- if index == 4:
- self.widgets.sidebar.setVisible(False)
- else:
- self.widgets.sidebar.setVisible(True)
-
-
def faction_combo_callback(self, new_faction: str):
"""
Saves new faction to build and changes species selector choices.
diff --git a/src/widgets.py b/src/widgets.py
index b9be2f2..980cf35 100644
--- a/src/widgets.py
+++ b/src/widgets.py
@@ -11,6 +11,15 @@
from .constants import AHCENTER, ATOP, EQUIPMENT_TYPES, SMINMIN
+CHAR_TAB_MAP = {
+ 0: 0,
+ 1: 0,
+ 2: 0,
+ 3: 0,
+ 4: 1,
+ 5: 2
+}
+
class Tabbers():
"""Manages tabbers"""
@@ -23,6 +32,18 @@ def __init__(self):
self.character_tabber: QTabWidget
self.character_frames: list[QFrame] = list()
+ def switch(self, index):
+ """
+ Callback to switch between tabs. Switches build and both sidebar tabs.
+
+ Parameters:
+ - :param index: index to switch to (0: space build, 1: ground build, 2: space skills,
+ 3: ground skills, 4: library, 5: settings)
+ """
+ self.build_tabber.setCurrentIndex(index)
+ self.sidebar_tabber.setCurrentIndex(index)
+ self.character_tabber.setCurrentIndex(CHAR_TAB_MAP[index])
+
class WidgetStorage():
"""
From 9e2f235ff4736204e52ef2a97c61bbd17c4603cb Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Tue, 12 May 2026 08:45:01 +0200
Subject: [PATCH 16/44] moving item label callbacks to buildmanager
---
src/app.py | 83 ++++-----
src/buildmanager.py | 415 +++++++++++++++++++++++++++++++++++++++++-
src/callbacks.py | 430 +-------------------------------------------
3 files changed, 456 insertions(+), 472 deletions(-)
diff --git a/src/app.py b/src/app.py
index b0e73a9..26a8515 100644
--- a/src/app.py
+++ b/src/app.py
@@ -37,11 +37,9 @@
class SETS():
from .callbacks import (
- clear_all, clear_build_callback, elite_callback, faction_combo_callback,
+ clear_all, clear_build_callback,
load_build_callback, load_skills_callback, save_build_callback, save_skills_callback,
- select_ship, set_build_item, ship_info_callback,
- skill_unlock_callback, spec_combo_callback, species_combo_callback,
- tier_callback)
+ select_ship)
from .datafunctions import (
autosave, backup_cargo_data, empty_build,
init_backend, load_legacy_build_image)
@@ -433,7 +431,7 @@ def setup_ship_frame(self):
tier_label = create_label2(self.theme2, 'Ship Tier:')
ship_layout.addWidget(tier_label, 1, 0)
tier_combo = create_combo_box2(self.theme2)
- tier_combo.currentTextChanged.connect(self.tier_callback)
+ tier_combo.currentTextChanged.connect(self.build2.tier_callback)
tier_combo.setSizePolicy(SMAXMAX)
self.build2.ship.tier = tier_combo
ship_layout.addWidget(tier_combo, 1, 1, alignment=ALEFT)
@@ -445,14 +443,14 @@ def setup_ship_frame(self):
dc_label.setSizePolicy(dc_label_size_policy)
self.build2.ship.dc = dc_label
ship_layout.addWidget(dc_label, 1, 2, alignment=ARIGHT)
- info_button = self.create_button('Ship Info', style_override={'margin': 0})
- info_button.clicked.connect(self.ship_info_callback)
+ info_button = create_button2(self.theme, 'Ship Info', style_override={'margin': 0})
+ info_button.clicked.connect(self.build2.ship_info_callback)
ship_layout.addWidget(info_button, 1, 3, alignment=ARIGHT)
name_label = create_label2(self.theme2, 'Ship Name:')
ship_layout.addWidget(name_label, 2, 0)
name_entry = create_entry2(self.theme2)
name_entry.editingFinished.connect(
- lambda: self.set_build_item(self.build['space'], 'ship_name', name_entry.text()))
+ lambda: self.build2.set('space', 'ship_name', value=name_entry.text()))
self.build2.ship.name = name_entry
name_entry.setSizePolicy(SMINMAX)
ship_layout.addWidget(name_entry, 2, 1, 1, 3)
@@ -463,8 +461,8 @@ def setup_ship_frame(self):
desc_edit.setStyleSheet(self.theme2.get_style_class('QPlainTextEdit', 'textedit'))
desc_edit.setFont(self.theme2.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
- desc_edit.textChanged.connect(lambda: self.set_build_item(
- self.build['space'], 'ship_desc', desc_edit.toPlainText(), autosave=False))
+ desc_edit.textChanged.connect(lambda: self.build2.set(
+ 'space', 'ship_desc', value=desc_edit.toPlainText(), autosave=False))
self.build2.ship.desc = desc_edit
ship_layout.addWidget(desc_edit, 4, 0, 1, 4)
ship_frame.setLayout(ship_layout)
@@ -534,8 +532,8 @@ def create_boff_station_space(
icon_label.hide()
label = create_combo_box2(
self.theme2, size_policy=SMAXMAX, style_override=self.theme['boff_combo'])
- # label.currentTextChanged.connect(
- # lambda new: boff_profession_callback_space(self, boff_id, new))
+ label.currentTextChanged.connect(
+ lambda new: self.build2.boff_profession_callback_space(boff_id, new))
label.addItems(label_options)
label_size_policy = label.sizePolicy()
label_size_policy.setRetainSizeWhenHidden(True)
@@ -567,14 +565,14 @@ def create_boff_station_ground(self, boff_id: int) -> VBoxLayout:
label_layout = HBoxLayout(spacing=m)
label_layout.setAlignment(ALEFT)
prof_label = create_combo_box2(self.theme2, style_override=self.theme['boff_combo'])
- # prof_label.currentTextChanged.connect(
- # lambda new: boff_label_callback_ground(self, boff_id, 'boff_profs', new))
+ prof_label.currentTextChanged.connect(
+ lambda new: self.build2.boff_label_callback_ground(boff_id, 'boff_profs', new))
prof_label.addItems(CAREERS)
widget_storage.boff_profs[boff_id] = prof_label
label_layout.addWidget(prof_label)
spec_label = create_combo_box2(self.theme2, style_override=self.theme['boff_combo'])
- # spec_label.currentTextChanged.connect(
- # lambda new: boff_label_callback_ground(self, boff_id, 'boff_specs', new))
+ spec_label.currentTextChanged.connect(
+ lambda new: self.build2.boff_label_callback_ground(boff_id, 'boff_specs', new))
spec_label.addItems(GROUND_BOFF_SPECS)
widget_storage['boff_specs'][boff_id] = spec_label
label_layout.addWidget(spec_label)
@@ -661,14 +659,14 @@ def create_doff_section(self, environment: str) -> GridLayout:
widget_storage = self.build2.space if environment == 'space' else self.build2.ground
for i in range(6):
spec_combo = create_combo_box2(self.theme2, style_override=self.theme['doff_combo'])
- # spec_combo.currentTextChanged.connect(
- # lambda spec, i=i: doff_spec_callback(self, spec, environment, i))
+ spec_combo.currentTextChanged.connect(
+ lambda spec, id=i: self.build2.doff_spec_callback(spec, environment, id))
doff_layout.addWidget(spec_combo, i, 0)
widget_storage.doffs_spec[i] = spec_combo
variant_combo = create_combo_box2(
self.theme2, style_override=self.theme['doff_combo'], class_=DoffCombobox)
- # variant_combo.currentTextChanged.connect(
- # lambda variant, i=i: doff_variant_callback(self, variant, environment, i))
+ variant_combo.currentTextChanged.connect(
+ lambda variant, id=i: self.build2.doff_variant_callback(variant, environment, id))
doff_layout.addWidget(variant_combo, i, 1)
widget_storage.doffs_variant[i] = variant_combo
return doff_layout
@@ -686,8 +684,9 @@ def create_skill_group_space(self, group_data: dict, id_offset: int) -> GridLayo
if group_data['grouping'] == 'column':
for index, node in enumerate(group_data['nodes']):
button = create_item_button2(self.theme2)
- # button.clicked.connect(lambda id=id_offset + index: skill_callback_space(
- # self, group_data['career'], id, 'column'))
+ skill_id = id_offset + index
+ button.clicked.connect(lambda id=skill_id: self.build2.skill_callback_space(
+ group_data['career'], id, 'column'))
button.skill_image_name = node['image']
button.tooltip = format_skill_tooltip(
group_data['skill'], group_data, index, 'space', self.theme2.tooltips)
@@ -697,24 +696,24 @@ def create_skill_group_space(self, group_data: dict, id_offset: int) -> GridLayo
# == 'separate': 3 separate skills
else:
button = create_item_button2(self.theme2)
- # button.clicked.connect(lambda id=id_offset: skill_callback_space(
- # self, group_data['career'], id, group_data['grouping']))
+ button.clicked.connect(lambda id=id_offset: self.build2.skill_callback_space(
+ group_data['career'], id, group_data['grouping']))
button.skill_image_name = group_data['nodes'][0]['image']
button.tooltip = format_skill_tooltip(
group_data['skill'][0], group_data, 0, 'space', self.theme2.tooltips)
layout.addWidget(button, 0, 0, 1, 2, alignment=AHCENTER | ABOTTOM)
self.build2.skills.space[group_data['career']][id_offset] = button
button = create_item_button2(self.theme2)
- # button.clicked.connect(lambda id=id_offset + 1: skill_callback_space(
- # self, group_data['career'], id, group_data['grouping']))
+ button.clicked.connect(lambda id=id_offset + 1: self.build2.skill_callback_space(
+ group_data['career'], id, group_data['grouping']))
button.skill_image_name = group_data['nodes'][1]['image']
button.tooltip = format_skill_tooltip(
group_data['skill'][1], group_data, 1, 'space', self.theme2.tooltips)
layout.addWidget(button, 1, 0, alignment=ATOP)
self.build2.skills.space[group_data['career']][id_offset + 1] = button
button = create_item_button2(self.theme2)
- # button.clicked.connect(lambda id=id_offset + 2: skill_callback_space(
- # self, group_data['career'], id, group_data['grouping']))
+ button.clicked.connect(lambda id=id_offset + 2: self.build2.skill_callback_space(
+ group_data['career'], id, group_data['grouping']))
button.skill_image_name = group_data['nodes'][2]['image']
button.tooltip = format_skill_tooltip(
group_data['skill'][2], group_data, 2, 'space', self.theme2.tooltips)
@@ -756,8 +755,8 @@ def create_bonus_bar_space(self, career: str, layout: GridLayout, column: int):
for row in range(29, 5, -1):
if row % 6 == 0:
button = create_item_button2(self.theme2)
- # button.clicked.connect(
- # lambda i=button_index: skill_unlock_callback(self, career, i))
+ button.clicked.connect(
+ lambda i=button_index: self.build2.skill_unlock_callback(career, i))
layout.addWidget(button, row, column, alignment=AHCENTER)
self.build2.skills.unlocks[career][button_index] = button
button_index += 1
@@ -770,7 +769,7 @@ def create_bonus_bar_space(self, career: str, layout: GridLayout, column: int):
layout.addWidget(segment, row, column, alignment=AHCENTER)
segment_index += 1
button = create_item_button2(self.theme2)
- # button.clicked.connect(lambda: skill_unlock_callback(self, career, 4))
+ button.clicked.connect(lambda: self.build2.skill_unlock_callback(self, career, 4))
layout.addWidget(button, 1, column, alignment=AHCENTER)
self.build2.skills.unlocks[career][4] = button
@@ -784,7 +783,7 @@ def create_skill_button_ground(self, group_data: dict, id: int, node_id: int) ->
- :param node_id: 0 or 1 for first or second node
"""
button = create_item_button2(self.theme2)
- # button.clicked.connect(lambda: skill_callback_ground(self, group_data['tree'], id))
+ button.clicked.connect(lambda: self.build2.skill_callback_ground(group_data['tree'], id))
button.skill_image_name = group_data['nodes'][node_id]['image']
button.tooltip = format_skill_tooltip(
group_data['nodes'][node_id]['name'], group_data, node_id, 'ground',
@@ -1017,17 +1016,17 @@ def setup_character_frame(self, frame: QFrame):
'background-color': '@sets', 'margin': '@isp'})
seperator.setFixedHeight(self.theme2['defaults']['sep'] * self.theme2.scale)
layout.addWidget(seperator, 0, 0, 1, 2, alignment=ATOP) # ATOP makes it respect the margin?
- char_name = self.create_entry(placeholder='NAME')
+ char_name = create_entry2(self.theme2, placeholder='NAME')
char_name.setAlignment(AHCENTER)
char_name.setSizePolicy(SMINMAX)
char_name.editingFinished.connect(
- lambda: self.set_build_item(self.build['captain'], 'name', char_name.text()))
+ lambda: self.build2.set('captain', 'name', value=char_name.text()))
layout.addWidget(char_name, 1, 0, 1, 2)
self.build2.character.name = char_name
elite_label = create_label2(self.theme2, 'Elite Captain')
layout.addWidget(elite_label, 2, 0, alignment=ARIGHT)
elite_checkbox = create_checkbox2(self.theme2)
- elite_checkbox.checkStateChanged.connect(self.elite_callback)
+ elite_checkbox.checkStateChanged.connect(self.build2.elite_callback)
layout.addWidget(elite_checkbox, 2, 1, alignment=ALEFT)
self.build2.character.elite = elite_checkbox
career_label = create_label2(self.theme2, 'Captain Career')
@@ -1035,28 +1034,29 @@ def setup_character_frame(self, frame: QFrame):
career_combo = create_combo_box2(self.theme2)
career_combo.addItems({''} | CAREERS)
career_combo.currentTextChanged.connect(
- lambda t: self.set_build_item(self.build['captain'], 'career', t))
+ lambda new_career: self.build2.set('captain', 'career', value=new_career))
layout.addWidget(career_combo, 3, 1)
self.build2.character.career = career_combo
faction_label = create_label2(self.theme2, 'Faction')
layout.addWidget(faction_label, 4, 0, alignment=ARIGHT)
faction_combo = create_combo_box2(self.theme2)
faction_combo.addItems({''} | FACTIONS)
- faction_combo.currentTextChanged.connect(self.faction_combo_callback)
+ faction_combo.currentTextChanged.connect(self.build2.faction_combo_callback)
layout.addWidget(faction_combo, 4, 1)
self.build2.character.faction = faction_combo
species_label = create_label2(self.theme2, 'Species')
layout.addWidget(species_label, 5, 0, alignment=ARIGHT)
species_combo = create_combo_box2(self.theme2)
species_combo.addItems({''})
- species_combo.currentTextChanged.connect(lambda t: self.species_combo_callback(t))
+ species_combo.currentTextChanged.connect(self.build2.species_combo_callback)
layout.addWidget(species_combo, 5, 1)
self.build2.character.species = species_combo
primary_label = create_label2(self.theme2, 'Primary Spec')
layout.addWidget(primary_label, 6, 0, alignment=ARIGHT)
primary_combo = create_combo_box2(self.theme2)
primary_combo.addItems({''} | PRIMARY_SPECS)
- primary_combo.currentTextChanged.connect(lambda t: self.spec_combo_callback(True, t))
+ primary_combo.currentTextChanged.connect(
+ lambda new_spec: self.build2.spec_combo_callback(True, new_spec))
layout.addWidget(primary_combo, 6, 1)
self.build2.character.primary = primary_combo
secondary_label = create_label2(
@@ -1064,7 +1064,8 @@ def setup_character_frame(self, frame: QFrame):
layout.addWidget(secondary_label, 7, 0, alignment=ARIGHT)
secondary_combo = create_combo_box2(self.theme2)
secondary_combo.addItems({''} | PRIMARY_SPECS | SECONDARY_SPECS)
- secondary_combo.currentTextChanged.connect(lambda t: self.spec_combo_callback(False, t))
+ secondary_combo.currentTextChanged.connect(
+ lambda new_spec: self.build2.spec_combo_callback(False, new_spec))
layout.addWidget(secondary_combo, 7, 1)
self.build2.character.secondary = secondary_combo
frame.setLayout(layout)
@@ -1258,7 +1259,7 @@ def setup_ground_skill_frame(self):
seg2 = self.create_bonus_bar_segment('ground', i * 2 + 1)
bonus_bar_layout.addWidget(seg2, row - 1, 1, alignment=AHCENTER)
button = create_item_button2(self.theme2)
- # button.clicked.connect(lambda i=i: self.skill_unlock_callback('ground', i))
+ button.clicked.connect(lambda i=i: self.build2.skill_unlock_callback('ground', i))
bonus_bar_layout.addWidget(button, row - 2, 1, alignment=AHCENTER)
self.build2.skills.unlocks['ground'][i] = button
row -= 3
diff --git a/src/buildmanager.py b/src/buildmanager.py
index 64509c0..0fb3412 100644
--- a/src/buildmanager.py
+++ b/src/buildmanager.py
@@ -5,9 +5,10 @@
from .buildhelpers import get_boff_spec, get_variable_slot_counts, empty_build
from .cargomanager import CargoManager
-from .constants import SHIP_TEMPLATE
+from .constants import (
+ PRIMARY_SPECS, SECONDARY_SPECS, SHIP_TEMPLATE, SKILL_POINTS_FOR_RANK, SPECIES, SPECIES_TRAITS)
from .imagemanager import ImageManager
-from .iofunc import store_json__new
+from .iofunc import open_wiki_page, store_json__new
from .textedit import add_equipment_tooltip_header__new, get_ultimate_skill_unlock_tooltip__new
from .theme import TooltipCSS
from .widgets import ItemButton, ItemSlot, ShipButton, ShipImage, Thread, TooltipLabel
@@ -759,3 +760,413 @@ def set_skill_unlock_ground(self, id: int, state: int | None):
self._build_data['skill_unlocks']['ground'][id] = state
if not self._building:
unlock_button.force_tooltip_update()
+
+ def faction_combo_callback(self, new_faction: str):
+ """
+ Saves new faction and changes species selector choices.
+
+ Parameters:
+ - :param new_faction: name of the new faction
+ """
+ self._build_data['captain']['faction'] = new_faction
+ self.character.species.clear()
+ if new_faction != '':
+ self.character.species.addItems(('', *SPECIES[new_faction]))
+ self._build_data['captain']['species'] = ''
+ self.autosave()
+
+ def species_combo_callback(self, new_species: str):
+ """
+ Saves new species to build and changes species trait
+
+ Parameters:
+ - :param new_species: name of the new species
+ """
+ self._build_data['captain']['species'] = new_species
+ if new_species == 'Alien':
+ if not self._building:
+ self._build_data['space']['traits'][10] = ''
+ self._build_data['ground']['traits'][10] = ''
+ self._build_data['space']['traits'][11] = ''
+ self._build_data['ground']['traits'][11] = ''
+ self.space.traits[10].show()
+ self.ground.traits[10].show()
+ self.space.traits[11].clear()
+ self.ground.traits[11].clear()
+ else:
+ self.space.traits[10].hide()
+ self.ground.traits[10].hide()
+ self.space.traits[10].clear()
+ self.ground.traits[10].clear()
+ self._build_data['space']['traits'][10] = None
+ self._build_data['ground']['traits'][10] = None
+ new_space_trait = SPECIES_TRAITS['space'].get(new_species, '')
+ new_ground_trait = SPECIES_TRAITS['ground'].get(new_species, '')
+ if new_space_trait == '':
+ self.space.traits[11].clear()
+ self._build_data['space']['traits'][11] = ''
+ else:
+ self.slot_trait_item({'item': new_space_trait}, 'space', 'traits', 11)
+ if new_ground_trait == '':
+ self.ground.traits[11].clear()
+ self._build_data['ground']['traits'][11] = ''
+ else:
+ self.slot_trait_item({'item': new_ground_trait}, 'ground', 'traits', 11)
+ self.autosave()
+
+ def spec_combo_callback(self, primary: bool, new_spec: str):
+ """
+ Saves new spec to build and adjusts choices in other spec combo box.
+
+ Parameters:
+ - :param primary: `True` when editing primary spec, `False` when editing secondary spec
+ - :param new_spec: name of the new specialization
+ """
+ if primary:
+ self._build_data['captain']['primary_spec'] = new_spec
+ secondary_combo = self.character.secondary
+ secondary_specs = set()
+ remove_index = None
+ for i in range(secondary_combo.count()):
+ secondary_specs.add(secondary_combo.itemText(i))
+ if secondary_combo.itemText(i) == new_spec and new_spec != '':
+ remove_index = i
+ if remove_index is not None:
+ secondary_combo.removeItem(remove_index)
+ secondary_combo.addItems((PRIMARY_SPECS | SECONDARY_SPECS) - secondary_specs)
+ else:
+ self._build_data['captain']['secondary_spec'] = new_spec
+ primary_combo = self.character.primary
+ primary_specs = set()
+ remove_index = None
+ for i in range(primary_combo.count()):
+ primary_specs.add(primary_combo.itemText(i))
+ if primary_combo.itemText(i) == new_spec and new_spec != '':
+ remove_index = i
+ if remove_index is not None:
+ primary_combo.removeItem(remove_index)
+ primary_combo.addItems(PRIMARY_SPECS - primary_specs)
+ self.autosave()
+
+ def elite_callback(self, state: Qt.CheckState):
+ """
+ Saves new state and updates build.
+
+ Parameters:
+ - :param state: new state of the checkbox
+ """
+ if state == Qt.CheckState.Checked:
+ if not self._building:
+ self._build_data['captain']['elite'] = True
+ self._build_data['space']['traits'][9] = ''
+ self._build_data['ground']['traits'][9] = ''
+ self._build_data['ground']['kit_modules'][5] = ''
+ self._build_data['ground']['ground_devices'][4] = ''
+ self.space.traits[9].show()
+ self.ground.traits[9].show()
+ self.ground.kit_modules[5].show()
+ self.ground.ground_devices[4].show()
+ else:
+ if not self.building:
+ self._build_data['captain']['elite'] = False
+ self._build_data['space']['traits'][9] = None
+ self._build_data['ground']['traits'][9] = None
+ self._build_data['ground']['kit_modules'][5] = None
+ self._build_data['ground']['ground_devices'][4] = None
+ self.space.traits[9].hide()
+ self.space.traits[9].clear()
+ self.ground.traits[9].hide()
+ self.ground.traits[9].clear()
+ self.ground.kit_modules[5].hide()
+ self.ground.kit_modules[5].clear()
+ self.ground.ground_devices[4].hide()
+ self.ground.ground_devices[4].clear()
+ self.autosave()
+
+ def boff_profession_callback_space(self, boff_id: int, new_spec: str):
+ """
+ updates build with newly assigned profession; clears abilities of the old profession
+
+ Parameters:
+ - :param boff_id: identifies the boff station
+ - :param new_spec: new profession and specialization
+ """
+ if self._building:
+ return
+ if ' / ' in new_spec:
+ profession, specialization = new_spec.split(' / ')
+ if specialization == 'Temporal Operative':
+ specialization = 'Temporal'
+ # Lt. Commander rank contains all abilities
+ all_abilities = self._cache.boff_abilities['space'][specialization][2]
+ for ability_num, ability in enumerate(self._build_data['space']['boffs'][boff_id]):
+ if ability is not None and ability != '' and ability['item'] not in all_abilities:
+ self._build_data['space']['boffs'][boff_id][ability_num] = ''
+ self.space.boffs[boff_id][ability_num].clear()
+ else:
+ profession = new_spec
+ specialization = ''
+ for ability_num, ability in enumerate(self._build_data['space']['boffs'][boff_id]):
+ if ability is not None and ability != '':
+ self._build_data['space']['boffs'][boff_id][ability_num] = ''
+ self.space.boffs[boff_id][ability_num].clear()
+ self._build_data['space']['boff_specs'][boff_id] = [profession, specialization]
+ self.autosave()
+
+ def boff_label_callback_ground(self, boff_id: int, type_: str, new_text: str):
+ """
+ updates build with newly assigned profession or specialization; clears invalid abilities
+
+ Parameters:
+ - :param boff_id: number of the boff station
+ - :param type_: "boff_profs" / "boff_specs"
+ - :param new_text: new profession / specialization
+ """
+ if self._building:
+ return
+ self._build_data['ground'][type_][boff_id] = new_text
+ other_type = 'boff_profs' if type_ == 'boff_specs' else 'boff_specs'
+ other_text = self._build_data['ground'][other_type][boff_id]
+ ground_abilities = self._cache.boff_abilities['ground']
+ for ability_num, ability in enumerate(self._build_data['ground']['boffs'][boff_id]):
+ if ability is not None and ability != '':
+ # Lt. Commander and Commander rank combined contain all abilities
+ if (ability['item'] not in ground_abilities[new_text][2]
+ and ability['item'] not in ground_abilities[new_text][3]
+ and ability['item'] not in ground_abilities[other_text][2]
+ and ability['item'] not in ground_abilities[other_text][3]):
+ self._build_data['ground']['boffs'][boff_id][ability_num] = ''
+ self.ground.boffs[boff_id][ability_num].clear()
+ self.autosave()
+
+ def tier_callback(self, new_tier: str):
+ """
+ Updates build according to new tier
+ """
+ if self._building:
+ return
+ self._build_data['space']['tier'] = new_tier
+ ship_name = self._build_data['space']['ship']
+ if ship_name == '':
+ ship_data = SHIP_TEMPLATE
+ else:
+ ship_data = self._cache.ships[ship_name]
+ uni, eng, sci, tac, devices, starship_traits = get_variable_slot_counts(ship_data, new_tier)
+ self.update_equipment_cat('uni_consoles', uni, can_hide=True)
+ self.update_equipment_cat('eng_consoles', eng)
+ self.update_equipment_cat('sci_consoles', sci)
+ self.update_equipment_cat('tac_consoles', tac)
+ self.update_equipment_cat('devices', devices)
+ self.update_starship_traits(starship_traits)
+ self.autosave()
+
+ def ship_info_callback(self):
+ """
+ Opens wiki page of ship if ship is slotted
+ """
+ if self._build_data['space']['ship'] != '':
+ open_wiki_page(self._cache.ships[self._build_data['space']['ship']]['Page'])
+
+ def doff_spec_callback(self, new_spec: str, environment: str, doff_id: int):
+ """
+ Callback for duty officer specialization combobox.
+
+ Parameters:
+ - :param new_spec: selected specialization
+ - :param environment: "space" / "ground"
+ - :param doff_id: index of the doff
+ """
+ if self._building:
+ return
+ self._build_data[environment]['doffs_spec'][doff_id] = new_spec
+ self._build_data[environment]['doffs_variant'][doff_id] = ''
+ widget_storage = self.space if environment == 'space' else self.ground
+ widget_storage.doffs_variant[doff_id].clear()
+ if new_spec != '':
+ variants = getattr(self._cache, f'{environment}_doffs')[new_spec].keys()
+ widget_storage.doffs_variant[doff_id].addItems({''} | variants)
+ self.autosave()
+
+ def doff_variant_callback(self, new_variant: str, environment: str, doff_id: int):
+ """
+ Callback for duty officer variant combobox.
+
+ Parameters:
+ - :param new_variant: selected variant
+ - :param environment: "space" / "ground"
+ - :param doff_id: index of the doff
+ """
+ if self._building:
+ return
+ self._build_data[environment]['doffs_variant'][doff_id] = new_variant
+ self.autosave()
+
+ def toggle_space_skill(self, current_state: bool, career: str, skill_id: int):
+ """
+ Activates space skill if it's deactivated, deactivates skill if it's activated.
+
+ Parameters:
+ - :param current_state: state of the button before toggling
+ - :param career: "eng" / "tac" / "sci"
+ - :param skill_id: id of the skill node
+ """
+ if current_state:
+ self.skills.space[career][skill_id].clear_overlay()
+ self.skills.space[career][skill_id].highlight = False
+ self._build_data['space_skills'][career][skill_id] = False
+ self._cache.skills['space_points_total'] -= 1
+ self._cache.skills[f'space_points_{career}'] -= 1
+ self._cache.skills['space_points_rank'][int(skill_id / 6)] -= 1
+ segment_index: int = self._skill_state[f'space_points_{career}']
+ if segment_index < 24:
+ self.skills.bonus_bars[career][segment_index].setChecked(False)
+ if segment_index % 5 == 4:
+ button_index = (segment_index - 4) // 5
+ self.set_skill_unlock_space(career, button_index, None)
+ elif segment_index == 23:
+ self.set_skill_unlock_space(career, 4, None)
+ elif 24 <= segment_index <= 26:
+ self.set_skill_unlock_space(career, 4, 0, segment_index)
+ else:
+ self.skills.space[career][skill_id].set_overlay(self._images.overlays.check)
+ self.skills.space[career][skill_id].highlight = True
+ self._build_data['space_skills'][career][skill_id] = True
+ self._skill_state['space_points_total'] += 1
+ self._skill_state[f'space_points_{career}'] += 1
+ self._skill_state['space_points_rank'][int(skill_id / 6)] += 1
+ segment_index: int = self._skill_state[f'space_points_{career}'] - 1
+ if segment_index < 24:
+ self.skills.bonus_bars[career][segment_index].setChecked(True)
+ if segment_index % 5 == 4:
+ button_index = (segment_index - 4) // 5
+ self.set_skill_unlock_space(career, button_index, 0)
+ elif segment_index == 23:
+ self.set_skill_unlock_space(career, 4, -1, 24)
+ elif 24 <= segment_index <= 25:
+ self.set_skill_unlock_space(career, 4, 0, segment_index + 1)
+ elif segment_index == 26:
+ self.set_skill_unlock_space(career, 4, 3, 27)
+ self.skills.count_labels[career].setText(str(self._skill_state[f'space_points_{career}']))
+ self.autosave()
+
+ def skill_unlock_callback(self, bar: str, unlock_id: int):
+ """
+ Callback for skill unlock buttons
+
+ Parameters:
+ - :param bar: "eng" / "sci" / "tac" / "ground"
+ - :param unlock_id: index of the unlock button
+ """
+ current_state = self._build_data['skill_unlocks'][bar][unlock_id]
+ if current_state is None:
+ return
+ if bar == 'ground':
+ if current_state == 0:
+ self.set_skill_unlock_ground(unlock_id, 1)
+ elif current_state == 1:
+ self.set_skill_unlock_ground(unlock_id, 0)
+ self.autosave()
+ else:
+ if unlock_id < 4:
+ if current_state == 0:
+ self.set_skill_unlock_space(bar, unlock_id, 1)
+ elif current_state == 1:
+ self.set_skill_unlock_space(bar, unlock_id, 0)
+ self.autosave()
+ else:
+ points_spent = self._skill_state[f'space_points_{bar}']
+ if 25 <= points_spent <= 26:
+ self.set_skill_unlock_space(bar, 4, (current_state + 1) % 3, points_spent)
+ self.autosave()
+
+ def toggle_ground_skill(self, current_state: bool, skill_group: int, skill_id: int):
+ """
+ Activates ground skill if it's deactivated, deactivates skill if it's activated.
+
+ Parameters:
+ - :param current_state: state of the button before toggling
+ - :param skill_group: number [0, 3] identifying the skill group
+ - :param skill_id: index of the skill within the group
+ """
+ if current_state:
+ self.skills.ground[skill_group][skill_id].clear_overlay()
+ self.skills.ground[skill_group][skill_id].highlight = False
+ self._build_data['ground_skills'][skill_group][skill_id] = False
+ self._skill_state['ground_points_total'] -= 1
+ segment_index = self._skill_state['ground_points_total']
+ self.skills.bonus_bars['ground'][segment_index].setChecked(False)
+ if segment_index % 2 == 1:
+ button_index = (segment_index - 1) // 2
+ self.set_skill_unlock_ground(self, button_index, None)
+ else:
+ self.skills.ground[skill_group][skill_id].set_overlay(self._images.overlays.check)
+ self.skills.ground[skill_group][skill_id].highlight = True
+ self._build_data['ground_skills'][skill_group][skill_id] = True
+ self._skill_state['ground_points_total'] += 1
+ segment_index = self._skill_state['ground_points_total'] - 1
+ self.skills.bonus_bars['ground'][segment_index].setChecked(True)
+ if segment_index % 2 == 1:
+ button_index = (segment_index - 1) // 2
+ self.set_skill_unlock_ground(button_index, 0)
+ self.skills.count_labels['ground'].setText(str(self._skill_state['ground_points_total']))
+ self.autosave()
+
+ def skill_callback_space(self, career: str, skill_id: int, grouping: str):
+ """
+ Callback for space skill node
+
+ Parameters:
+ - :param career: "eng" / "tac" / "sci"
+ - :param skill_id: id of the skill node (index in self.build and self.widgets.build)
+ - :param grouping: type of skill grouping: "column" / "pair+1" / "separate"
+ """
+ space_skills = self._build_data['space_skills']
+ skill_active = space_skills[career][skill_id]
+ skill_lvl = skill_id % 3
+ skill_rank = int(skill_id / 6)
+ if skill_active: # check for valid deselect
+ if (skill_lvl == 2
+ or grouping != 'column' and skill_lvl == 1
+ or not space_skills[career][skill_id + 1]):
+ skill_count = sum(self._skill_state['space_points_rank'][:skill_rank + 1])
+ for offset, points_required in enumerate(SKILL_POINTS_FOR_RANK[skill_rank + 1:]):
+ if (skill_count - 1 < points_required
+ and self._skill_state['space_points_total'] - skill_count > 0):
+ return
+ skill_count += self._skill_state['space_points_rank'][skill_rank + offset + 1]
+ self.toggle_space_skill(skill_active, career, skill_id)
+ else: # check for valid select
+ if 46 > self._skill_state['space_points_total'] >= SKILL_POINTS_FOR_RANK[skill_rank]:
+ if skill_lvl == 0:
+ self.toggle_space_skill(skill_active, career, skill_id)
+ elif (grouping == 'column' and space_skills[career][skill_id - 1]):
+ self.toggle_space_skill(skill_active, career, skill_id)
+ elif (grouping != 'column' and space_skills[career][skill_id - skill_lvl]):
+ self.toggle_space_skill(skill_active, career, skill_id)
+
+ def skill_callback_ground(self, skill_group: int, skill_id: int):
+ """
+ Callback for ground skill node
+
+ Parameters:
+ - :param skill_group: number [0, 3] identifying the skill group
+ - :param skill_id: index of the skill within the group
+ """
+ ground_skills = self._build_data['ground_skills']
+ skill_active = ground_skills[skill_group][skill_id]
+ if skill_active: # check for valid deselect
+ if skill_id == 0 and (
+ ground_skills[skill_group][1] or ground_skills[skill_group][2]
+ or skill_group <= 1 and ground_skills[skill_group][4]):
+ return
+ elif skill_id % 2 == 0 and ground_skills[skill_group][skill_id + 1]:
+ return
+ self.toggle_ground_skill(skill_active, skill_group, skill_id)
+ else: # check for valid select
+ if self._skill_state['ground_points_total'] < 10:
+ if skill_id % 2 == 1 and ground_skills[skill_group][skill_id - 1]:
+ self.toggle_ground_skill(skill_active, skill_group, skill_id)
+ elif skill_id == 0:
+ self.toggle_ground_skill(skill_active, skill_group, skill_id)
+ elif (skill_id == 2 or skill_id == 4) and ground_skills[skill_group][0]:
+ self.toggle_ground_skill(skill_active, skill_group, skill_id)
diff --git a/src/callbacks.py b/src/callbacks.py
index 91adf66..8067594 100644
--- a/src/callbacks.py
+++ b/src/callbacks.py
@@ -2,9 +2,7 @@
align_space_frame, clear_captain, clear_doffs, clear_ground_build, clear_ship, clear_traits,
get_variable_slot_counts, set_skill_unlock_ground, set_skill_unlock_space,
slot_equipment_item, slot_trait_item, update_equipment_cat, update_starship_traits)
-from .constants import (
- EQUIPMENT_TYPES, PRIMARY_SPECS, SECONDARY_SPECS, SHIP_TEMPLATE, SKILL_POINTS_FOR_RANK,
- SPECIES, SPECIES_TRAITS)
+from .constants import EQUIPMENT_TYPES, SHIP_TEMPLATE, SKILL_POINTS_FOR_RANK
from .datafunctions import (
load_build_file, load_skill_tree_file, save_build_file, save_skill_tree_file)
from .iofunc import browse_path, image, open_wiki_page
@@ -13,137 +11,6 @@
from PySide6.QtCore import Qt
-def faction_combo_callback(self, new_faction: str):
- """
- Saves new faction to build and changes species selector choices.
- """
- self.build['captain']['faction'] = new_faction
- self.widgets.character['species'].clear()
- if new_faction != '':
- self.widgets.character['species'].addItems(('', *SPECIES[new_faction]))
- self.build['captain']['species'] = ''
- self.autosave()
-
-
-def species_combo_callback(self, new_species: str):
- """
- Saves new species to build and changes species trait
- """
- self.build['captain']['species'] = new_species
- if new_species == 'Alien':
- if not self.building:
- self.build['space']['traits'][10] = ''
- self.build['ground']['traits'][10] = ''
- self.build['space']['traits'][11] = ''
- self.build['ground']['traits'][11] = ''
- self.widgets.build['space']['traits'][10].show()
- self.widgets.build['ground']['traits'][10].show()
- self.widgets.build['space']['traits'][11].clear()
- self.widgets.build['ground']['traits'][11].clear()
- else:
- self.widgets.build['space']['traits'][10].hide()
- self.widgets.build['ground']['traits'][10].hide()
- self.widgets.build['space']['traits'][10].clear()
- self.widgets.build['ground']['traits'][10].clear()
- self.build['space']['traits'][10] = None
- self.build['ground']['traits'][10] = None
- new_space_trait = SPECIES_TRAITS['space'].get(new_species, '')
- new_ground_trait = SPECIES_TRAITS['ground'].get(new_species, '')
- if new_space_trait == '':
- self.widgets.build['space']['traits'][11].clear()
- self.build['space']['traits'][11] = ''
- else:
- slot_trait_item(self, {'item': new_space_trait}, 'space', 'traits', 11)
- if new_ground_trait == '':
- self.widgets.build['ground']['traits'][11].clear()
- self.build['ground']['traits'][11] = ''
- else:
- slot_trait_item(self, {'item': new_ground_trait}, 'ground', 'traits', 11)
- self.autosave()
-
-
-def spec_combo_callback(self, primary: bool, new_spec: str):
- """
- Saves new spec to build and adjusts choices in other spec combo box.
- """
- if primary:
- self.build['captain']['primary_spec'] = new_spec
- secondary_combo = self.widgets.character['secondary']
- secondary_specs = set()
- remove_index = None
- for i in range(secondary_combo.count()):
- secondary_specs.add(secondary_combo.itemText(i))
- if secondary_combo.itemText(i) == new_spec and new_spec != '':
- remove_index = i
- if remove_index is not None:
- secondary_combo.removeItem(remove_index)
- secondary_combo.addItems((PRIMARY_SPECS | SECONDARY_SPECS) - secondary_specs)
- else:
- self.build['captain']['secondary_spec'] = new_spec
- primary_combo = self.widgets.character['primary']
- primary_specs = set()
- remove_index = None
- for i in range(primary_combo.count()):
- primary_specs.add(primary_combo.itemText(i))
- if primary_combo.itemText(i) == new_spec and new_spec != '':
- remove_index = i
- if remove_index is not None:
- primary_combo.removeItem(remove_index)
- primary_combo.addItems(PRIMARY_SPECS - primary_specs)
- self.autosave()
-
-
-def set_build_item(self, dictionary, key, value, autosave: bool = True):
- """
- Assigns value to dictionary item. Triggers autosave.
-
- Parameters:
- - :param dictionary: dictionary to use key on
- - :param key: key for the dictionary
- - :param value: value to be assigned to the item
- - :param autosave: set to False to disable autosave
- """
- dictionary[key] = value
- if autosave:
- self.autosave()
-
-
-def elite_callback(self, state):
- """
- Saves new state and updates build.
-
- Parameters:
- - :param state: new state of the checkbox
- """
- if state == Qt.CheckState.Checked:
- if not self.building:
- self.build['captain']['elite'] = True
- self.build['space']['traits'][9] = ''
- self.build['ground']['traits'][9] = ''
- self.build['ground']['kit_modules'][5] = ''
- self.build['ground']['ground_devices'][4] = ''
- self.widgets.build['space']['traits'][9].show()
- self.widgets.build['ground']['traits'][9].show()
- self.widgets.build['ground']['kit_modules'][5].show()
- self.widgets.build['ground']['ground_devices'][4].show()
- else:
- if not self.building:
- self.build['captain']['elite'] = False
- self.build['space']['traits'][9] = None
- self.build['ground']['traits'][9] = None
- self.build['ground']['kit_modules'][5] = None
- self.build['ground']['ground_devices'][4] = None
- self.widgets.build['space']['traits'][9].hide()
- self.widgets.build['space']['traits'][9].clear()
- self.widgets.build['ground']['traits'][9].hide()
- self.widgets.build['ground']['traits'][9].clear()
- self.widgets.build['ground']['kit_modules'][5].hide()
- self.widgets.build['ground']['kit_modules'][5].clear()
- self.widgets.build['ground']['ground_devices'][4].hide()
- self.widgets.build['ground']['ground_devices'][4].clear()
- self.autosave()
-
-
def get_boff_abilities(
self, environment: str, rank: int, boff_id: int) -> set:
"""
@@ -233,60 +100,6 @@ def picker(
self.autosave()
-def boff_profession_callback_space(self, boff_id: int, new_spec: str):
- """
- updates build with newly assigned profession; clears abilities of the old profession
- """
- # to prevent overwriting the build while loading
- if self.building:
- return
- if ' / ' in new_spec:
- profession, specialization = new_spec.split(' / ')
- if specialization == 'Temporal Operative':
- specialization = 'Temporal'
- for ability_num, ability in enumerate(self.build['space']['boffs'][boff_id]):
- if ability is not None and ability != '':
- # Lt. Commander rank contains all abilities
- if ability['item'] not in self.cache.boff_abilities['space'][specialization][2]:
- self.build['space']['boffs'][boff_id][ability_num] = ''
- self.widgets.build['space']['boffs'][boff_id][ability_num].clear()
- else:
- profession = new_spec
- specialization = ''
- for ability_num, ability in enumerate(self.build['space']['boffs'][boff_id]):
- if ability is not None and ability != '':
- self.build['space']['boffs'][boff_id][ability_num] = ''
- self.widgets.build['space']['boffs'][boff_id][ability_num].clear()
- self.build['space']['boff_specs'][boff_id] = [profession, specialization]
- self.autosave()
-
-
-def boff_label_callback_ground(self, boff_id: int, type_: str, new_text: str):
- """
- updates build with newly assigned profession or specialization; clears invalid abilities
-
- Parameters:
- - :param boff_id: number of the boff station
- - :param type_: "boff_profs" / "boff_specs"
- - :param new_text: new profession / specialization
- """
- if self.building:
- return
- self.build['ground'][type_][boff_id] = new_text
- other_type = 'boff_profs' if type_ == 'boff_specs' else 'boff_specs'
- other_text = self.build['ground'][other_type][boff_id]
- for ability_num, ability in enumerate(self.build['ground']['boffs'][boff_id]):
- if ability is not None and ability != '':
- # Lt. Commander and Commander rank combined contain all abilities
- if (ability['item'] not in self.cache.boff_abilities['ground'][new_text][2]
- and ability['item'] not in self.cache.boff_abilities['ground'][new_text][3]
- and ability['item'] not in self.cache.boff_abilities['ground'][other_text][2]
- and ability['item'] not in self.cache.boff_abilities['ground'][other_text][3]):
- self.build['ground']['boffs'][boff_id][ability_num] = ''
- self.widgets.build['ground']['boffs'][boff_id][ability_num].clear()
- self.autosave()
-
-
def select_ship(self):
"""
Opens ship picker and updates UI to reflect new ship.
@@ -319,28 +132,6 @@ def select_ship(self):
self.autosave()
-def tier_callback(self, new_tier: str):
- """
- Updates build according to new tier
- """
- if self.building:
- return
- self.build['space']['tier'] = new_tier
- ship_name = self.build['space']['ship']
- if ship_name == '':
- ship_data = SHIP_TEMPLATE
- else:
- ship_data = self.cache.ships[ship_name]
- uni, eng, sci, tac, devices, starship_traits = get_variable_slot_counts(self, ship_data)
- update_equipment_cat(self, 'uni_consoles', uni, can_hide=True)
- update_equipment_cat(self, 'eng_consoles', eng)
- update_equipment_cat(self, 'sci_consoles', sci)
- update_equipment_cat(self, 'tac_consoles', tac)
- update_equipment_cat(self, 'devices', devices)
- update_starship_traits(self, starship_traits)
- self.autosave()
-
-
def clear_build_callback(self):
"""
Clears current build section
@@ -494,222 +285,3 @@ def save_skills_callback(self):
save_path = browse_path(self, default_path, file_types, save=True)
if save_path != '':
save_skill_tree_file(self, save_path)
-
-
-def ship_info_callback(self):
- """
- Opens wiki page of ship if ship is slotted
- """
- if self.build['space']['ship'] != '':
- open_wiki_page(self.cache.ships[self.build['space']['ship']]['Page'])
-
-
-def doff_spec_callback(self, new_spec: str, environment: str, doff_id: int):
- """
- Callback for duty officer specialization combobox.
-
- Parameters:
- - :param new_spec: selected specialization
- - :param environment: "space" / "ground"
- - :param doff_id: index of the doff
- """
- if self.building:
- return
- self.build[environment]['doffs_spec'][doff_id] = new_spec
- self.build[environment]['doffs_variant'][doff_id] = ''
- self.widgets.build[environment]['doffs_variant'][doff_id].clear()
- if new_spec != '':
- variants = getattr(self.cache, f'{environment}_doffs')[new_spec].keys()
- self.widgets.build[environment]['doffs_variant'][doff_id].addItems({''} | variants)
- self.autosave()
-
-
-def doff_variant_callback(self, new_variant: str, environment: str, doff_id: int):
- """
- Callback for duty officer variant combobox.
-
- Parameters:
- - :param new_variant: selected variant
- - :param environment: "space" / "ground"
- - :param doff_id: index of the doff
- """
- if self.building:
- return
- self.build[environment]['doffs_variant'][doff_id] = new_variant
- self.autosave()
-
-
-def toggle_space_skill(self, current_state: bool, career: str, skill_id: int):
- """
- Activates space skill if it's deactivated, deactivates skill if it's activated.
-
- Parameters:
- - :param current_state: state of the button before toggling
- - :param career: "eng" / "tac" / "sci"
- - :param skill_id: id of the skill node (index in self.build and self.widgets.build)
- """
- if current_state:
- self.widgets.build['space_skills'][career][skill_id].clear_overlay()
- self.widgets.build['space_skills'][career][skill_id].highlight = False
- self.build['space_skills'][career][skill_id] = False
- self.cache.skills['space_points_total'] -= 1
- self.cache.skills[f'space_points_{career}'] -= 1
- self.cache.skills['space_points_rank'][int(skill_id / 6)] -= 1
- segment_index = self.cache.skills[f'space_points_{career}']
- if segment_index < 24:
- self.widgets.skill_bonus_bars[career][segment_index].setChecked(False)
- if segment_index % 5 == 4:
- button_index = (segment_index - 4) // 5
- set_skill_unlock_space(self, career, button_index, None)
- elif segment_index == 23:
- set_skill_unlock_space(self, career, 4, None)
- elif 24 <= segment_index <= 26:
- set_skill_unlock_space(self, career, 4, 0, segment_index)
- else:
- self.widgets.build['space_skills'][career][skill_id].set_overlay(self.cache.overlays.check)
- self.widgets.build['space_skills'][career][skill_id].highlight = True
- self.build['space_skills'][career][skill_id] = True
- self.cache.skills['space_points_total'] += 1
- self.cache.skills[f'space_points_{career}'] += 1
- self.cache.skills['space_points_rank'][int(skill_id / 6)] += 1
- segment_index = self.cache.skills[f'space_points_{career}'] - 1
- if segment_index < 24:
- self.widgets.skill_bonus_bars[career][segment_index].setChecked(True)
- if segment_index % 5 == 4:
- button_index = (segment_index - 4) // 5
- set_skill_unlock_space(self, career, button_index, 0)
- elif segment_index == 23:
- set_skill_unlock_space(self, career, 4, -1, 24)
- elif 24 <= segment_index <= 25:
- set_skill_unlock_space(self, career, 4, 0, segment_index + 1)
- elif segment_index == 26:
- set_skill_unlock_space(self, career, 4, 3, 27)
- self.widgets.skill_counts_space[career].setText(
- str(self.cache.skills[f'space_points_{career}']))
- self.autosave()
-
-
-def skill_unlock_callback(self, bar: str, unlock_id: int):
- """
- Callback for skill unlock buttons
-
- Parameters:
- - :param bar: "eng" / "sci" / "tac" / "ground"
- - :param unlock_id: index of the unlock button
- """
- current_state = self.build['skill_unlocks'][bar][unlock_id]
- if current_state is None:
- return
- if bar == 'ground':
- if current_state == 0:
- set_skill_unlock_ground(self, unlock_id, 1)
- elif current_state == 1:
- set_skill_unlock_ground(self, unlock_id, 0)
- self.autosave()
- else:
- if unlock_id < 4:
- if current_state == 0:
- set_skill_unlock_space(self, bar, unlock_id, 1)
- elif current_state == 1:
- set_skill_unlock_space(self, bar, unlock_id, 0)
- self.autosave()
- else:
- points_spent = self.cache.skills[f'space_points_{bar}']
- if 25 <= points_spent <= 26:
- set_skill_unlock_space(self, bar, 4, (current_state + 1) % 3, points_spent)
- self.autosave()
-
-
-def toggle_ground_skill(self, current_state: bool, skill_group: int, skill_id: int):
- """
- Activates ground skill if it's deactivated, deactivates skill if it's activated.
-
- Parameters:
- - :param current_state: state of the button before toggling
- - :param skill_group: number [0, 3] identifying the skill group
- - :param skill_id: index of the skill within the group
- """
- if current_state:
- self.widgets.build['ground_skills'][skill_group][skill_id].clear_overlay()
- self.widgets.build['ground_skills'][skill_group][skill_id].highlight = False
- self.build['ground_skills'][skill_group][skill_id] = False
- self.cache.skills['ground_points_total'] -= 1
- segment_index = self.cache.skills['ground_points_total']
- self.widgets.skill_bonus_bars['ground'][segment_index].setChecked(False)
- if segment_index % 2 == 1:
- button_index = (segment_index - 1) // 2
- set_skill_unlock_ground(self, button_index, None)
- else:
- self.widgets.build['ground_skills'][skill_group][skill_id].set_overlay(
- self.cache.overlays.check)
- self.widgets.build['ground_skills'][skill_group][skill_id].highlight = True
- self.build['ground_skills'][skill_group][skill_id] = True
- self.cache.skills['ground_points_total'] += 1
- segment_index = self.cache.skills['ground_points_total'] - 1
- self.widgets.skill_bonus_bars['ground'][segment_index].setChecked(True)
- if segment_index % 2 == 1:
- button_index = (segment_index - 1) // 2
- set_skill_unlock_ground(self, button_index, 0)
- self.widgets.skill_count_ground.setText(str(self.cache.skills['ground_points_total']))
- self.autosave()
-
-
-def skill_callback_space(self, career: str, skill_id: int, grouping: str):
- """
- Callback for space skill node
-
- Parameters:
- - :param career: "eng" / "tac" / "sci"
- - :param skill_id: id of the skill node (index in self.build and self.widgets.build)
- - :param grouping: type of skill grouping: "column" / "pair+1" / "separate"
- """
- skill_active = self.build['space_skills'][career][skill_id]
- skill_lvl = skill_id % 3
- skill_rank = int(skill_id / 6)
- if skill_active: # check for valid deselect
- if (skill_lvl == 2
- or grouping != 'column' and skill_lvl == 1
- or not self.build['space_skills'][career][skill_id + 1]):
- skill_count = sum(self.cache.skills['space_points_rank'][:skill_rank + 1])
- for rank_offset, points_required in enumerate(SKILL_POINTS_FOR_RANK[skill_rank + 1:]):
- if (skill_count - 1 < points_required
- and self.cache.skills['space_points_total'] - skill_count > 0):
- return
- skill_count += self.cache.skills['space_points_rank'][skill_rank + rank_offset + 1]
- toggle_space_skill(self, skill_active, career, skill_id)
- else: # check for valid select
- if 46 > self.cache.skills['space_points_total'] >= SKILL_POINTS_FOR_RANK[skill_rank]:
- if skill_lvl == 0:
- toggle_space_skill(self, skill_active, career, skill_id)
- elif grouping == 'column' and self.build['space_skills'][career][skill_id - 1]:
- toggle_space_skill(self, skill_active, career, skill_id)
- elif grouping != 'column' and self.build['space_skills'][career][skill_id - skill_lvl]:
- toggle_space_skill(self, skill_active, career, skill_id)
-
-
-def skill_callback_ground(self, skill_group: int, skill_id: int):
- """
- Callback for ground skill node
-
- Parameters:
- - :param skill_group: number [0, 3] identifying the skill group
- - :param skill_id: index of the skill within the group
- """
- skill_active = self.build['ground_skills'][skill_group][skill_id]
- if skill_active: # check for valid deselect
- if skill_id == 0 and (
- self.build['ground_skills'][skill_group][1]
- or self.build['ground_skills'][skill_group][2]
- or skill_group <= 1 and self.build['ground_skills'][skill_group][4]):
- return
- elif skill_id % 2 == 0 and self.build['ground_skills'][skill_group][skill_id + 1]:
- return
- toggle_ground_skill(self, skill_active, skill_group, skill_id)
- else: # check for valid select
- if self.cache.skills['ground_points_total'] < 10:
- if skill_id % 2 == 1 and self.build['ground_skills'][skill_group][skill_id - 1]:
- toggle_ground_skill(self, skill_active, skill_group, skill_id)
- elif skill_id == 0:
- toggle_ground_skill(self, skill_active, skill_group, skill_id)
- elif (skill_id == 2 or skill_id == 4) and self.build['ground_skills'][skill_group][0]:
- toggle_ground_skill(self, skill_active, skill_group, skill_id)
From bb71f406773314bae9c81e4ba01796ef412812eb Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Tue, 12 May 2026 09:56:27 +0200
Subject: [PATCH 17/44] moving picker logic out of callbacks.py
---
src/app.py | 40 +++++++++++++--
src/buildmanager.py | 72 +++++++++++++++++++++++++-
src/callbacks.py | 121 --------------------------------------------
src/picker.py | 30 ++++++++---
4 files changed, 129 insertions(+), 134 deletions(-)
diff --git a/src/app.py b/src/app.py
index 26a8515..0bbf051 100644
--- a/src/app.py
+++ b/src/app.py
@@ -1,7 +1,7 @@
import os
from pathlib import Path
-from PySide6.QtCore import QDir, Qt, QThread
+from PySide6.QtCore import QDir, QPoint, Qt, QThread
from PySide6.QtGui import QCloseEvent, QFontDatabase, QTextOption
from PySide6.QtWidgets import (
QApplication, QFrame, QPlainTextEdit, QPushButton, QScrollArea, QTabWidget, QWidget)
@@ -114,9 +114,11 @@ def __init__(self, theme, args, path, config, versions):
self.setup_main_layout()
self.export_window = ExportWindow(self.theme2, self.window, self.build2, self.cargo)
self.picker_window: Picker = Picker(self.theme2, self.window, self.settings, self.images)
+ self.picker_window.dialog_result.connect(self.build2.handle_picker_result)
self.edit_window: ItemEditor = ItemEditor(self.theme2, self.window)
self.edit_window.dialog_result.connect(self.build2.finish_item_edit)
self.ship_selector_window: ShipSelector = ShipSelector(self.theme2, self.window)
+ self.ship_selector_window.dialog_result.connect(self.build2.finish_ship_pick)
self.context_menu: ContextMenu = ContextMenu(self.theme2, self.build2, self.cargo)
self.context_menu.edit_slot.connect(self.edit_window.edit_item)
self.window.show()
@@ -253,7 +255,7 @@ def create_main_window(self, argv=[]) -> tuple[QApplication, QWidget]:
return app, window
def picker(
- self, environment: str, build_key: str, build_subkey: int, button,
+ self, environment: str, build_key: str, build_subkey: int, button: ItemButton,
equipment: bool = False, boff_id: int | None = None):
"""
opens dialog to select item, stores it to build and updates item button
@@ -267,6 +269,38 @@ def picker(
- :param equipment: set to True to show rarity, mark, and modifier selector (optional)
- :param boff_id: id of the boff; only set when picking boff abilities! (optional)
"""
+ modifiers = {}
+ image_suffix = ''
+ if equipment:
+ items = self.cargo.equipment[build_key].keys()
+ modifiers = self.cargo.modifiers[build_key]
+ elif build_key == 'boffs':
+ if environment == 'space':
+ profession, specialization = self.build2['space']['boff_specs'][boff_id]
+ if specialization == 'Temporal Operative':
+ specialization = 'Temporal'
+ else:
+ profession = self.build['ground']['boff_profs'][boff_id]
+ specialization = self.build['ground']['boff_specs'][boff_id]
+ items = self.cargo.boff_abilities[environment][profession][build_subkey]
+ if specialization != '':
+ items = items + self.cache.boff_abilities[environment][specialization][build_subkey]
+ elif build_key == 'starship_traits':
+ items = self.cargo.starship_traits.keys()
+ image_suffix = '__space__starship_traits'
+ elif 'traits' in build_key:
+ if environment == 'space':
+ items = self.cargo.space_traits[build_key].keys()
+ else:
+ items = self.cargo.ground_traits[build_key].keys()
+ image_suffix = f'__{environment}__{build_key}'
+ else:
+ items = []
+ if self.settings.picker_relative == 1:
+ pos = button.mapToGlobal(QPoint(0, 0))
+ else:
+ pos = None
+ self.picker_window.pick_item(items, pos, equipment, modifiers, image_suffix)
def setup_main_layout(self):
"""
@@ -425,7 +459,7 @@ def setup_ship_frame(self):
ship_selector.setStyleSheet(
self.theme2.get_style_class('ShipButton', 'button', override={'margin': 0}))
ship_selector.setFont(self.theme2.get_font(font_spec='@subhead'))
- ship_selector.clicked.connect(self.select_ship)
+ ship_selector.clicked.connect(self.ship_selector_window.pick_ship)
self.build2.ship.button = ship_selector
ship_layout.addWidget(ship_selector, 0, 0, 1, 4, alignment=ATOP)
tier_label = create_label2(self.theme2, 'Ship Tier:')
diff --git a/src/buildmanager.py b/src/buildmanager.py
index 0fb3412..434cecf 100644
--- a/src/buildmanager.py
+++ b/src/buildmanager.py
@@ -1,12 +1,13 @@
from pathlib import Path
-from PySide6.QtCore import Qt
+from PySide6.QtCore import Qt, Slot
from PySide6.QtWidgets import QCheckBox, QComboBox, QLabel, QLineEdit, QPlainTextEdit, QPushButton
from .buildhelpers import get_boff_spec, get_variable_slot_counts, empty_build
from .cargomanager import CargoManager
from .constants import (
- PRIMARY_SPECS, SECONDARY_SPECS, SHIP_TEMPLATE, SKILL_POINTS_FOR_RANK, SPECIES, SPECIES_TRAITS)
+ EQUIPMENT_TYPES, PRIMARY_SPECS, SECONDARY_SPECS, SHIP_TEMPLATE, SKILL_POINTS_FOR_RANK, SPECIES,
+ SPECIES_TRAITS)
from .imagemanager import ImageManager
from .iofunc import open_wiki_page, store_json__new
from .textedit import add_equipment_tooltip_header__new, get_ultimate_skill_unlock_tooltip__new
@@ -561,6 +562,73 @@ def unslot_item(self, environment: str, build_key: str, build_subkey: int, boff_
getattr(self, environment), build_key)[boff_id][build_subkey]
item_button.clear()
+ @Slot(dict, ItemSlot)
+ def handle_picker_result(self, new_item: dict[str, str | list[str]], slot: ItemSlot):
+ """
+ Inserts picked item into given slot if picking was not cancelled.
+
+ Parameters:
+ - :param new_item: contains picked item, or empty item if picking was cancelled
+ - :param slot: information about the slot
+ """
+ if new_item['item'] != '':
+ widget_storage = self.space if slot.environment == 'space' else self.ground
+ if slot.is_equipment:
+ if 'consoles' in slot.type:
+ item_data = self._cache.equipment[slot.type][new_item['item']]
+ type_ = EQUIPMENT_TYPES[item_data['type']]
+ for i, mod in enumerate(new_item['modifiers']):
+ if mod not in self._cache.modifiers[type_]:
+ new_item['modifiers'][i] = ''
+ self.slot_equipment_item(self, new_item, slot.environment, slot.type, slot.index)
+ else:
+ if slot.boff_id is None:
+ self.slot_trait_item(
+ self, {'item': new_item['item']}, slot.environment, slot.type, slot.index)
+ elif slot.type == 'boffs':
+ ability_name, _, ability_rank = new_item['item'].rpartition(' ')
+ self._build_data[slot.environment]['boffs'][slot.boff_id][slot.index] = {
+ 'item': ability_name,
+ 'rank': ability_rank
+ }
+ button: ItemButton = widget_storage.boffs[slot.boff_id][slot.index]
+ button.set_item(self._images.get(ability_name))
+ button.tooltip = (self._cache.boff_abilities['all'][ability_name][ability_rank])
+ self.autosave()
+
+ @Slot(str)
+ def finish_ship_pick(self, ship_name: str):
+ """
+ Switches to selected ship.
+
+ Parameters:
+ - :param ship_name: name of the selected ship, or empty
+ """
+ if ship_name == '':
+ return
+ self._building = True
+ self.ship.button.setText(ship_name)
+ ship_data = self._cache.ships[ship_name]
+ self.set_ship_image(ship_data['image'][5:])
+ tier = ship_data['tier']
+ self.ship.tier.clear()
+ if tier == 6:
+ self.ship.tier.addItems(('T6', 'T6-X', 'T6-X2'))
+ elif tier == 5:
+ self.ship.tier.addItems(('T5', 'T5-U', 'T5-X', 'T5-X2'))
+ else:
+ self.ship.tier.addItem(f'T{tier}')
+ self._build_data['space']['ship'] = ship_name
+ self._build_data['space']['tier'] = f'T{tier}'
+ if ship_data['equipcannons'] == 'yes':
+ self.ship.dc.show()
+ else:
+ self.ship.dc.hide()
+ self.align_space_frame(ship_data, clear=True)
+ self._building = False
+ self.autosave()
+
+ @Slot(dict, ItemSlot)
def finish_item_edit(self, new_item: dict[str], slot: ItemSlot):
"""
Updates item after editing if editing was not cancelled. Autosaves.
diff --git a/src/callbacks.py b/src/callbacks.py
index 8067594..188a4f4 100644
--- a/src/callbacks.py
+++ b/src/callbacks.py
@@ -11,127 +11,6 @@
from PySide6.QtCore import Qt
-def get_boff_abilities(
- self, environment: str, rank: int, boff_id: int) -> set:
- """
- Returns list of boff abilities appropriate for the station described by the parameters.
-
- Parameters:
- - :param environment: space/ground
- - :param rank: rank of the ability slot
- - :param boff_id: id of the boff
- """
- if environment == 'space':
- profession, specialization = self.build['space']['boff_specs'][boff_id]
- if specialization == 'Temporal Operative':
- specialization = 'Temporal'
- else:
- profession = self.build['ground']['boff_profs'][boff_id]
- specialization = self.build['ground']['boff_specs'][boff_id]
- abilities = self.cache.boff_abilities[environment][profession][rank]
- if specialization != '':
- abilities = abilities + self.cache.boff_abilities[environment][specialization][rank]
- return abilities
-
-
-def picker(
- self, environment: str, build_key: str, build_subkey: int, button, equipment: bool = False,
- boff_id=None):
- """
- opens dialog to select item, stores it to build and updates item button
-
- Parameters:
- - :param items: iterable of items available to pick from
- - :param environment: space or ground
- - :param build_key: key to self.build[environment]; for storing picked item
- - :param build_subkey: index of the item within its build_key (category)
- - :param button: reference to the button clicked
- - :param equipment: set to True to show rarity, mark, and modifier selector (optional)
- - :param boff_id: id of the boff; only set when picking boff abilities! (optional)
- """
- modifiers = {}
- image_suffix = ''
- if equipment:
- items = self.cache.equipment[build_key].keys()
- modifiers = self.cache.modifiers[build_key]
- elif build_key == 'boffs':
- items = get_boff_abilities(self, environment, build_subkey, boff_id)
- elif build_key == 'traits':
- items = self.cache.traits[environment]['traits'].keys()
- image_suffix = f'__{environment}__{build_key}'
- elif build_key == 'starship_traits':
- items = self.cache.starship_traits.keys()
- image_suffix = '__space__starship_traits'
- elif build_key == 'rep_traits':
- items = self.cache.traits[environment]['rep_traits'].keys()
- image_suffix = f'__{environment}__{build_key}'
- elif build_key == 'active_rep_traits':
- items = self.cache.traits[environment]['active_rep_traits'].keys()
- image_suffix = f'__{environment}__{build_key}'
- else:
- items = []
- if self.settings.picker_relative == 1:
- pos = button.parent().mapToGlobal(button.pos())
- else:
- pos = None
- new_item = self.picker_window.pick_item(items, pos, equipment, modifiers, image_suffix)
- if new_item is not None:
- widget_storage = self.widgets.build[environment]
- if equipment:
- if 'consoles' in build_key:
- type_ = EQUIPMENT_TYPES[self.cache.equipment[build_key][new_item['item']]['type']]
- for i, mod in enumerate(new_item['modifiers']):
- if mod not in self.cache.modifiers[type_]:
- new_item['modifiers'][i] = ''
- slot_equipment_item(self, new_item, environment, build_key, build_subkey)
- else:
- if boff_id is None:
- slot_trait_item(
- self, {'item': new_item['item']}, environment, build_key, build_subkey)
- elif build_key == 'boffs':
- ability_name, _, ability_rank = new_item['item'].rpartition(' ')
- self.build[environment]['boffs'][boff_id][build_subkey] = {
- 'item': ability_name,
- 'rank': ability_rank
- }
- widget_storage['boffs'][boff_id][build_subkey].set_item(image(self, ability_name))
- widget_storage['boffs'][boff_id][build_subkey].tooltip = (
- self.cache.boff_abilities['all'][ability_name][ability_rank])
- self.autosave()
-
-
-def select_ship(self):
- """
- Opens ship picker and updates UI to reflect new ship.
- """
- new_ship = self.ship_selector_window.pick_ship()
- if new_ship is None:
- return
- self.building = True
- self.widgets.ship['button'].setText(new_ship)
- ship_data = self.cache.ships[new_ship]
- exec_in_thread(
- self, self.images.get_ship_image, ship_data['image'][5:],
- result=lambda img: self.widgets.ship['image'].set_image(*img))
- tier = ship_data['tier']
- self.widgets.ship['tier'].clear()
- if tier == 6:
- self.widgets.ship['tier'].addItems(('T6', 'T6-X', 'T6-X2'))
- elif tier == 5:
- self.widgets.ship['tier'].addItems(('T5', 'T5-U', 'T5-X', 'T5-X2'))
- else:
- self.widgets.ship['tier'].addItem(f'T{tier}')
- self.build['space']['ship'] = new_ship
- self.build['space']['tier'] = f'T{tier}'
- if ship_data['equipcannons'] == 'yes':
- self.widgets.ship['dc'].show()
- else:
- self.widgets.ship['dc'].hide()
- align_space_frame(self, ship_data, clear=True)
- self.building = False
- self.autosave()
-
-
def clear_build_callback(self):
"""
Clears current build section
diff --git a/src/picker.py b/src/picker.py
index 6bc3b4c..f087107 100644
--- a/src/picker.py
+++ b/src/picker.py
@@ -140,6 +140,7 @@ def __init__(
self._item_model: QStringListModel
self._sort_model: QSortFilterProxyModel
self._items_list: QListView
+ self.finished.connect(self.finish_pick)
self.setWindowFlags(
self.windowFlags() | Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
@@ -352,9 +353,10 @@ def mouseMoveEvent(self, event: QMouseEvent):
class ShipSelector(QDialog):
- """
- Selection Window for ships
- """
+ """Selection Window for ships"""
+
+ dialog_result: Signal = Signal(str)
+
def __init__(self, theme: AppTheme, parent_window: QWidget, style: str = 'picker'):
super().__init__(parent=parent_window)
self.setWindowFlags(
@@ -363,6 +365,7 @@ def __init__(self, theme: AppTheme, parent_window: QWidget, style: str = 'picker
self.setWindowModality(Qt.WindowModality.WindowModal)
self.setMinimumSize(10, 10)
self.setSizePolicy(SMAXMAX)
+ self.finished.connect(self.finish_pick)
ui_scale = theme.scale
spacing = theme['defaults']['isp'] * ui_scale
@@ -403,9 +406,10 @@ def __init__(self, theme: AppTheme, parent_window: QWidget, style: str = 'picker
def set_ships(self, ships: Iterable):
self._ship_data_model.setStringList(ships)
+ @Slot()
def pick_ship(self):
"""
- Executes Picker, returns selected ship, returns None when cancelled.
+ Shows picker window.
"""
window = self.parentWidget()
size = (window.width() * 0.2, window.height() * 0.9)
@@ -413,13 +417,22 @@ def pick_ship(self):
self.setFixedSize(*size)
self.move(*pos)
self._ship_list.scrollToTop()
- action = self.exec()
+ self.open()
+
+ @Slot(int)
+ def finish_pick(self, action: int):
+ """
+ Completes the ship pick action, resets the dialog and emits the data using the
+ `dialog_result` signal.
+
+ Parameters:
+ - :param action: indicates whether the result should be saved (`1`) or not (`0`)
+ """
self._search_bar.clear()
+ ship_name = ''
if action == 1:
ship_name = self._ship_list.currentIndex().data(Qt.ItemDataRole.DisplayRole)
- if ship_name != '':
- return ship_name
- return None
+ self.dialog_result.emit(ship_name)
def mousePressEvent(self, event: QMouseEvent):
self.start_pos = event.globalPosition().toPoint()
@@ -438,6 +451,7 @@ class ItemEditor(BasePicker):
"""
def __init__(self, theme: AppTheme, parent_window: QWidget, style: str = 'picker'):
super().__init__(parent=parent_window)
+ self.finished.connect(self.finish_edit)
self.setWindowFlags(
self.windowFlags() | Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
self.setStyleSheet(theme.get_style(style))
From cf69cb184c97a9b91c4e1e1d96e151e8e326a08d Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Tue, 12 May 2026 15:43:16 +0200
Subject: [PATCH 18/44] creating buildloader and relocating relevant functions
into it
---
src/app.py | 3 +
src/buildloader.py | 519 +++++++++++++++++++++++++++++++++++++++++++
src/buildmanager.py | 11 +-
src/callbacks.py | 56 -----
src/constants.py | 4 +
src/datafunctions.py | 476 ---------------------------------------
src/iofunc.py | 44 ++--
src/widgets.py | 21 ++
8 files changed, 582 insertions(+), 552 deletions(-)
create mode 100644 src/buildloader.py
diff --git a/src/app.py b/src/app.py
index 0bbf051..d70b706 100644
--- a/src/app.py
+++ b/src/app.py
@@ -6,6 +6,7 @@
from PySide6.QtWidgets import (
QApplication, QFrame, QPlainTextEdit, QPushButton, QScrollArea, QTabWidget, QWidget)
+from .buildloader import BuildLoader
from .buildmanager import BuildManager
from .cargomanager import CargoManager
from .config import SETSConfig, SETSSettings
@@ -112,6 +113,8 @@ def __init__(self, theme, args, path, config, versions):
self.build = self.empty_build()
self.cargo.load_static_data()
self.setup_main_layout()
+ self.build_loader: BuildLoader = BuildLoader(
+ self.build2, self.cargo, self.config, self.settings, self.window)
self.export_window = ExportWindow(self.theme2, self.window, self.build2, self.cargo)
self.picker_window: Picker = Picker(self.theme2, self.window, self.settings, self.images)
self.picker_window.dialog_result.connect(self.build2.handle_picker_result)
diff --git a/src/buildloader.py b/src/buildloader.py
new file mode 100644
index 0000000..ecb11ce
--- /dev/null
+++ b/src/buildloader.py
@@ -0,0 +1,519 @@
+from json import dumps as json__dumps, JSONDecodeError, loads as json__loads
+from numpy import (
+ array as np__array, append as np__append, fromiter as np__fromiter, packbits as np__packbits,
+ uint8, unpackbits as np__unpackbits, zeros as np__zeros)
+from pathlib import Path
+from zlib import compress as zlib_compress, decompress as zlib_decompress
+
+from PySide6.QtGui import QImage
+from PySide6.QtWidgets import QWidget
+
+from .buildhelpers import empty_build, get_boff_spec
+from .buildmanager import BuildManager
+from .cargomanager import CargoManager
+from .config import SETSConfig, SETSSettings
+from .constants import BUILD_CONVERSION, BUILD_VERSION, SETS_FILE_FILTER
+from .iofunc import browse_path, load_json__new, store_json__new
+from .widgets import pixel_range
+
+
+class BuildLoader():
+ """Loads/saves build files from/to disk."""
+
+ def __init__(
+ self, build: BuildManager, cargo: CargoManager, config: SETSConfig,
+ settings: SETSSettings, window: QWidget):
+ self._build: BuildManager = build
+ self._cargo: CargoManager = cargo
+ self._config: SETSConfig = config
+ self._settings: SETSSettings = settings
+ self._window: QWidget = window
+
+ def load_build_callback(self):
+ """
+ Loads build from file
+ """
+ load_path = browse_path(
+ self._config.config_subfolders['library'], SETS_FILE_FILTER, parent_window=self._window)
+ if load_path is not None:
+ self.load_build_file(load_path)
+
+ def save_build_callback(self):
+ """
+ Saves build to file
+ """
+ if self._build.ship.button.text() == '':
+ proposed_filename = '(Ship Template)'
+ else:
+ proposed_filename = f"({self._build['space']['ship']})"
+ if self._build['space']['ship_name'] != '':
+ proposed_filename = f"{self._build['space']['ship_name']} {proposed_filename}"
+ preset_path = self._config.config_subfolders['library'] / proposed_filename
+ if self._settings.default_save_format == 'PNG':
+ file_types = 'PNG image (*.png);;JSON file (*.json);;Any File (*.*)'
+ else:
+ file_types = 'JSON file (*.json);;PNG image (*.png);;Any File (*.*)'
+ save_path = browse_path(preset_path, file_types, save=True, parent_window=self._window)
+ if save_path is not None:
+ self.save_build_file(save_path)
+
+ def load_skills_callback(self):
+ """
+ Loads skills from file
+ """
+ load_path = browse_path(
+ self._config.config_subfolders['library'], SETS_FILE_FILTER, parent_window=self._window)
+ if load_path is not None:
+ self.load_skill_tree_file(load_path)
+
+ def save_skills_callback(self):
+ """
+ Save skills to file
+ """
+ preset_path = self._config.config_subfolders['library'] / 'Skill Tree'
+ if self._settings.default_save_format == 'PNG':
+ file_types = 'PNG image (*.png);;JSON file (*.json);;Any File (*.*)'
+ else:
+ file_types = 'JSON file (*.json);;PNG image (*.png);;Any File (*.*)'
+ save_path = browse_path(preset_path, file_types, save=True, parent_window=self._window)
+ if save_path is not None:
+ self.save_skill_tree_file(save_path)
+
+ def load_build_file(self, filepath: Path, update_ui: bool = True):
+ """
+ Loads build from json or png file and puts it into self.build
+
+ Parameters:
+ - :param filepath: path to build file
+ """
+ extension = filepath.suffix.lower()
+ if extension == '.json':
+ build_data = load_json__new(filepath)
+ elif extension == '.png':
+ decoded_str = self.decode_from_image(self, QImage(filepath))
+ if decoded_str == '':
+ return
+ build_data = json__loads(decoded_str)
+ else:
+ return
+ new_build = empty_build()
+ if build_data.get('_version', -1) == BUILD_VERSION:
+ self.merge_build(new_build, build_data)
+ elif 'versionJSON' in build_data:
+ build_data = json__loads(self.compensate_old_build(json__dumps(build_data)))
+ new_build.update(self.convert_old_build(build_data))
+ else:
+ self.merge_build(new_build, build_data)
+ self.update_build_version(new_build)
+ self._build.data = new_build
+ if update_ui:
+ try:
+ self._build.load_build()
+ except KeyError:
+ self.remove_invalid_build_items(self._build.data)
+ self._build.load_build()
+
+ def save_build_file(self, filepath: Path):
+ """
+ Saves build to json or png file
+
+ Parameters:
+ - :param filepath: path to build file
+ """
+ extension = filepath.suffix.lower()
+ if extension == '.json':
+ store_json__new(self._build, filepath)
+ elif extension == '.png':
+ image = self._window.grab().toImage()
+ self.encode_in_image(image, json__dumps(self._build.data))
+ image.save(filepath)
+
+ def load_skill_tree_file(self, filepath: Path):
+ """
+ Loads skill tree from json or png file and puts it into self.build
+
+ Parameters:
+ - :param filepath: path to skill tree file
+ """
+ extension = filepath.suffix.lower()
+ if extension == '.json':
+ build_data = load_json__new(filepath)
+ elif extension == '.png':
+ decoded_str = self.decode_from_image(self, QImage(filepath))
+ if decoded_str == '':
+ return
+ build_data = json__loads(decoded_str)
+ else:
+ return
+ new_build = empty_build('skills')
+ self.merge_build(new_build, build_data)
+ self._build.data['space_skills'] = new_build['space_skills']
+ self._build.data['ground_skills'] = new_build['ground_skills']
+ self._build.data['skill_unlocks'] = new_build['skill_unlocks']
+ self._build.data['skill_desc'] = new_build['skill_desc']
+ self._build.load_skill_pages()
+
+ def save_skill_tree_file(self, filepath: Path):
+ """
+ Saves skill tree to json or png file
+
+ Parameters:
+ - :param filepath: path to skill tree file
+ """
+ extension = filepath.suffix.lower()
+ skill_tree = {
+ 'space_skills': self._build['space_skills'],
+ 'ground_skills': self._build['ground_skills'],
+ 'skill_unlocks': self._build['skill_unlocks'],
+ 'skill_desc': self._build['skill_desc'],
+ }
+ if extension == '.json':
+ store_json__new(skill_tree, filepath)
+ elif extension == '.png':
+ image = self._window.grab().toImage()
+ self.encode_in_image(image, json__dumps(skill_tree))
+ image.save(filepath)
+
+ def merge_build(self, original_build: dict[str, dict[str]], new_build: dict[str, dict[str]]):
+ """
+ updates `original_build` with contents of `new_build`
+ """
+ for build_segment in original_build:
+ subdict = new_build.get(build_segment, None)
+ if subdict is None:
+ continue
+ if isinstance(subdict, dict):
+ original_build[build_segment].update(subdict)
+ else:
+ original_build[build_segment] = subdict
+
+ def update_build_version(self, build: dict[str]):
+ """
+ Updates contents of `build` to match the newest version.
+
+ Parameters:
+ - :param build: contains build data of outdated version
+ """
+ def _fix_boff_seat(environment):
+ for rank_id in range(4):
+ if isinstance(boff_seat[rank_id], dict) and 'rank' not in boff_seat[rank_id]:
+ ability_name = boff_seat[rank_id]['item']
+ prof_abilities = self._cargo.boff_abilities[environment][prof]
+ spec_abilities = self._cargo.boff_abilities[environment].get(spec, None)
+ for rank in ('III', 'II', 'I'):
+ if f'{ability_name} {rank}' in prof_abilities[rank_id]:
+ boff_seat[rank_id]['rank'] = rank
+ break
+ elif (spec_abilities is not None
+ and f'{ability_name} {rank}' in spec_abilities[rank_id]):
+ boff_seat[rank_id]['rank'] = rank
+ break
+ else:
+ boff_seat[rank_id] = ''
+
+ for boff_seat, (prof, spec) in zip(build['space']['boffs'], build['space']['boff_specs']):
+ _fix_boff_seat('space')
+ for station_id, boff_seat in enumerate(build['ground']['boffs']):
+ prof = build['ground']['boff_profs'][station_id]
+ spec = build['ground']['boff_specs'][station_id]
+ _fix_boff_seat('ground')
+
+ build['_version'] = BUILD_VERSION
+
+ def remove_invalid_build_items(self, build: dict[str, int | dict[str]]):
+ """
+ Checks build for invalid items and removes these to maintain compatibility.
+
+ Parameters:
+ - :param build: build to remove items from (in place)
+ """
+ for environment in ('space', 'ground'):
+ for category, category_items in build[environment].items():
+ if isinstance(category_items, str):
+ continue
+ elif category == 'boffs':
+ for station in category_items:
+ for index, ability in enumerate(station):
+ if (isinstance(ability, dict)
+ and ability['item'] not in self._cargo.images_set):
+ station[index] = ''
+ elif (category.startswith('doff')
+ or category == 'boff_specs'
+ or category == 'boff_profs'):
+ continue
+ elif isinstance(category_items, list):
+ for index, item in enumerate(category_items):
+ if isinstance(item, dict) and item['item'] not in self._cargo.images_set:
+ category_items[index] = ''
+
+ def encode_in_image(self, image: QImage, data: str):
+ """
+ Embeds data into image
+
+ Parameters:
+ - :param image: image to edit
+ - :param data: data string to embed into image
+ """
+ data_bytes = zlib_compress(bytes(data, encoding='utf-8'))
+ total_characters = len(data_bytes)
+ bits = np__zeros(total_characters * 8 + 32 + 8, dtype=uint8)
+ prefix = np__array(
+ [167, total_characters >> 8, total_characters & 0b11111111, 167], dtype=uint8)
+ bits[0:32] = np__unpackbits(prefix)
+ bits[32:-8] = np__unpackbits(np__fromiter(data_bytes, dtype=uint8, count=total_characters))
+ bits[-8:] = np__unpackbits(np__array([167], dtype=uint8))
+ total_characters += 5 # prefix and suffix length
+ w = image.width()
+ total_bits = total_characters * 8
+ full_rows = total_bits // (w * 3)
+ additional_pixels = (total_bits - full_rows * w * 3) // 3
+ additional_subpixels = total_bits % 3
+ i = -1
+ row = -1
+ for row in range(full_rows):
+ row_data = image.scanLine(row)
+ for i, subpixel in pixel_range(w, i + 1):
+ row_data[subpixel] = row_data[subpixel] & 0b11111110 | bits[i]
+ row_data = image.scanLine(row + 1)
+ for i, subpixel in pixel_range(additional_pixels, i + 1):
+ row_data[subpixel] = row_data[subpixel] & 0b11111110 | bits[i]
+ if additional_pixels == 0:
+ subpixel = -2
+ if additional_subpixels == 1:
+ row_data[subpixel + 2] = row_data[subpixel + 2] & 0b11111110 | bits[i + 1]
+ elif additional_subpixels == 2:
+ row_data[subpixel + 2] = row_data[subpixel + 2] & 0b11111110 | bits[i + 1]
+ row_data[subpixel + 3] = row_data[subpixel + 3] & 0b11111110 | bits[i + 2]
+
+ def decode_from_image(self, image: QImage) -> str:
+ """
+ Extracts embedded data from image; returns empty string if no data was found
+
+ Parameters:
+ - :param image: image with embedded data
+ """
+ # prefix: §15000§ where 15000 is the number (as uint16) of bytes the encoded data occupies
+ prefix_bits = np__zeros(32, dtype=uint8)
+ first_row = image.constScanLine(0)
+ for i, subpixel in pixel_range(10):
+ prefix_bits[i] = first_row[subpixel] & 0b1
+ prefix_bits[30] = first_row[40] & 0b1
+ prefix_bits[31] = first_row[41] & 0b1
+ prefix_bytes = np__packbits(prefix_bits)
+ if prefix_bytes[0] != 167 or prefix_bytes[3] != 167: # ord('§') == 167
+ return ''
+ total_characters = int(prefix_bytes[1]) << 8 | int(prefix_bytes[2]) # constructs 16-bit int
+ total_characters += 5 # prefix and suffix length
+ w = image.width()
+ total_bits = total_characters * 8
+ bits = np__zeros(total_bits, dtype=uint8)
+ full_rows = total_bits // (w * 3)
+ additional_pixels = (total_bits - full_rows * w * 3) // 3
+ additional_subpixels = total_bits % 3
+ i = -1
+ row = -1
+ for row in range(full_rows):
+ row_data = image.constScanLine(row)
+ for i, subpixel in pixel_range(w, i + 1):
+ bits[i] = row_data[subpixel] & 0b1
+ row_data = image.constScanLine(row + 1)
+ for i, subpixel in pixel_range(additional_pixels, i + 1):
+ bits[i] = row_data[subpixel] & 0b1
+ if additional_pixels == 0:
+ subpixel = -2
+ if additional_subpixels == 1:
+ bits[i + 1] = row_data[subpixel + 2] & 0b1
+ elif additional_subpixels == 2:
+ bits[i + 1] = row_data[subpixel + 2] & 0b1
+ bits[i + 2] = row_data[subpixel + 3] & 0b1
+ decoded_bytes = bytes(np__packbits(bits))
+ if decoded_bytes[-1] != 167:
+ return ''
+ return str(zlib_decompress(decoded_bytes[4:-1]), 'utf-8')
+
+ def map_build_items(self, old_build: dict, new_build: dict, mapping):
+ """
+ Inserts items from old build into new build according to mapping; in-place
+
+ Parameters:
+ - :param old_build: source
+ - :param new_build: target
+ - :param mapping: iterable of 2-tuples containing source and target key
+ """
+ for source_key, target_key in mapping:
+ try:
+ if isinstance(new_build[target_key], list):
+ for index, element in enumerate(old_build[source_key]):
+ try:
+ if isinstance(element, dict) and 'modifiers' in element:
+ element['modifiers'] += [None] * (5 - len(element['modifiers']))
+ new_build[target_key][index] = element
+ except IndexError:
+ break
+ else:
+ new_build[target_key] = old_build[source_key]
+ except KeyError:
+ continue
+
+ def load_legacy_build_image(self):
+ """
+ Loads legacy build from image file
+ """
+ load_path = browse_path(
+ self._config.config_subfolders['library'],
+ 'PNG image (*.png);;Any File (*.*)', parent_window=self._window)
+ if load_path is not None:
+ if load_path.suffix.lower() != '.png':
+ return
+ raw_build = self.legacy_decode_from_image(load_path)
+ try:
+ build_data = json__loads(self.compensate_old_build(raw_build))
+ except JSONDecodeError:
+ return
+ if 'versionJSON' in build_data:
+ new_build = empty_build()
+ new_build.update(self.convert_old_build(build_data))
+ self._build.data = new_build
+ try:
+ self._build.load_build()
+ except KeyError:
+ self.remove_invalid_build_items(self._build.data)
+ self._build.load_build()
+
+ def legacy_decode_from_image(self, image_path: str) -> str:
+ """
+ Decodes build from image using old embedding specification.
+
+ Parameters:
+ - :param image_path: path to image
+ """
+ message = ''
+ image = QImage(image_path)
+ width = image.width()
+ pixel_num = width * 3
+ bit_diff = pixel_num % 8
+ decoded_binary = np__zeros(pixel_num, dtype=uint8)
+ extra_bits = np__zeros(0, dtype=uint8)
+ for line in range(image.height()):
+ data = image.constScanLine(line)
+ for col in range(width):
+ pixel_index = col * 4
+ bin_index = col * 3
+ decoded_binary[bin_index] = data[pixel_index + 2] & 0b1
+ decoded_binary[bin_index + 1] = data[pixel_index + 1] & 0b1
+ decoded_binary[bin_index + 2] = data[pixel_index] & 0b1
+ if bit_diff == 0:
+ decoded_bytes = np__packbits(np__append(extra_bits, decoded_binary))
+ extra_bits = np__zeros(0, dtype=uint8)
+ bit_diff = pixel_num % 8
+ else:
+ decoded_bytes = np__packbits(np__append(extra_bits, decoded_binary[:-1 * bit_diff]))
+ extra_bits = decoded_binary[-1 * bit_diff:].copy()
+ bit_diff = (pixel_num + len(extra_bits)) % 8
+ new_message = ''.join(map(chr, decoded_bytes))
+ message += new_message
+ if '$t3g0' in new_message:
+ break
+ return message.split('$t3g0', maxsplit=1)[0]
+
+ def compensate_old_build(self, build: str):
+ """
+ replaces known wrong terms in build string
+ """
+ build = build.replace('Ultra rare', 'Ultra Rare')
+ build = build.replace('Very rare', 'Very Rare')
+ return build
+
+ def convert_old_build(self, build: dict) -> dict:
+ """
+ converts build from old spec to current spec
+ """
+ new_build = empty_build(self)
+
+ # space
+ self.map_build_items(build, new_build['space'], BUILD_CONVERSION['space'])
+
+ new_build['space']['traits'] = build['personalSpaceTrait'] + build['personalSpaceTrait2']
+ if len(new_build['space']['traits']) < 12:
+ new_build['space']['traits'] += [None] * (12 - len(new_build['space']['traits']))
+ elite_captain_trait = new_build['space']['traits'][5]
+ new_build['space']['traits'][5] = new_build['space']['traits'][9]
+ new_build['space']['traits'][9] = elite_captain_trait
+
+ ship_data = self._cargo.ships[new_build['space']['ship']]
+ boff_data = sorted(map(lambda s: get_boff_spec(s), ship_data['boffs']), reverse=True)
+ boff_data_old = []
+ for boff_id, boff_profession in enumerate(build['boffseats']['space']):
+ if f'spaceBoff_{boff_id}' in build['boffs'] and boff_profession is not None:
+ abilities = build['boffs'][f'spaceBoff_{boff_id}']
+ boff_data_old.append((len(abilities), boff_profession, abilities))
+ boff_data_old.sort(reverse=True)
+ for boff_id, (new_station, old_station) in enumerate(zip(boff_data, boff_data_old)):
+ if new_station[1] == old_station[1] or new_station[1] == 'Universal':
+ continue
+ for i, test_station in enumerate(boff_data):
+ if old_station[0] == test_station[0] and old_station[1] == test_station[1]:
+ boff_data_old[boff_id] = boff_data_old[i]
+ boff_data_old[i] = old_station
+ break
+ else:
+ for i, test_station in enumerate(boff_data):
+ if old_station[0] == test_station[0] and test_station[1] == 'Universal':
+ boff_data_old[boff_id] = boff_data_old[i]
+ boff_data_old[i] = old_station
+ break
+ for boff_id, station in enumerate(boff_data_old):
+ new_build['space']['boff_specs'][boff_id] = [station[1], boff_data[boff_id][2]]
+ for i, ability in enumerate(station[2]):
+ if ability is None or ability == '':
+ new_build['space']['boffs'][boff_id][i] = ''
+ else:
+ new_build['space']['boffs'][boff_id][i] = {'item': ability}
+
+ # ground
+ self.map_build_items(build, new_build['ground'], BUILD_CONVERSION['ground'])
+
+ try:
+ for boff_id in range(4):
+ new_build['ground']['boff_profs'][boff_id] = build['boffseats']['ground'][boff_id]
+ new_build['ground']['boff_specs'][boff_id] = (
+ build['boffseats']['ground_spec'][boff_id])
+ if new_build['ground']['boff_specs'][boff_id] is None:
+ new_build['ground']['boff_specs'][boff_id] = 'Command'
+ for i, ability in enumerate(build['boffs'][f'groundBoff_{boff_id}']):
+ if ability is None or ability == '':
+ new_build['ground']['boffs'][boff_id][i]
+ else:
+ new_build['ground']['boffs'][boff_id][i] = {'item': ability}
+ except KeyError:
+ pass
+
+ new_build['ground']['traits'] = build['personalGroundTrait'] + build['personalGroundTrait2']
+ if len(new_build['ground']['traits']) < 12:
+ new_build['ground']['traits'] += [None] * (12 - len(new_build['ground']['traits']))
+ elite_captain_trait = new_build['ground']['traits'][5]
+ new_build['ground']['traits'][5] = new_build['ground']['traits'][9]
+ new_build['ground']['traits'][9] = elite_captain_trait
+
+ # captain
+ self.map_build_items(build, new_build['captain'], BUILD_CONVERSION['captain'])
+ try:
+ new_build['captain']['name'] = build['playerName'] + build['playerHandle']
+ new_build['captain']['faction'] = build['captain']['faction']
+ except KeyError:
+ pass
+
+ # doffs
+ for environment in ('space', 'ground'):
+ for doff_index, doff in enumerate(build['doffs'][environment]):
+ if doff is not None and doff != '':
+ new_build[environment]['doffs_spec'][doff_index] = doff['spec']
+ try:
+ for variant in getattr(self._cargo, f'{environment}_doffs')[doff['spec']]:
+ if doff['effect'] in variant:
+ new_build[environment]['doffs_variant'][doff_index] = variant
+ break
+ except KeyError:
+ pass
+
+ return new_build
diff --git a/src/buildmanager.py b/src/buildmanager.py
index 434cecf..b3829fa 100644
--- a/src/buildmanager.py
+++ b/src/buildmanager.py
@@ -160,6 +160,15 @@ def __init__(
'ground_points_total': 0
}
+ @property
+ def data(self) -> dict[str, int | dict[str]]:
+ """Raw build data"""
+ return self._build_data
+
+ @data.setter
+ def data(self, build_data: dict[str, int | dict[str]]):
+ self._build_data = build_data
+
def autosave(self):
"""
Saves build to autosave file.
@@ -192,7 +201,7 @@ def set(
def load_build(self):
"""
- Updates UI to show the build currently in self._build_data
+ Updates UI to show the build currently in self._build_data.
"""
self._building = True
# ship section
diff --git a/src/callbacks.py b/src/callbacks.py
index 188a4f4..62edc27 100644
--- a/src/callbacks.py
+++ b/src/callbacks.py
@@ -108,59 +108,3 @@ def clear_all(self):
clear_ground_skills(self)
self.building = False
self.autosave()
-
-
-def load_build_callback(self):
- """
- Loads build from file
- """
- load_path = browse_path(
- self, str(self.config.config_subfolders['library']),
- 'SETS Files (*.json *.png);;JSON file (*.json);;PNG image (*.png);;Any File (*.*)')
- if load_path != '':
- load_build_file(self, load_path)
-
-
-def load_skills_callback(self):
- """
- Loads skills from file
- """
- load_path = browse_path(
- self, str(self.config.config_subfolders['library']),
- 'SETS Files (*.json *.png);;JSON file (*.json);;PNG image (*.png);;Any File (*.*)')
- if load_path != '':
- load_skill_tree_file(self, load_path)
-
-
-def save_build_callback(self):
- """
- Saves build to file
- """
- if self.widgets.ship['button'].text() == '':
- proposed_filename = '(Ship Template)'
- else:
- proposed_filename = f"({self.widgets.ship['button'].text()})"
- if self.widgets.ship['name'].text() != '':
- proposed_filename = f"{self.widgets.ship['name'].text()} {proposed_filename}"
- default_path = str(self.config.config_subfolders['library'] / proposed_filename)
- if self.settings.default_save_format == 'PNG':
- file_types = 'PNG image (*.png);;JSON file (*.json);;Any File (*.*)'
- else:
- file_types = 'JSON file (*.json);;PNG image (*.png);;Any File (*.*)'
- save_path = browse_path(self, default_path, file_types, save=True)
- if save_path != '':
- save_build_file(self, save_path)
-
-
-def save_skills_callback(self):
- """
- Save skills to file
- """
- default_path = str(self.config.config_subfolders['library'] / 'Skill Tree')
- if self.settings.default_save_format == 'PNG':
- file_types = 'PNG image (*.png);;JSON file (*.json);;Any File (*.*)'
- else:
- file_types = 'JSON file (*.json);;PNG image (*.png);;Any File (*.*)'
- save_path = browse_path(self, default_path, file_types, save=True)
- if save_path != '':
- save_skill_tree_file(self, save_path)
diff --git a/src/constants.py b/src/constants.py
index 0e58ff1..5e17380 100644
--- a/src/constants.py
+++ b/src/constants.py
@@ -208,6 +208,10 @@
SKILL_POINTS_FOR_RANK = (0, 5, 15, 25, 35)
+SETS_FILE_FILTER = (
+ 'SETS Files (*.json *.png);;JSON file (*.json);;PNG image (*.png);;Any File (*.*)'
+)
+
# commented maps must be transferred manually
BUILD_CONVERSION = {
'space': (
diff --git a/src/datafunctions.py b/src/datafunctions.py
index 666753e..9f0923e 100644
--- a/src/datafunctions.py
+++ b/src/datafunctions.py
@@ -439,453 +439,6 @@ def cache_skills(skill_cache: dict[str, dict], app_directory: str):
skill_cache['ground_unlocks'] = ground_skill_data['ground_unlocks']
-def autosave(self):
- """
- Saves build to autosave file.
- """
- if not self.building:
- store_json(self.build, str(self.config.autosave_path))
-
-
-def map_build_items(self, old_build: dict, new_build: dict, mapping):
- """
- Inserts items from old build into new build according to mapping; in-place
-
- Parameters:
- - :param old_build: source
- - :param new_build: target
- - :param mapping: iterable of 2-tuples containing source and target key
- """
- for source_key, target_key in mapping:
- try:
- if isinstance(new_build[target_key], list):
- for index, element in enumerate(old_build[source_key]):
- try:
- if isinstance(element, dict) and 'modifiers' in element:
- element['modifiers'] += [None] * (5 - len(element['modifiers']))
- new_build[target_key][index] = element
- except IndexError:
- break
- else:
- new_build[target_key] = old_build[source_key]
- except KeyError:
- continue
-
-
-def convert_old_build(self, build: dict) -> dict:
- """
- converts build from old spec to current spec
- """
- new_build = empty_build(self)
-
- # space
- map_build_items(self, build, new_build['space'], BUILD_CONVERSION['space'])
-
- new_build['space']['traits'] = build['personalSpaceTrait'] + build['personalSpaceTrait2']
- if len(new_build['space']['traits']) < 12:
- new_build['space']['traits'] += [None] * (12 - len(new_build['space']['traits']))
- elite_captain_trait = new_build['space']['traits'][5]
- new_build['space']['traits'][5] = new_build['space']['traits'][9]
- new_build['space']['traits'][9] = elite_captain_trait
-
- ship_data = self.cache.ships[new_build['space']['ship']]
- boff_data = sorted(map(lambda s: get_boff_spec(self, s), ship_data['boffs']), reverse=True)
- boff_data_old = []
- for boff_id, boff_profession in enumerate(build['boffseats']['space']):
- if f'spaceBoff_{boff_id}' in build['boffs'] and boff_profession is not None:
- abilities = build['boffs'][f'spaceBoff_{boff_id}']
- boff_data_old.append((len(abilities), boff_profession, abilities))
- boff_data_old.sort(reverse=True)
- for boff_id, (new_station, old_station) in enumerate(zip(boff_data, boff_data_old)):
- if new_station[1] == old_station[1] or new_station[1] == 'Universal':
- continue
- for i, test_station in enumerate(boff_data):
- if old_station[0] == test_station[0] and old_station[1] == test_station[1]:
- boff_data_old[boff_id] = boff_data_old[i]
- boff_data_old[i] = old_station
- break
- else:
- for i, test_station in enumerate(boff_data):
- if old_station[0] == test_station[0] and test_station[1] == 'Universal':
- boff_data_old[boff_id] = boff_data_old[i]
- boff_data_old[i] = old_station
- break
- for boff_id, station in enumerate(boff_data_old):
- new_build['space']['boff_specs'][boff_id] = [station[1], boff_data[boff_id][2]]
- for i, ability in enumerate(station[2]):
- if ability is None or ability == '':
- new_build['space']['boffs'][boff_id][i] = ''
- else:
- new_build['space']['boffs'][boff_id][i] = {'item': ability}
-
- # ground
- map_build_items(self, build, new_build['ground'], BUILD_CONVERSION['ground'])
-
- try:
- for boff_id in range(4):
- new_build['ground']['boff_profs'][boff_id] = build['boffseats']['ground'][boff_id]
- new_build['ground']['boff_specs'][boff_id] = build['boffseats']['ground_spec'][boff_id]
- if new_build['ground']['boff_specs'][boff_id] is None:
- new_build['ground']['boff_specs'][boff_id] = 'Command'
- for i, ability in enumerate(build['boffs'][f'groundBoff_{boff_id}']):
- if ability is None or ability == '':
- new_build['ground']['boffs'][boff_id][i]
- else:
- new_build['ground']['boffs'][boff_id][i] = {'item': ability}
- except KeyError:
- pass
-
- new_build['ground']['traits'] = build['personalGroundTrait'] + build['personalGroundTrait2']
- if len(new_build['ground']['traits']) < 12:
- new_build['ground']['traits'] += [None] * (12 - len(new_build['ground']['traits']))
- elite_captain_trait = new_build['ground']['traits'][5]
- new_build['ground']['traits'][5] = new_build['ground']['traits'][9]
- new_build['ground']['traits'][9] = elite_captain_trait
-
- # captain
- map_build_items(self, build, new_build['captain'], BUILD_CONVERSION['captain'])
- try:
- new_build['captain']['name'] = build['playerName'] + build['playerHandle']
- new_build['captain']['faction'] = build['captain']['faction']
- except KeyError:
- pass
-
- # doffs
- for environment in ('space', 'ground'):
- for doff_index, doff in enumerate(build['doffs'][environment]):
- if doff is not None and doff != '':
- new_build[environment]['doffs_spec'][doff_index] = doff['spec']
- try:
- for variant in getattr(self.cache, f'{environment}_doffs')[doff['spec']]:
- if doff['effect'] in variant:
- new_build[environment]['doffs_variant'][doff_index] = variant
- break
- except KeyError:
- pass
-
- return new_build
-
-
-def compensate_old_build(self, build: str):
- """
- replaces known wrong terms in build string
- """
- build = build.replace('Ultra rare', 'Ultra Rare')
- build = build.replace('Very rare', 'Very Rare')
- return build
-
-
-def remove_invalid_build_items(self, build: dict):
- """
- Checks build for invalid items and removes these to maintain compatibility.
-
- Parameters:
- - :param build: build to remove items from (in place)
- """
- for environment in ('space', 'ground'):
- for category, category_items in build[environment].items():
- if isinstance(category_items, str):
- continue
- elif category == 'boffs':
- for station in category_items:
- for index, ability in enumerate(station):
- if (isinstance(ability, dict)
- and ability['item'] not in self.cache.images_set):
- station[index] = ''
- elif (category.startswith('doff')
- or category == 'boff_specs'
- or category == 'boff_profs'):
- continue
- elif isinstance(category_items, list):
- for index, item in enumerate(category_items):
- if isinstance(item, dict) and item['item'] not in self.cache.images_set:
- category_items[index] = ''
-
-
-def update_build_version(self, build: dict[str]):
- """
- Updates contents of `build` to match the newest version.
-
- Parameters:
- - :param build: contains build data of outdated version
- """
- def _fix_station(environment):
- for rank_id in range(4):
- if isinstance(boff_station[rank_id], dict) and 'rank' not in boff_station[rank_id]:
- ability_name = boff_station[rank_id]['item']
- prof_abilities = self.cache.boff_abilities[environment][prof]
- spec_abilities = self.cache.boff_abilities[environment].get(spec, None)
- for rank in ('III', 'II', 'I'):
- if f'{ability_name} {rank}' in prof_abilities[rank_id]:
- boff_station[rank_id]['rank'] = rank
- break
- elif (spec_abilities is not None
- and f'{ability_name} {rank}' in spec_abilities[rank_id]):
- boff_station[rank_id]['rank'] = rank
- break
- else:
- boff_station[rank_id] = ''
-
- for boff_station, (prof, spec) in zip(build['space']['boffs'], build['space']['boff_specs']):
- _fix_station('space')
- for station_id, boff_station in enumerate(build['ground']['boffs']):
- prof = build['ground']['boff_profs'][station_id]
- spec = build['ground']['boff_specs'][station_id]
- _fix_station('ground')
-
- build['_version'] = BUILD_VERSION
-
-
-def encode_in_image(self, image: QImage, data: str):
- """
- Embeds data into image
-
- Parameters:
- - :param image: image to edit
- - :param data: data string to embed into image
- """
- data_bytes = zlib_compress(bytes(data, encoding='utf-8'))
- total_characters = len(data_bytes)
- bits = zeros(total_characters * 8 + 32 + 8, dtype=uint8)
- prefix = array([167, total_characters >> 8, total_characters & 0b11111111, 167], dtype=uint8)
- bits[0:32] = unpackbits(prefix)
- bits[32:-8] = unpackbits(fromiter(data_bytes, dtype=uint8, count=total_characters))
- bits[-8:] = unpackbits(array([167], dtype=uint8))
- total_characters += 5 # prefix and suffix length
- w = image.width()
- total_bits = total_characters * 8
- full_rows = total_bits // (w * 3)
- additional_pixels = (total_bits - full_rows * w * 3) // 3
- additional_subpixels = total_bits % 3
- i = -1
- row = -1
- for row in range(full_rows):
- row_data = image.scanLine(row)
- for i, subpixel in pixel_range(w, i + 1):
- row_data[subpixel] = row_data[subpixel] & 0b11111110 | bits[i]
- row_data = image.scanLine(row + 1)
- for i, subpixel in pixel_range(additional_pixels, i + 1):
- row_data[subpixel] = row_data[subpixel] & 0b11111110 | bits[i]
- if additional_pixels == 0:
- subpixel = -2
- if additional_subpixels == 1:
- row_data[subpixel + 2] = row_data[subpixel + 2] & 0b11111110 | bits[i + 1]
- elif additional_subpixels == 2:
- row_data[subpixel + 2] = row_data[subpixel + 2] & 0b11111110 | bits[i + 1]
- row_data[subpixel + 3] = row_data[subpixel + 3] & 0b11111110 | bits[i + 2]
-
-
-def decode_from_image(self, image: QImage) -> str:
- """
- Extracts embedded data from image; returns empty string if no data was found
-
- Parameters:
- - :param image: image with embedded data
- """
- # prefix: §15000§ where 15000 is the number (as uint16) of bytes the encoded data occupies
- prefix_bits = zeros(32, dtype=uint8)
- first_row = image.constScanLine(0)
- for i, subpixel in pixel_range(10):
- prefix_bits[i] = first_row[subpixel] & 0b1
- prefix_bits[30] = first_row[40] & 0b1
- prefix_bits[31] = first_row[41] & 0b1
- prefix_bytes = packbits(prefix_bits)
- if prefix_bytes[0] != 167 or prefix_bytes[3] != 167: # ord('§') == 167
- return ''
- total_characters = int(prefix_bytes[1]) << 8 | int(prefix_bytes[2]) # constructs 16-bit int
- total_characters += 5 # prefix and suffix length
- w = image.width()
- total_bits = total_characters * 8
- bits = zeros(total_bits, dtype=uint8)
- full_rows = total_bits // (w * 3)
- additional_pixels = (total_bits - full_rows * w * 3) // 3
- additional_subpixels = total_bits % 3
- i = -1
- row = -1
- for row in range(full_rows):
- row_data = image.constScanLine(row)
- for i, subpixel in pixel_range(w, i + 1):
- bits[i] = row_data[subpixel] & 0b1
- row_data = image.constScanLine(row + 1)
- for i, subpixel in pixel_range(additional_pixels, i + 1):
- bits[i] = row_data[subpixel] & 0b1
- if additional_pixels == 0:
- subpixel = -2
- if additional_subpixels == 1:
- bits[i + 1] = row_data[subpixel + 2] & 0b1
- elif additional_subpixels == 2:
- bits[i + 1] = row_data[subpixel + 2] & 0b1
- bits[i + 2] = row_data[subpixel + 3] & 0b1
- decoded_bytes = bytes(packbits(bits))
- if decoded_bytes[-1] != 167:
- raise ValueError('End delimiter not found! Decoded data not intact.')
- return str(zlib_decompress(decoded_bytes[4:-1]), 'utf-8')
-
-
-def legacy_decode_from_image(self, image_path: str) -> str:
- """
- Decodes build from image using old embedding specification.
-
- Parameters:
- - :param image_path: path to image
- """
- message = ''
- image = QImage(image_path)
- width = image.width()
- pixel_num = width * 3
- bit_diff = pixel_num % 8
- decoded_binary = zeros(pixel_num, dtype=uint8)
- extra_bits = zeros(0, dtype=uint8)
- for line in range(image.height()):
- data = image.constScanLine(line)
- for col in range(width):
- pixel_index = col * 4
- bin_index = col * 3
- decoded_binary[bin_index] = data[pixel_index + 2] & 0b1
- decoded_binary[bin_index + 1] = data[pixel_index + 1] & 0b1
- decoded_binary[bin_index + 2] = data[pixel_index] & 0b1
- if bit_diff == 0:
- decoded_bytes = packbits(append(extra_bits, decoded_binary))
- extra_bits = zeros(0, dtype=uint8)
- bit_diff = pixel_num % 8
- else:
- decoded_bytes = packbits(append(extra_bits, decoded_binary[:-1 * bit_diff]))
- extra_bits = decoded_binary[-1 * bit_diff:].copy()
- bit_diff = (pixel_num + len(extra_bits)) % 8
- new_message = ''.join(map(chr, decoded_bytes))
- message += new_message
- if '$t3g0' in new_message:
- break
- return message.split('$t3g0', maxsplit=1)[0]
-
-
-def load_legacy_build_image(self):
- """
- Loads legacy build from image file
- """
- load_path = browse_path(
- self, self.config.config_subfolders['library'],
- 'PNG image (*.png);;Any File (*.*)')
- if load_path != '':
- _, _, extension = load_path.rpartition('.')
- if extension.lower() != 'png':
- return
- try:
- raw_build = legacy_decode_from_image(self, load_path)
- build_data = json__loads(compensate_old_build(self, raw_build))
- except JSONDecodeError:
- sys.stderr.write('[Error] Image contains no build or is corrupted.')
- return
- if 'versionJSON' in build_data:
- new_build = empty_build(self)
- new_build.update(convert_old_build(self, build_data))
- self.build = new_build
- try:
- load_build(self)
- except KeyError:
- remove_invalid_build_items(self, self.build)
- load_build(self)
-
-
-def load_build_file(self, filepath: str, update_ui: bool = True):
- """
- Loads build from json or png file and puts it into self.build
-
- Parameters:
- - :param filepath: path to build file
- """
- _, _, extension = filepath.rpartition('.')
- if extension.lower() == 'json':
- build_data = load_json(filepath)
- elif extension.lower() == 'png':
- decoded_str = decode_from_image(self, QImage(filepath))
- if decoded_str == '':
- return
- build_data = json__loads(decoded_str)
- else:
- return
- new_build = empty_build(self)
- if build_data.get('_version', -1) == BUILD_VERSION:
- merge_build(self, new_build, build_data)
- elif 'versionJSON' in build_data:
- build_data = json__loads(compensate_old_build(self, json__dumps(build_data)))
- new_build.update(convert_old_build(self, build_data))
- else:
- merge_build(self, new_build, build_data)
- update_build_version(self, new_build)
- self.build = new_build
- if update_ui:
- try:
- load_build(self)
- except KeyError:
- remove_invalid_build_items(self, self.build)
- load_build(self)
-
-
-def save_build_file(self, filepath: str):
- """
- Saves build to json or png file
-
- Parameters:
- - :param filepath: path to build file
- """
- _, _, extension = filepath.rpartition('.')
- if extension.lower() == 'json':
- store_json(self.build, filepath)
- elif extension.lower() == 'png':
- image = self.window.grab().toImage()
- encode_in_image(self, image, json__dumps(self.build))
- image.save(filepath)
-
-
-def load_skill_tree_file(self, filepath: str):
- """
- Loads skill tree from json or png file and puts it into self.build
-
- Parameters:
- - :param filepath: path to skill tree file
- """
- _, _, extension = filepath.rpartition('.')
- if extension.lower() == 'json':
- build_data = load_json(filepath)
- elif extension.lower() == 'png':
- decoded_str = decode_from_image(self, QImage(filepath))
- if decoded_str == '':
- return
- build_data = json__loads(decoded_str)
- else:
- return
- new_build = empty_build(self, 'skills')
- merge_build(self, new_build, build_data)
- self.build['space_skills'] = new_build['space_skills']
- self.build['ground_skills'] = new_build['ground_skills']
- self.build['skill_unlocks'] = new_build['skill_unlocks']
- self.build['skill_desc'] = new_build['skill_desc']
- load_skill_pages(self)
-
-
-def save_skill_tree_file(self, filepath: str):
- """
- Saves skill tree to json or png file
-
- Parameters:
- - :param filepath: path to skill tree file
- """
- _, _, extension = filepath.rpartition('.')
- skill_tree = {
- 'space_skills': self.build['space_skills'],
- 'ground_skills': self.build['ground_skills'],
- 'skill_unlocks': self.build['skill_unlocks'],
- 'skill_desc': self.build['skill_desc'],
- }
- if extension.lower() == 'json':
- store_json(skill_tree, filepath)
- elif extension.lower() == 'png':
- image = self.window.grab().toImage()
- encode_in_image(self, image, json__dumps(skill_tree))
- image.save(filepath)
-
-
def empty_build(self, build_type: str = 'full') -> dict:
"""
Creates empty build and returns it.
@@ -988,35 +541,6 @@ def empty_build(self, build_type: str = 'full') -> dict:
return new_skills
-def merge_build(self, original_build: dict, new_build: dict):
- """
- updates `original_build` with contents of `new_build`
- """
- for build_segment in original_build:
- subdict = new_build.get(build_segment, None)
- if subdict is None:
- continue
- if isinstance(subdict, dict):
- original_build[build_segment].update(subdict)
- else:
- original_build[build_segment] = subdict
-
-
-def pixel_range(num: int = 0, range_start: int = 0, /):
- """
- Returns appropriate indices to access the RGB (not A) channels of the pixel row of `num` pixels,
- as well as an 1-step increasing range index -> (range_index, pixel_index)
- """
- counter = range_start
- for index in range(0, num * 4, 4):
- yield counter, index
- counter += 1
- yield counter, index + 1
- counter += 1
- yield counter, index + 2
- counter += 1
-
-
def backup_cargo_data(self):
"""
Saves current cargo data to backup folder.
diff --git a/src/iofunc.py b/src/iofunc.py
index 1637b4a..9d99c5b 100644
--- a/src/iofunc.py
+++ b/src/iofunc.py
@@ -10,7 +10,7 @@
from webbrowser import open as webbrowser_open
from PySide6.QtGui import QIcon, QImage, QPixmap
-from PySide6.QtWidgets import QFileDialog
+from PySide6.QtWidgets import QFileDialog, QWidget
import requests
from requests.cookies import create_cookie as requests__create_cookie
from requests_html import HTMLSession
@@ -33,30 +33,36 @@ def join(self):
return self._return
-def browse_path(self, default_path: str = None, types: str = 'Any File (*.*)', save=False) -> str:
+def browse_path(
+ preset_path: Path, types: str = 'Any File (*.*)', save: bool = False,
+ parent_window: QWidget | None = None) -> Path | None:
"""
Opens file dialog prompting the user to select a file.
Parameters:
- - :param default_path: path that the file dialog opens at
- - :param types: string containing all file extensions and their respective names that are
- allowed.
- Format: " (*.);; (*.);; [...]"
- Example: "Logfile (*.log);;Any File (*.*)"
- """
- if default_path is None or default_path == '':
- default_path = self.app_dir
- default_path = os.path.abspath(default_path)
- if not os.path.exists(os.path.dirname(default_path)):
- default_path = self.app_dir
+ - :param preset_path: path that the file dialog opens at; includes default file name
+ - :param types: string containing all file extensions and their respective names that are \
+ allowed. Format: ` (*.);; (*.);; \
+ [...]` Example: `Logfile (*.log);;Any File (*.*)`
+ - :param save: False => open file with dialog; True => save file with dialog
+ - :param parent_window: window to use as parent; uses window icon and name of parent window
+
+ :return: returns selected path; None if user aborts or tries to open not-existing file
+ """
if save:
- file, filter = QFileDialog.getSaveFileName(self.window, 'Save...', default_path, types)
- selected_extension = filter.rpartition('.')[2][:-1]
- if file.rpartition('.')[2].lower() != selected_extension:
- file += f".{selected_extension}"
+ f = QFileDialog.getSaveFileName(parent_window, 'Save Log', str(preset_path), types)[0]
+ if f == '':
+ return None
+ return Path(f)
else:
- file, _ = QFileDialog.getOpenFileName(self.window, 'Open...', default_path, types)
- return file
+ f = QFileDialog.getOpenFileName(parent_window, 'Open Log', str(preset_path), types)[0]
+ if f == '':
+ return None
+ selected_path = Path(f)
+ if selected_path.exists():
+ return selected_path
+ else:
+ return None
def get_cargo_data(self, filename: str, url: str, ignore_cache_age=False) -> dict | list:
diff --git a/src/widgets.py b/src/widgets.py
index 980cf35..b2a52af 100644
--- a/src/widgets.py
+++ b/src/widgets.py
@@ -652,6 +652,27 @@ def __iter__(self):
return self._gen
+class pixel_range():
+ """
+ Returns appropriate indices to access the RGB (not A) channels of the pixel row of `num` pixels,
+ as well as an 1-step increasing range index -> (range_index, pixel_index)
+ """
+ def __init__(self, num: int = 0, range_start: int = 0, /):
+ def generator():
+ counter = range_start
+ for index in range(0, num * 4, 4):
+ yield counter, index
+ counter += 1
+ yield counter, index + 1
+ counter += 1
+ yield counter, index + 2
+ counter += 1
+ self.__gen = generator()
+
+ def __iter__(self):
+ return self.__gen
+
+
class TooltipLabel(QLabel):
"""Label with tooltip"""
def __init__(self, text: str, tooltip: QLabel):
From d82d7dbfa45d83efd2286858c70ec9384acaea2a Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Tue, 12 May 2026 16:14:20 +0200
Subject: [PATCH 19/44] moving build clear logic to buildmanager
---
src/app.py | 19 ++--
src/buildmanager.py | 225 +++++++++++++++++++++++++++++++++++++++++++-
src/buildupdater.py | 137 ---------------------------
src/callbacks.py | 110 ----------------------
4 files changed, 231 insertions(+), 260 deletions(-)
delete mode 100644 src/callbacks.py
diff --git a/src/app.py b/src/app.py
index d70b706..f79ea33 100644
--- a/src/app.py
+++ b/src/app.py
@@ -37,13 +37,7 @@
class SETS():
- from .callbacks import (
- clear_all, clear_build_callback,
- load_build_callback, load_skills_callback, save_build_callback, save_skills_callback,
- select_ship)
- from .datafunctions import (
- autosave, backup_cargo_data, empty_build,
- init_backend, load_legacy_build_image)
+ from .datafunctions import backup_cargo_data, empty_build, init_backend
from .splash import enter_splash, exit_splash, splash_text
from .style import prepare_tooltip_css
@@ -344,10 +338,11 @@ def setup_main_layout(self):
menu_layout.setColumnStretch(1, 5)
menu_layout.setColumnStretch(2, 2)
left_button_group = {
- 'Save': {'callback': self.save_build_callback},
- 'Open': {'callback': self.load_build_callback},
- 'Clear Current Tab': {'callback': self.clear_build_callback},
- 'Clear All Tabs': {'callback': self.clear_all}
+ 'Save': {'callback': self.build_loader.save_build_callback},
+ 'Open': {'callback': self.build_loader.load_build_callback},
+ 'Clear Current Tab': {'callback': lambda: self.build2.clear_build_callback(
+ self.tabbers.build_tabber.currentIndex())},
+ 'Clear All Tabs': {'callback': self.build2.clear_all}
}
menu_layout.addLayout(
create_button_series2(self.theme2, left_button_group), 0, 0, alignment=ALEFT | ATOP)
@@ -1472,7 +1467,7 @@ def setup_settings_frame(self):
sec_3.setColumnMinimumWidth(1, 3 * isp)
sec_3.setColumnStretch(3, 1)
build_image_button = create_button2(self.theme2, 'Convert Legacy Build Image')
- build_image_button.clicked.connect(self.load_legacy_build_image)
+ build_image_button.clicked.connect(self.build_loader.load_legacy_build_image)
sec_3.addWidget(build_image_button, 0, 0, alignment=ALEFT)
build_image_label = create_label2(
self.theme2, 'Loads build from legacy build image. Use the "Load" button to load '
diff --git a/src/buildmanager.py b/src/buildmanager.py
index b3829fa..8221d55 100644
--- a/src/buildmanager.py
+++ b/src/buildmanager.py
@@ -364,6 +364,201 @@ def align_space_frame(self, ship_data: dict, clear: bool = False):
for boff_to_hide in range(boff_num + 1, 6):
self.update_boff_seat(boff_to_hide, rank=0, profession='', clear=clear, hide_seat=True)
+ def clear_all(self):
+ """
+ Clears space and ground build, skills and captain info
+ """
+ self._building = True
+ self.clear_space_build()
+ self.clear_ground_build()
+ self.clear_captain()
+ self.clear_space_skills()
+ self.clear_ground_skills()
+ self._building = False
+ self.autosave()
+
+ def clear_build_callback(self, current_tab: int):
+ """
+ Clears current build section
+ """
+ self._building = True
+ if current_tab == 0:
+ self.clear_space_build()
+ elif current_tab == 1:
+ self.clear_ground_build()
+ elif current_tab == 2:
+ self.clear_space_skills()
+ elif current_tab == 3:
+ self.clear_ground_skills()
+ self._building = False
+ self.autosave()
+
+ def clear_space_build(self):
+ """
+ clears space build
+ """
+ self.clear_ship()
+ self.align_space_frame(SHIP_TEMPLATE, clear=True)
+ self.clear_traits('space')
+ self.clear_doffs('space')
+
+ def clear_ship(self):
+ """
+ Clears ship section of sidebar
+ """
+ self.ship.image.set_image(self._images.empty)
+ self.ship.button.setText('')
+ self._build_data['space']['ship'] = ''
+ self.ship.tier.clear()
+ self.ship.dc.hide()
+ self.ship.name.setText('')
+ self._build_data['space']['ship_name'] = ''
+ self.ship.desc.setPlainText('')
+ self._build_data['space']['ship_desc'] = ''
+
+ def clear_ground_build(self):
+ """
+ Clears ground build
+ """
+ self.ground.desc.clear()
+ self._build_data['ground']['ground_desc'] = ''
+ self.clear_equipment_cat_ground('kit_modules')
+ self.clear_equipment_cat_ground('weapons')
+ self.clear_equipment_cat_ground('ground_devices')
+ self.clear_equipment_cat_ground('kit')
+ self.clear_equipment_cat_ground('armor')
+ self.clear_equipment_cat_ground('ev_suit')
+ self.clear_equipment_cat_ground('personal_shield')
+ self.clear_boff_seat_ground(0)
+ self.clear_boff_seat_ground(1)
+ self.clear_boff_seat_ground(2)
+ self.clear_boff_seat_ground(3)
+ self.clear_traits('ground')
+ self.clear_doffs('ground')
+
+ def clear_traits(self, environment: str = 'both'):
+ """
+ Clears traits from build and UI
+
+ Parameters:
+ - :param environment: environment to clear the traits from (space/ground/both)
+ """
+ if environment == 'space' or environment == 'both':
+ for i, trait_button in enumerate(self.space.traits):
+ trait_button.clear()
+ self._build_data['space']['traits'][i] = ''
+ for i, trait_button in enumerate(self.space.starship_traits):
+ trait_button.clear()
+ self._build_data['space']['starship_traits'][i] = ''
+ for i, trait_button in enumerate(self.space.rep_traits):
+ trait_button.clear()
+ self._build_data['space']['rep_traits'][i] = ''
+ for i, trait_button in enumerate(self.space.active_rep_traits):
+ trait_button.clear()
+ self._build_data['space']['active_rep_traits'][i] = ''
+ if environment == 'ground' or environment == 'both':
+ for i, trait_button in enumerate(self.ground.traits):
+ trait_button.clear()
+ self._build_data['ground']['traits'][i] = ''
+ for i, trait_button in enumerate(self.ground.rep_traits):
+ trait_button.clear()
+ self._build_data['ground']['rep_traits'][i] = ''
+ for i, trait_button in enumerate(self.ground.active_rep_traits):
+ trait_button.clear()
+ self._build_data['ground']['active_rep_traits'][i] = ''
+
+ def clear_doffs(self, environment: str = 'both'):
+ """
+ Clears doff frame(s)
+
+ Parameters:
+ - :param environment: "space" / "ground" / "both"
+ """
+ if environment == 'space' or environment == 'both':
+ for i in range(6):
+ self.space.doffs_spec[i].setCurrentText('')
+ self.space.doffs_variant[i].clear()
+ self._build_data['space']['doffs_spec'][i] = ''
+ self._build_data['space']['doffs_variant'][i] = ''
+ if environment == 'ground' or environment == 'both':
+ for i in range(6):
+ self.ground.doffs_spec[i].setCurrentText('')
+ self.ground.doffs_variant[i].clear()
+ self._build_data['ground']['doffs_spec'][i] = ''
+ self._build_data['ground']['doffs_variant'][i] = ''
+
+ def clear_space_skills(self):
+ """
+ resets space skill tree
+ """
+ self.skills.space_desc.clear()
+ self._build_data['skill_desc']['space'] = ''
+ self._build_data['space_skills'] = {
+ 'eng': [False] * 30,
+ 'sci': [False] * 30,
+ 'tac': [False] * 30
+ }
+ self._skill_state['space_points_total'] = 0
+ self._skill_state['space_points_eng'] = 0
+ self.skills.count_labels['eng'].setText('0')
+ self._skill_state['space_points_sci'] = 0
+ self.skills.count_labels['sci'].setText('0')
+ self._skill_state['space_points_tac'] = 0
+ self.skills.count_labels['tac'].setText('0')
+ self._skill_state['space_points_rank'] = [0] * 5
+ for career in ('eng', 'sci', 'tac'):
+ for skill_button in self.skills.space[career]:
+ skill_button.clear_overlay()
+ skill_button.highlight = False
+ self._build_data['skill_unlocks'][career] = [None] * 5
+ for bar_segment in self.skills.bonus_bars[career]:
+ bar_segment.setChecked(False)
+ for unlock_button in self.skills.unlocks[career]:
+ unlock_button.clear()
+
+ def clear_ground_skills(self):
+ """
+ resets ground skill tree
+ """
+ self.skills.ground_desc.clear()
+ self._build_data['skill_desc']['ground'] = ''
+ self._build_data['ground_skills'] = [
+ [False] * 6,
+ [False] * 6,
+ [False] * 4,
+ [False] * 4
+ ]
+ self._build_data['skill_unlocks']['ground'] = [None] * 5
+ self._skill_state['ground_points_total'] = 0
+ self.skills.count_labels['ground'].setText('0')
+ for skill_subtree in self.skills.ground:
+ for skill_button in skill_subtree:
+ skill_button.clear_overlay()
+ skill_button.highlight = False
+ for unlock_button in self.skills.unlocks['ground']:
+ unlock_button.clear()
+ for bar_segment in self.skills.bonus_bars['ground']:
+ bar_segment.setChecked(False)
+
+ def clear_captain(self):
+ """
+ Clears Captain information from build and UI
+ """
+ self.character.name.clear()
+ self._build_data['captain']['name'] = ''
+ self.character.elite.setCheckState(Qt.CheckState.Unchecked)
+ self._build_data['captain']['elite'] = False
+ self.character.career.setCurrentText('')
+ self._build_data['captain']['career'] = ''
+ self.character.faction.setCurrentText('')
+ self._build_data['captain']['faction'] = ''
+ self.character.species.setCurrentText('')
+ self._build_data['captain']['species'] = ''
+ self.character.primary.setCurrentText('')
+ self._build_data['captain']['primary_spec'] = ''
+ self.character.secondary.setCurrentText('')
+ self._build_data['captain']['secondary_spec'] = ''
+
def update_equipment_cat(
self, build_key: str, target_quantity: int | None, clear: bool = False,
can_hide: bool = False):
@@ -491,6 +686,18 @@ def load_equipment_cat(self, build_key: str, environment: str):
else:
getattr(getattr(self, environment), build_key)[subkey].clear()
+ def clear_equipment_cat_ground(self, build_key: str):
+ """
+ Clears buttons and build; ground build only
+
+ Parameters:
+ - :param build_key: key to self.build and self.widgets
+ """
+ category: list[ItemButton] = getattr(self.ground, build_key)
+ for subkey, button in enumerate(category):
+ button.clear()
+ self._build_data['ground'][build_key][subkey] = ''
+
def load_trait_cat(self, build_key: str, environment: str):
"""
Updates trait category buttons to show items from build.
@@ -686,6 +893,22 @@ def load_boff_stations(self, environment: str):
else:
slot.clear()
+ def clear_boff_seat_ground(self, boff_id: int):
+ """
+ Resets boff seat.
+
+ Parameters:
+ - :param boff_id: boff number counted from the top/beginning
+ """
+ boff_station: list[ItemButton] = self.ground.boffs[boff_id]
+ for subkey, button in enumerate(boff_station):
+ button.clear()
+ self._build_data['ground']['boffs'][boff_id][subkey] = ''
+ self.ground.boff_profs[boff_id].setCurrentText('Tactical')
+ self._build_data['ground']['boff_profs'][boff_id] = 'Tactical'
+ self.ground.boff_specs[boff_id].setCurrentText('Command')
+ self._build_data['ground']['boff_specs'][boff_id] = 'Command'
+
def load_doffs(self, environment: str):
"""
Updates UI to show doffs in self.build
@@ -944,7 +1167,7 @@ def elite_callback(self, state: Qt.CheckState):
self.ground.kit_modules[5].show()
self.ground.ground_devices[4].show()
else:
- if not self.building:
+ if not self._building:
self._build_data['captain']['elite'] = False
self._build_data['space']['traits'][9] = None
self._build_data['ground']['traits'][9] = None
diff --git a/src/buildupdater.py b/src/buildupdater.py
index 425cdba..f8bc78a 100644
--- a/src/buildupdater.py
+++ b/src/buildupdater.py
@@ -315,18 +315,6 @@ def update_equipment_cat(
self.build['space'][build_key][hide_index] = None
-def clear_equipment_cat(self, build_key: str):
- """
- Clears buttons and build; ground build only
-
- Parameters:
- - :param build_key: key to self.build and self.widgets
- """
- for subkey, button in enumerate(self.widgets.build['ground'][build_key]):
- button.clear()
- self.build['ground'][build_key][subkey] = ''
-
-
def update_starship_traits(self, target_quantity: int, clear: bool = False):
"""
Shows/hides appropriate amount of starship trait buttons; updates `self.build`
@@ -412,22 +400,6 @@ def update_boff_seat(
self.build['space']['boff_specs'][boff_id] = [default_profession, specialization]
-def clear_boff_seat_ground(self, boff_id: int):
- """
- Resets boff seat.
-
- Parameters:
- - :param boff_id: boff number counted from the top/beginning
- """
- for subkey, button in enumerate(self.widgets.build['ground']['boffs'][boff_id]):
- button.clear()
- self.build['ground']['boffs'][boff_id][subkey] = ''
- self.widgets.build['ground']['boff_profs'][boff_id].setCurrentText('Tactical')
- self.build['ground']['boff_profs'][boff_id] = 'Tactical'
- self.widgets.build['ground']['boff_specs'][boff_id].setCurrentText('Command')
- self.build['ground']['boff_specs'][boff_id] = 'Command'
-
-
def load_equipment_cat(self, build_key: str, environment: str):
"""
Updates equipment category buttons to show items from build.
@@ -615,94 +587,6 @@ def set_skill_unlock_space(
self.build['skill_unlocks'][career][id] = None
-def clear_traits(self, environment: str = 'both'):
- """
- Clears traits from build and UI
-
- Parameters:
- - :param environment: environment to clear the traits from (space/ground/both)
- """
- if environment == 'space' or environment == 'both':
- for i, trait_button in enumerate(self.widgets.build['space']['traits']):
- trait_button.clear()
- self.build['space']['traits'][i] = ''
- for i, trait_button in enumerate(self.widgets.build['space']['starship_traits']):
- trait_button.clear()
- self.build['space']['starship_traits'][i] = ''
- for i, trait_button in enumerate(self.widgets.build['space']['rep_traits']):
- trait_button.clear()
- self.build['space']['rep_traits'][i] = ''
- for i, trait_button in enumerate(self.widgets.build['space']['active_rep_traits']):
- trait_button.clear()
- self.build['space']['active_rep_traits'][i] = ''
- if environment == 'ground' or environment == 'both':
- for i, trait_button in enumerate(self.widgets.build['ground']['traits']):
- trait_button.clear()
- self.build['ground']['traits'][i] = ''
- for i, trait_button in enumerate(self.widgets.build['ground']['rep_traits']):
- trait_button.clear()
- self.build['ground']['rep_traits'][i] = ''
- for i, trait_button in enumerate(self.widgets.build['ground']['active_rep_traits']):
- trait_button.clear()
- self.build['ground']['active_rep_traits'][i] = ''
-
-
-def clear_captain(self):
- """
- Clears Captain information from build and UI
- """
- self.widgets.character['name'].clear()
- self.build['captain']['name'] = ''
- self.widgets.character['elite'].setCheckState(Qt.CheckState.Unchecked)
- self.build['captain']['elite'] = False
- self.widgets.character['career'].setCurrentText('')
- self.build['captain']['career'] = ''
- self.widgets.character['faction'].setCurrentText('')
- self.build['captain']['faction'] = ''
- self.widgets.character['species'].setCurrentText('')
- self.build['captain']['species'] = ''
- self.widgets.character['primary'].setCurrentText('')
- self.build['captain']['primary_spec'] = ''
- self.widgets.character['secondary'].setCurrentText('')
- self.build['captain']['secondary_spec'] = ''
-
-
-def clear_ship(self):
- """
- Clears ship section of sidebar
- """
- self.widgets.ship['image'].set_image(self.cache.empty_image)
- self.widgets.ship['button'].setText('')
- self.build['space']['ship'] = ''
- self.widgets.ship['tier'].clear()
- self.widgets.ship['dc'].hide()
- self.widgets.ship['name'].setText('')
- self.build['space']['ship_name'] = ''
- self.widgets.ship['desc'].setPlainText('')
- self.build['space']['ship_desc'] = ''
-
-
-def clear_ground_build(self):
- """
- Clears ground build
- """
- self.widgets.ground_desc.clear()
- self.build['ground']['ground_desc'] = ''
- clear_equipment_cat(self, 'kit_modules')
- clear_equipment_cat(self, 'weapons')
- clear_equipment_cat(self, 'ground_devices')
- clear_equipment_cat(self, 'kit')
- clear_equipment_cat(self, 'armor')
- clear_equipment_cat(self, 'ev_suit')
- clear_equipment_cat(self, 'personal_shield')
- clear_boff_seat_ground(self, 0)
- clear_boff_seat_ground(self, 1)
- clear_boff_seat_ground(self, 2)
- clear_boff_seat_ground(self, 3)
- clear_traits(self, 'ground')
- clear_doffs(self, 'ground')
-
-
def load_doffs(self, environment: str):
"""
Updates UI to show doffs in self.build
@@ -721,24 +605,3 @@ def load_doffs(self, environment: str):
variants = getattr(self.cache, f'{environment}_doffs')[spec].keys()
variant_combo.addItems({''} | variants)
variant_combo.setCurrentText(variant)
-
-
-def clear_doffs(self, environment: str = 'both'):
- """
- Clears doff frame(s)
-
- Parameters:
- - :param environment: "space" / "ground" / "both"
- """
- if environment == 'space' or environment == 'both':
- for i in range(6):
- self.widgets.build['space']['doffs_spec'][i].setCurrentText('')
- self.widgets.build['space']['doffs_variant'][i].clear()
- self.build['space']['doffs_spec'][i] = ''
- self.build['space']['doffs_variant'][i] = ''
- if environment == 'ground' or environment == 'both':
- for i in range(6):
- self.widgets.build['ground']['doffs_spec'][i].setCurrentText('')
- self.widgets.build['ground']['doffs_variant'][i].clear()
- self.build['ground']['doffs_spec'][i] = ''
- self.build['ground']['doffs_variant'][i] = ''
diff --git a/src/callbacks.py b/src/callbacks.py
deleted file mode 100644
index 62edc27..0000000
--- a/src/callbacks.py
+++ /dev/null
@@ -1,110 +0,0 @@
-from .buildupdater import (
- align_space_frame, clear_captain, clear_doffs, clear_ground_build, clear_ship, clear_traits,
- get_variable_slot_counts, set_skill_unlock_ground, set_skill_unlock_space,
- slot_equipment_item, slot_trait_item, update_equipment_cat, update_starship_traits)
-from .constants import EQUIPMENT_TYPES, SHIP_TEMPLATE, SKILL_POINTS_FOR_RANK
-from .datafunctions import (
- load_build_file, load_skill_tree_file, save_build_file, save_skill_tree_file)
-from .iofunc import browse_path, image, open_wiki_page
-from .widgets import exec_in_thread
-
-from PySide6.QtCore import Qt
-
-
-def clear_build_callback(self):
- """
- Clears current build section
- """
- current_tab = self.widgets.build_tabber.currentIndex()
- self.building = True
- if current_tab == 0:
- clear_space_build(self)
- elif current_tab == 1:
- clear_ground_build(self)
- elif current_tab == 2:
- clear_space_skills(self)
- elif current_tab == 3:
- clear_ground_skills(self)
- self.building = False
-
-
-def clear_space_build(self):
- """
- clears space build
- """
- self.building = True
- clear_ship(self)
- align_space_frame(self, SHIP_TEMPLATE, clear=True)
- clear_traits(self, 'space')
- clear_doffs(self, 'space')
- self.building = False
- self.autosave()
-
-
-def clear_space_skills(self):
- """
- resets space skill tree
- """
- self.widgets.build['skill_desc']['space'].clear()
- self.build['skill_desc']['space'] = ''
- self.build['space_skills'] = {
- 'eng': [False] * 30,
- 'sci': [False] * 30,
- 'tac': [False] * 30
- }
- self.cache.skills['space_points_total'] = 0
- self.cache.skills['space_points_eng'] = 0
- self.widgets.skill_counts_space['eng'].setText('0')
- self.cache.skills['space_points_sci'] = 0
- self.widgets.skill_counts_space['sci'].setText('0')
- self.cache.skills['space_points_tac'] = 0
- self.widgets.skill_counts_space['tac'].setText('0')
- self.cache.skills['space_points_rank'] = [0] * 5
- for career in ('eng', 'sci', 'tac'):
- for skill_button in self.widgets.build['space_skills'][career]:
- skill_button.clear_overlay()
- skill_button.highlight = False
- self.build['skill_unlocks'][career] = [None] * 5
- for bar_segment in self.widgets.skill_bonus_bars[career]:
- bar_segment.setChecked(False)
- for unlock_button in self.widgets.build['skill_unlocks'][career]:
- unlock_button.clear()
-
-
-def clear_ground_skills(self):
- """
- resets ground skill tree
- """
- self.widgets.build['skill_desc']['ground'].clear()
- self.build['skill_desc']['ground'] = ''
- self.build['ground_skills'] = [
- [False] * 6,
- [False] * 6,
- [False] * 4,
- [False] * 4
- ]
- self.build['skill_unlocks']['ground'] = [None] * 5
- self.cache.skills['ground_points_total'] = 0
- self.widgets.skill_count_ground.setText('0')
- for skill_subtree in self.widgets.build['ground_skills']:
- for skill_button in skill_subtree:
- skill_button.clear_overlay()
- skill_button.highlight = False
- for unlock_button in self.widgets.build['skill_unlocks']['ground']:
- unlock_button.clear()
- for bar_segment in self.widgets.skill_bonus_bars['ground']:
- bar_segment.setChecked(False)
-
-
-def clear_all(self):
- """
- Clears space and ground build, skills and captain info
- """
- self.building = True
- clear_space_build(self)
- clear_ground_build(self)
- clear_captain(self)
- clear_space_skills(self)
- clear_ground_skills(self)
- self.building = False
- self.autosave()
From 06042a599ea028ded8233ab25c437e9948cae4d9 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Tue, 12 May 2026 21:43:31 +0200
Subject: [PATCH 20/44] reworking backend init and removing unneccessary code
---
src/app.py | 57 +++-
src/buildloader.py | 4 +-
src/buildupdater.py | 607 --------------------------------------
src/cargomanager.py | 61 ++--
src/datafunctions.py | 685 -------------------------------------------
src/imagemanager.py | 1 -
6 files changed, 91 insertions(+), 1324 deletions(-)
delete mode 100644 src/buildupdater.py
delete mode 100644 src/datafunctions.py
diff --git a/src/app.py b/src/app.py
index f79ea33..4dd67b2 100644
--- a/src/app.py
+++ b/src/app.py
@@ -6,6 +6,7 @@
from PySide6.QtWidgets import (
QApplication, QFrame, QPlainTextEdit, QPushButton, QScrollArea, QTabWidget, QWidget)
+from .buildhelpers import empty_build
from .buildloader import BuildLoader
from .buildmanager import BuildManager
from .cargomanager import CargoManager
@@ -28,7 +29,7 @@
create_combo_box2, create_entry2, create_frame2, create_item_button2, create_label2)
from .widgets import (
Cache, DoffCombobox, GridLayout, HBoxLayout, ImageLabel, ItemButton, ShipButton, ShipImage,
- Tabbers, TooltipLabel, VBoxLayout, WidgetStorage)
+ Tabbers, Thread, TooltipLabel, VBoxLayout, WidgetStorage)
# only for developing; allows to terminate the qt event loop with keyboard interrupt
# from signal import signal, SIGINT, SIG_DFL
@@ -37,10 +38,6 @@
class SETS():
- from .datafunctions import backup_cargo_data, empty_build, init_backend
- from .splash import enter_splash, exit_splash, splash_text
- from .style import prepare_tooltip_css
-
app_dir = None
# (release version, dev version)
versions = ('', '')
@@ -85,7 +82,6 @@ def __init__(self, theme, args, path, config, versions):
self.init_config()
QDir.addSearchPath('local_folder', os.path.join(path, 'local'))
self.theme2: AppTheme = AppTheme(self.config.ui_scale)
- self.prepare_tooltip_css()
self.init_environment()
self.downloader = Downloader(
self.config.config_subfolders['images'],
@@ -119,7 +115,9 @@ def __init__(self, theme, args, path, config, versions):
self.context_menu: ContextMenu = ContextMenu(self.theme2, self.build2, self.cargo)
self.context_menu.edit_slot.connect(self.edit_window.edit_item)
self.window.show()
- self.init_backend()
+ self._backend_thread: Thread = Thread(self.init_backend)
+ self._backend_thread.done.connect(self.complete_app_init)
+ self._backend_thread.start()
def run(self) -> int:
"""
@@ -192,7 +190,28 @@ def init_environment(self):
Creates external files before starting the app.
"""
if not self.config.autosave_path.exists():
- store_json(self.empty_build(), str(self.config.autosave_path))
+ store_json(empty_build(), str(self.config.autosave_path))
+
+ def init_backend(self):
+ """
+ Sets up downloader and provides cargo data and images.
+ """
+ self.downloader.default_session_from_env()
+ self.cargo.provision_cargo_data()
+ self.images.image_set = self.cargo.image_set
+ self.images.failed_images = self.cargo.failed_images
+ self.images.download_images(self.cargo.skills)
+ self.cargo.store_failed_images()
+ self.images.load_base_images()
+
+ def complete_app_init(self):
+ """
+ Updates ui and starts thread to load images.
+ """
+ self.init_ui()
+ self.build_loader.load_build_file(self.config.autosave_path)
+ self._backend_thread = Thread(self.images.load_images)
+ self._backend_thread.start()
def cache_icons(self):
"""
@@ -222,7 +241,7 @@ def main_window_close_callback(self, event: QCloseEvent):
"""
window_geometry = self.window.saveGeometry()
self.settings.state__geometry = window_geometry
- self.autosave()
+ self.build2.autosave()
self.settings.store_settings()
event.accept()
@@ -251,6 +270,24 @@ def create_main_window(self, argv=[]) -> tuple[QApplication, QWidget]:
QThread.currentThread().setPriority(QThread.Priority.TimeCriticalPriority)
return app, window
+ def init_ui(self):
+ """
+ Updates ui with cargo data and loads base images.
+ """
+ self.ship_selector_window.set_ships(self.cargo.ships.keys())
+ space_doff_specs = [''] + sorted(self.cargo.space_doffs.keys())
+ for combobox in self.build2.space.doffs_spec:
+ combobox.addItems(space_doff_specs)
+ ground_doff_specs = [''] + sorted(self.cache.ground_doffs.keys())
+ for combobox in self.build2.ground.doffs_spec:
+ combobox.addItems(ground_doff_specs)
+ for career_block in self.build2.skills.space.values():
+ for skill_button in career_block:
+ skill_button.set_item(self.images.get(skill_button.skill_image_name))
+ for skill_group in self.build2.skills.ground:
+ for skill_button in skill_group:
+ skill_button.set_item(self.images.get(skill_button.skill_image_name))
+
def picker(
self, environment: str, build_key: str, build_subkey: int, button: ItemButton,
equipment: bool = False, boff_id: int | None = None):
@@ -1450,7 +1487,7 @@ def setup_settings_frame(self):
self.theme2, 'Clears cache. Restart to rebuild cache.', 'hint_label')
sec_2.addWidget(cache_clear_label, 1, 2, alignment=ALEFT)
backup_cargo_button = create_button2(self.theme2, 'Backup Cargo Data')
- backup_cargo_button.clicked.connect(self.backup_cargo_data)
+ backup_cargo_button.clicked.connect(self.cargo.backup_cargo_data)
sec_2.addWidget(backup_cargo_button, 2, 0, alignment=ALEFT)
backup_cargo_label = create_label2(
self.theme2, 'Creates cargo backup to protect against download failures.', 'hint_label')
diff --git a/src/buildloader.py b/src/buildloader.py
index ecb11ce..00436ea 100644
--- a/src/buildloader.py
+++ b/src/buildloader.py
@@ -235,7 +235,7 @@ def remove_invalid_build_items(self, build: dict[str, int | dict[str]]):
for station in category_items:
for index, ability in enumerate(station):
if (isinstance(ability, dict)
- and ability['item'] not in self._cargo.images_set):
+ and ability['item'] not in self._cargo.image_set):
station[index] = ''
elif (category.startswith('doff')
or category == 'boff_specs'
@@ -243,7 +243,7 @@ def remove_invalid_build_items(self, build: dict[str, int | dict[str]]):
continue
elif isinstance(category_items, list):
for index, item in enumerate(category_items):
- if isinstance(item, dict) and item['item'] not in self._cargo.images_set:
+ if isinstance(item, dict) and item['item'] not in self._cargo.image_set:
category_items[index] = ''
def encode_in_image(self, image: QImage, data: str):
diff --git a/src/buildupdater.py b/src/buildupdater.py
deleted file mode 100644
index f8bc78a..0000000
--- a/src/buildupdater.py
+++ /dev/null
@@ -1,607 +0,0 @@
-from PySide6.QtCore import Qt
-
-from .constants import BOFF_RANKS, SHIP_TEMPLATE
-from .iofunc import get_ship_image, image
-from .textedit import (
- add_equipment_tooltip_header, get_tooltip, get_skill_unlock_tooltip_ground,
- get_skill_unlock_tooltip_space, get_ultimate_skill_unlock_tooltip)
-from .widgets import exec_in_thread
-
-
-def load_build(self):
- """
- Updates UI to show the build currently in self.build
- """
- self.building = True
- # ship section
- ship = self.build['space']['ship']
- if ship == '' or ship == '':
- ship_data = SHIP_TEMPLATE
- self.widgets.ship['button'].setText('')
- self.widgets.ship['tier'].clear()
- self.widgets.ship['image'].set_image(self.cache.empty_image)
- self.widgets.ship['dc'].hide()
- else:
- self.widgets.ship['button'].setText(ship)
- ship_data = self.cache.ships[ship]
- exec_in_thread(
- self, self.images.get_ship_image, ship_data['image'][5:],
- result=lambda img: self.widgets.ship['image'].set_image(*img))
- tier = self.build['space']['tier']
- ship_tier = ship_data['tier']
- self.widgets.ship['tier'].clear()
- if ship_tier == 6:
- self.widgets.ship['tier'].addItems(('T6', 'T6-X', 'T6-X2'))
- elif ship_tier == 5:
- self.widgets.ship['tier'].addItems(('T5', 'T5-U', 'T5-X', 'T5-X2'))
- else:
- self.widgets.ship['tier'].addItem(f'T{ship_tier}')
- self.widgets.ship['tier'].setCurrentText(tier)
- if ship_data['equipcannons'] == 'yes':
- self.widgets.ship['dc'].show()
- else:
- self.widgets.ship['dc'].hide()
- self.widgets.ship['name'].setText(self.build['space']['ship_name'])
- self.widgets.ship['desc'].setPlainText(self.build['space']['ship_desc'])
-
- # Character section
- elite_captain = self.build['captain']['elite']
- self.widgets.character['name'].setText(self.build['captain']['name'])
- elite_state = Qt.CheckState.Checked if elite_captain else Qt.CheckState.Unchecked
- self.widgets.character['elite'].setCheckState(elite_state)
- self.widgets.character['career'].setCurrentText(self.build['captain']['career'])
- species = self.build['captain']['species']
- self.widgets.character['faction'].setCurrentText(self.build['captain']['faction'])
- self.widgets.character['species'].setCurrentText(species)
- if species != 'Alien':
- self.widgets.build['space']['traits'][10].hide()
- self.widgets.build['ground']['traits'][10].hide()
- self.widgets.character['primary'].setCurrentText(self.build['captain']['primary_spec'])
- self.widgets.character['secondary'].setCurrentText(self.build['captain']['secondary_spec'])
-
- # Space Build Section
- if ship == '' or ship == '':
- align_space_frame(self, ship_data, clear=True)
- else:
- align_space_frame(self, ship_data)
- load_equipment_cat(self, 'fore_weapons', 'space')
- load_equipment_cat(self, 'aft_weapons', 'space')
- load_equipment_cat(self, 'experimental', 'space')
- load_equipment_cat(self, 'devices', 'space')
- load_equipment_cat(self, 'hangars', 'space')
- load_equipment_cat(self, 'deflector', 'space')
- load_equipment_cat(self, 'sec_def', 'space')
- load_equipment_cat(self, 'engines', 'space')
- load_equipment_cat(self, 'core', 'space')
- load_equipment_cat(self, 'shield', 'space')
- load_equipment_cat(self, 'uni_consoles', 'space')
- load_equipment_cat(self, 'eng_consoles', 'space')
- load_equipment_cat(self, 'sci_consoles', 'space')
- load_equipment_cat(self, 'tac_consoles', 'space')
- load_boff_stations(self, 'space')
- load_trait_cat(self, 'traits', 'space')
- if not elite_captain:
- self.widgets.build['space']['traits'][9].hide()
- load_trait_cat(self, 'starship_traits', 'space')
- load_trait_cat(self, 'rep_traits', 'space')
- load_trait_cat(self, 'active_rep_traits', 'space')
- load_doffs(self, 'space')
-
- # Ground Build Section
- self.widgets.ground_desc.setPlainText(self.build['ground']['ground_desc'])
- load_equipment_cat(self, 'kit_modules', 'ground')
- if not elite_captain:
- self.widgets.build['ground']['kit_modules'][5].hide()
- load_equipment_cat(self, 'weapons', 'ground')
- load_equipment_cat(self, 'ground_devices', 'ground')
- if not elite_captain:
- self.widgets.build['ground']['ground_devices'][4].hide()
- load_equipment_cat(self, 'kit', 'ground')
- load_equipment_cat(self, 'armor', 'ground')
- load_equipment_cat(self, 'ev_suit', 'ground')
- load_equipment_cat(self, 'personal_shield', 'ground')
- load_boff_stations(self, 'ground')
- load_trait_cat(self, 'traits', 'ground')
- if not elite_captain:
- self.widgets.build['ground']['traits'][9].hide()
- load_trait_cat(self, 'rep_traits', 'ground')
- load_trait_cat(self, 'active_rep_traits', 'ground')
- load_doffs(self, 'ground')
-
- load_skill_pages(self)
-
- self.building = False
- self.autosave()
-
-
-def load_skill_pages(self):
- """
- Updates UI to show skill trees in self.build
- """
- # space skills
- self.widgets.build['skill_desc']['space'].setPlainText(self.build['skill_desc']['space'])
- self.cache.skills['space_points_eng'] = 0
- self.cache.skills['space_points_sci'] = 0
- self.cache.skills['space_points_tac'] = 0
- self.cache.skills['space_points_rank'] = [0] * 5
- self.cache.skills['space_points_total'] = 0
- for career in ('eng', 'sci', 'tac'):
- for skill_id, (button, enable) in enumerate(zip(
- self.widgets.build['space_skills'][career], self.build['space_skills'][career])):
- if enable:
- button.set_overlay(self.cache.overlays.check)
- button.highlight = True
- self.cache.skills[f'space_points_{career}'] += 1
- self.cache.skills['space_points_rank'][int(skill_id / 6)] += 1
- else:
- button.clear_overlay()
- button.highlight = False
- self.cache.skills['space_points_total'] = sum(self.cache.skills['space_points_rank'])
- for career in ('eng', 'sci', 'tac'):
- skill_points = self.cache.skills[f'space_points_{career}']
- self.widgets.skill_counts_space[career].setText(str(skill_points))
- for unlock_id, unlock_choice in enumerate(self.build['skill_unlocks'][career]):
- set_skill_unlock_space(self, career, unlock_id, unlock_choice, skill_points)
- if skill_points > 24:
- skill_points = 24
- for i in range(skill_points):
- self.widgets.skill_bonus_bars[career][i].setChecked(True)
- for i in range(skill_points, 24, 1):
- self.widgets.skill_bonus_bars[career][i].setChecked(False)
-
- # ground skills
- self.widgets.build['skill_desc']['ground'].setPlainText(self.build['skill_desc']['ground'])
- self.cache.skills['ground_points_total'] = 0
- for skill_data, skill_buttons in zip(
- self.build['ground_skills'], self.widgets.build['ground_skills']):
- for enable, skill_button in zip(skill_data, skill_buttons):
- if enable:
- skill_button.set_overlay(self.cache.overlays.check)
- skill_button.highlight = True
- self.cache.skills['ground_points_total'] += 1
- else:
- skill_button.clear_overlay()
- skill_button.highlight = False
- self.widgets.skill_count_ground.setText(str(self.cache.skills['ground_points_total']))
- for i in range(self.cache.skills['ground_points_total']):
- self.widgets.skill_bonus_bars['ground'][i].setChecked(True)
- for i in range(self.cache.skills['ground_points_total'], 10, 1):
- self.widgets.skill_bonus_bars['ground'][i].setChecked(False)
- for unlock_id, unlock_choice in enumerate(self.build['skill_unlocks']['ground']):
- set_skill_unlock_ground(self, unlock_id, unlock_choice)
-
-
-def get_boff_spec(self, seat_details: str) -> tuple[int, str, str]:
- """
- Returns rank, profession and specialization from cargo string
-
- Parameters:
- - :param seat_details: contains rank, profession and specialization:
- " -"
- """
- if '-' in seat_details:
- rank_and_profession, spec = seat_details.split('-')
- else:
- rank_and_profession = seat_details
- spec = ''
- rank_name, _, profession = rank_and_profession.rpartition(' ')
- return (BOFF_RANKS[rank_name], profession, spec)
-
-
-def align_space_frame(self, ship_data: dict, clear: bool = False):
- """
- Hides / shows the appropriate buttons of the ship build. Updates Boff stations.
-
- Parameters:
- - :param ship_data: ship specifications
- - :param clear: set to True to clear build
- """
- uni, eng, sci, tac, devices, starship_traits = get_variable_slot_counts(self, ship_data)
-
- # Equipment
- update_equipment_cat(self, 'fore_weapons', ship_data['fore'], clear)
- update_equipment_cat(self, 'aft_weapons', ship_data['aft'], clear, can_hide=True)
- update_equipment_cat(self, 'experimental', ship_data['experimental'], clear, can_hide=True)
- update_equipment_cat(self, 'devices', devices, clear)
- update_equipment_cat(self, 'hangars', ship_data['hangars'], clear, can_hide=True)
- update_equipment_cat(self, 'sec_def', ship_data['secdeflector'], clear, can_hide=True)
- if clear:
- self.widgets.build['space']['deflector'][0].clear()
- self.build['space']['deflector'][0] = ''
- self.widgets.build['space']['engines'][0].clear()
- self.build['space']['engines'][0] = ''
- self.widgets.build['space']['core'][0].clear()
- self.build['space']['core'][0] = ''
- self.widgets.build['space']['shield'][0].clear()
- self.build['space']['shield'][0] = ''
- update_equipment_cat(self, 'uni_consoles', uni, clear, can_hide=True)
- update_equipment_cat(self, 'eng_consoles', eng, clear, can_hide=True)
- update_equipment_cat(self, 'sci_consoles', sci, clear, can_hide=True)
- update_equipment_cat(self, 'tac_consoles', tac, clear, can_hide=True)
-
- # Starship Traits
- update_starship_traits(self, starship_traits, clear)
-
- # Boffs
- boff_specs = map(lambda s: get_boff_spec(self, s), ship_data['boffs'])
- if 'Science Destroyer' in ship_data['type']:
- for boff_num, boff_details in enumerate(sorted(boff_specs, reverse=True)):
- if (boff_details[0] == 3 and boff_details[1] == 'Tactical'
- or boff_details[0] == 4 and boff_details[1] == 'Science'):
- update_boff_seat(self, boff_num, *boff_details, clear, sci_destroyer_seat=True)
- else:
- update_boff_seat(self, boff_num, *boff_details, clear)
- else:
- for boff_num, boff_details in enumerate(sorted(boff_specs, reverse=True)):
- update_boff_seat(self, boff_num, *boff_details, clear)
- for boff_to_hide in range(boff_num + 1, 6):
- update_boff_seat(self, boff_to_hide, rank=0, profession='', clear=clear, hide_seat=True)
-
-
-def get_variable_slot_counts(self, ship_data: dict):
- """
- returns the number of universal consoles, devices and starship traits the current build should
- have
-
- Parameters:
- - :param ship_data: ship specifications
-
- :return: 6-tuple containing universal consoles, engineering consoles, science consoles, \
- tactical consoles, devices, starship traits
- """
- if ship_data['name'] == '':
- uni_consoles = 3
- starship_traits = 7
- devices = 6
- eng_consoles = 5
- sci_consoles = 5
- tac_consoles = 5
- else:
- uni_consoles = 0
- starship_traits = 5
- devices = ship_data['devices']
- eng_consoles = ship_data['consoleseng']
- sci_consoles = ship_data['consolessci']
- tac_consoles = ship_data['consolestac']
- if 'Innovation Effects' in ship_data['abilities']:
- uni_consoles += 1
- elif ship_data['name'] == 'Federation Intel Holoship':
- uni_consoles += 1
- if '-X2' in self.build['space']['tier']:
- uni_consoles += 2
- starship_traits += 2
- devices += 2
- elif '-X' in self.build['space']['tier']:
- uni_consoles += 1
- starship_traits += 1
- devices += 1
- if self.build['space']['tier'].startswith(('T5-U', 'T5-X')):
- if ship_data['t5uconsole'] == 'eng':
- eng_consoles += 1
- elif ship_data['t5uconsole'] == 'sci':
- sci_consoles += 1
- elif ship_data['t5uconsole'] == 'tac':
- tac_consoles += 1
- return uni_consoles, eng_consoles, sci_consoles, tac_consoles, devices, starship_traits
-
-
-def update_equipment_cat(
- self, build_key: str, target_quantity: int | None, clear: bool = False,
- can_hide: bool = False):
- """
- Shows/hides appropriate amount of buttons of the given category; updates build; space build only
-
- Parameters:
- - :param build_key: key to self.build and self.widgets
- - :param target_quantity: number of slots that should be available in this category
- - :param clear: True to clear build
- - :param can_hide: hides/shows category label when target_quantity is 0/None
- """
- if target_quantity is None or target_quantity == 0:
- target_quantity = 0
- self.widgets.build['space'][build_key + '_label'].hide()
- elif can_hide:
- self.widgets.build['space'][build_key + '_label'].show()
- buttons = self.widgets.build['space'][build_key]
- max_quantity = len(buttons)
- for show_index in range(target_quantity):
- buttons[show_index].show()
- if clear:
- buttons[show_index].clear()
- self.build['space'][build_key][show_index] = ''
- for hide_index in range(target_quantity, max_quantity):
- buttons[hide_index].clear()
- buttons[hide_index].hide()
- self.build['space'][build_key][hide_index] = None
-
-
-def update_starship_traits(self, target_quantity: int, clear: bool = False):
- """
- Shows/hides appropriate amount of starship trait buttons; updates `self.build`
-
- Parameters:
- - :param target_quantity: number of slots that should be available in this category
- - :param clear: True to clear build
- """
- buttons = self.widgets.build['space']['starship_traits']
- for show_index in range(target_quantity):
- buttons[show_index].show()
- if clear:
- buttons[show_index].clear()
- self.build['space']['starship_traits'][show_index] = ''
- for hide_index in range(target_quantity, 7):
- buttons[hide_index].clear()
- buttons[hide_index].hide()
- self.build['space']['starship_traits'][hide_index] = None
-
-
-def update_boff_seat(
- self, boff_id: str, rank: int, profession: str, specialization: str = '',
- clear: bool = False, hide_seat: bool = False, sci_destroyer_seat: bool = False):
- """
- Shows/hides appropriate amount of buttons of the boff seat; updates build; space build only
-
- Parameters:
- - :param boff_id: boff number counted from the top/beginning
- - :param rank: number of slots that should be available in this category
- - :param profession: seat profession
- - :param specialization: seat specialization
- - :param clear: set to True to clear build
- - :param hide_seat: hides/shows seat label
- - :param sci_destroyer_seat: set to `True` to upgrade seat to commander and show info label
- """
- buttons = self.widgets.build['space']['boffs'][boff_id]
- max_quantity = 4
- if sci_destroyer_seat:
- rank = 4
- for show_index in range(rank):
- buttons[show_index].show()
- if clear:
- buttons[show_index].clear()
- self.build['space']['boffs'][boff_id][show_index] = ''
- for hide_index in range(rank, max_quantity):
- buttons[hide_index].clear()
- buttons[hide_index].hide()
- self.build['space']['boffs'][boff_id][hide_index] = None
- label = self.widgets.build['space']['boff_labels'][boff_id]
- label.clear()
- if hide_seat:
- label.hide()
- else:
- label.show()
- if specialization != '':
- spec_label = f' / {specialization}'
- else:
- spec_label = ''
- if profession == 'Universal':
- label_options = (
- f'Tactical{spec_label}',
- f'Science{spec_label}',
- f'Engineering{spec_label}'
- )
- label.setDisabled(False)
- else:
- label_options = (profession + spec_label,)
- label.setDisabled(True)
- label.addItems(label_options)
- icon_label = self.widgets.build['space']['boff_label_icons'][boff_id]
- if sci_destroyer_seat:
- if profession == 'Science':
- icon_label.setPixmap(self.cache.icons['sci-small'])
- icon_label._tooltip.setText('Commander slot only available in science mode.')
- elif profession == 'Tactical':
- icon_label.setPixmap(self.cache.icons['tac-small'])
- icon_label._tooltip.setText('Commander slot only available in tactical mode.')
- icon_label.show()
- else:
- icon_label.hide()
- if clear:
- default_profession = 'Tactical' if profession == 'Universal' else profession
- self.build['space']['boff_specs'][boff_id] = [default_profession, specialization]
-
-
-def load_equipment_cat(self, build_key: str, environment: str):
- """
- Updates equipment category buttons to show items from build.
-
- Parameters:
- - :param build_key: equipment category
- - :param environment: space/ground
- """
- for subkey, item in enumerate(self.build[environment][build_key]):
- if item is not None and item != '':
- slot_equipment_item(self, item, environment, build_key, subkey)
- else:
- self.widgets.build[environment][build_key][subkey].clear()
-
-
-def load_trait_cat(self, build_key: str, environment: str):
- """
- Updates trait category buttons to show items from build.
-
- Parameters:
- - :param build_key: trait category
- - :param environment: space/ground
- """
- for subkey, item in enumerate(self.build[environment][build_key]):
- if item is not None and item != '':
- slot_trait_item(self, item, environment, build_key, subkey)
- else:
- self.widgets.build[environment][build_key][subkey].clear()
-
-
-def load_boff_stations(self, environment: str):
- """
- Updates boff stations to show items from build
-
- Parameters:
- - :param environment: "space" / "ground"
- """
- if environment == 'space':
- for boff_id, boff_data in enumerate(self.build['space']['boffs']):
- boff_spec = self.build['space']['boff_specs'][boff_id]
- if boff_spec[1] == '':
- boff_text = boff_spec[0]
- else:
- boff_text = f'{boff_spec[0]} / {boff_spec[1]}'
- self.widgets.build['space']['boff_labels'][boff_id].setCurrentText(boff_text)
- for ability, slot in zip(boff_data, self.widgets.build['space']['boffs'][boff_id]):
- if ability is not None and ability != '':
- tooltip = self.cache.boff_abilities['all'][ability['item']][ability['rank']]
- slot.set_item_full(image(self, ability['item']), None, tooltip)
- else:
- slot.clear()
- elif environment == 'ground':
- for boff_id, boff_data in enumerate(self.build['ground']['boffs']):
- self.widgets.build['ground']['boff_profs'][boff_id].setCurrentText(
- self.build['ground']['boff_profs'][boff_id])
- self.widgets.build['ground']['boff_specs'][boff_id].setCurrentText(
- self.build['ground']['boff_specs'][boff_id])
- for ability, slot in zip(boff_data, self.widgets.build['ground']['boffs'][boff_id]):
- if ability is not None and ability != '':
- tooltip = self.cache.boff_abilities['all'][ability['item']][ability['rank']]
- slot.set_item_full(image(self, ability['item']), None, tooltip)
- else:
- slot.clear()
-
-
-def slot_equipment_item(self, item: dict, environment: str, build_key: str, build_subkey: int):
- """
- Updates build and UI with item
-
- Parameters:
- - :param item: item to be slotted
- - :param environment: space/ground
- - :param build_key: key to self.build[environment]
- - :param build_subkey: index of the item within its build_key (category)
- """
- self.build[environment][build_key][build_subkey] = item
- item_image = image(self, item['item'])
- overlay = getattr(self.cache.overlays, item['rarity'].lower().replace(' ', ''))
- tooltip = add_equipment_tooltip_header(
- self, item, self.cache.equipment[build_key][item['item']]['tooltip'], build_key)
- self.widgets.build[environment][build_key][build_subkey].set_item_full(
- item_image, overlay, tooltip)
-
-
-def slot_trait_item(self, item: dict, environment: str, build_key: str, build_subkey: int):
- """
- Updates build and UI with item
-
- Parameters:
- - :param item: item to be slotted
- - :param environment: space/ground
- - :param build_key: key to self.build[environment]
- - :param build_subkey: index of the item within its build_key (category)
- """
- self.build[environment][build_key][build_subkey] = item
- alt_image_key = f"{item['item']}__{environment}__{build_key}"
- if alt_image_key in self.cache.alt_images:
- image_name = self.cache.alt_images[alt_image_key]
- else:
- image_name = item['item']
- item_image = image(self, image_name)
- self.widgets.build[environment][build_key][build_subkey].set_item_full(
- item_image, None, get_tooltip(self, item['item'], build_key, environment))
-
-
-def set_skill_unlock_ground(self, id: int, state: int | None):
- """
- Sets unlock button to state and updates build
-
- Parameters:
- - :param id: id of the unlock, counted from the unlock with the lowest requirement
- - :param state: `0`, `1` set the button to the respective unlock, `None` clears
- """
- unlock_button = self.widgets.build['skill_unlocks']['ground'][id]
- if state == 0:
- unlock_button.set_item(
- self.cache.images['arrow-up'])
- unlock_button.tooltip = get_skill_unlock_tooltip_ground(self, id, 0)
- self.build['skill_unlocks']['ground'][id] = 0
- if not self.building:
- unlock_button.force_tooltip_update()
- elif state == 1:
- unlock_button.set_item(
- self.cache.images['arrow-down'])
- unlock_button.tooltip = get_skill_unlock_tooltip_ground(self, id, 1)
- self.build['skill_unlocks']['ground'][id] = 1
- if not self.building:
- unlock_button.force_tooltip_update()
- else:
- unlock_button.clear()
- self.build['skill_unlocks']['ground'][id] = None
-
-
-def set_skill_unlock_space(
- self, career: str, id: int, state: int | None = None, points_spent: int = -1):
- """
- Sets unlock button to state and updates build
-
- Parameters:
- - :param career: "eng" / "sci" / "tac"
- - :param id: id of the unlock, counted from the unlock with the lowest requirement
- - :param state: `0`, `1` set the button to the respective unlock, `None` clears
- """
- unlock_button = self.widgets.build['skill_unlocks'][career][id]
- if id == 4:
- if points_spent > 27 and state == self.build['skill_unlocks'][career][id]:
- return
- if state is None:
- unlock_button.clear()
- self.build['skill_unlocks'][career][id] = None
- else:
- unlock_button.set_item(
- self.cache.images[self.cache.skills['space_unlocks']['_icons'][career]])
- if points_spent == 24:
- unlock_button.tooltip = get_ultimate_skill_unlock_tooltip(self, career, -1, 0)
- self.build['skill_unlocks'][career][id] = -1
- elif points_spent == 25:
- unlock_button.tooltip = get_ultimate_skill_unlock_tooltip(self, career, state, 1)
- self.build['skill_unlocks'][career][id] = state
- elif points_spent == 26:
- unlock_button.tooltip = get_ultimate_skill_unlock_tooltip(self, career, state, 2)
- self.build['skill_unlocks'][career][id] = state
- else:
- unlock_button.tooltip = get_ultimate_skill_unlock_tooltip(self, career, 4, 3)
- self.build['skill_unlocks'][career][id] = 3
- if not self.building:
- unlock_button.force_tooltip_update()
- else:
- if state == 0:
- unlock_button.set_item(
- self.cache.images['arrow-up'])
- unlock_button.tooltip = get_skill_unlock_tooltip_space(self, career, id, 0)
- self.build['skill_unlocks'][career][id] = 0
- if not self.building:
- unlock_button.force_tooltip_update()
- elif state == 1:
- unlock_button.set_item(
- self.cache.images['arrow-down'])
- unlock_button.tooltip = get_skill_unlock_tooltip_space(self, career, id, 1)
- self.build['skill_unlocks'][career][id] = 1
- if not self.building:
- unlock_button.force_tooltip_update()
- else:
- unlock_button.clear()
- self.build['skill_unlocks'][career][id] = None
-
-
-def load_doffs(self, environment: str):
- """
- Updates UI to show doffs in self.build
-
- Parameters:
- - :param environment: "space" / "ground"
- """
- doff_zipper = zip(
- self.widgets.build[environment]['doffs_spec'],
- self.build[environment]['doffs_spec'],
- self.widgets.build[environment]['doffs_variant'],
- self.build[environment]['doffs_variant'])
- for spec_combo, spec, variant_combo, variant in doff_zipper:
- spec_combo.setCurrentText(spec)
- if spec != '':
- variants = getattr(self.cache, f'{environment}_doffs')[spec].keys()
- variant_combo.addItems({''} | variants)
- variant_combo.setCurrentText(variant)
diff --git a/src/cargomanager.py b/src/cargomanager.py
index c1dcb44..0c98fde 100644
--- a/src/cargomanager.py
+++ b/src/cargomanager.py
@@ -59,8 +59,9 @@ def __init__(
'ground': dict(),
'ground_unlocks': dict()
}
- self.images_set: set[str] = set()
+ self.image_set: set[str] = set()
self.alt_images: dict[str, str] = dict()
+ self.failed_images: dict[str, int] = dict()
def load_static_data(self):
"""
@@ -113,16 +114,19 @@ def provision_cargo_data(self):
alt_images = dict()
all_images = self.get_cached_data('images_list.json')
if all_images is None:
- images_set = set()
+ image_set = set()
else:
- images_set = set(all_images)
+ image_set = set(all_images)
if images_updated:
alt_images.update(self.alt_images)
store_json__new(alt_images, self._folders['cache'] / 'alt_images.json')
- images_set |= self.images_set
- store_json__new(list(images_set), self._folders['cache'] / 'images_list.json')
+ image_set |= self.image_set
+ store_json__new(list(image_set), self._folders['cache'] / 'images_list.json')
self.alt_images = alt_images
- self.images_set = images_set
+ self.image_set = image_set
+ self.failed_images = self.get_cached_data('images_failed.json')
+ if self.failed_images is None:
+ self.failed_images = dict()
def get_cached_data(self, file_name: str) -> dict | list | None:
"""
@@ -136,7 +140,13 @@ def get_cached_data(self, file_name: str) -> dict | list | None:
if time() - last_modified < SEVEN_DAYS_IN_SECONDS:
return load_json__new(file_path)
return None
-
+
+ def store_failed_images(self):
+ """
+ Stores failed images to cache folder
+ """
+ store_json__new(self.failed_images, self._folders['cache'] / 'images_failed.json')
+
def cache_ship_data(self):
"""
Retrieves ship data and caches it.
@@ -171,7 +181,7 @@ def cache_equipment_data(self):
'type': item['type'],
'tooltip': create_equipment_tooltip__new(item, tooltip_styles)
}
- self.images_set.add(name)
+ self.image_set.add(name)
self.equipment['fore_weapons'].update(self.equipment['ship_weapon'])
self.equipment['aft_weapons'].update(self.equipment['ship_weapon'])
del self.equipment['ship_weapon']
@@ -182,7 +192,7 @@ def cache_equipment_data(self):
self.equipment['uni_consoles'].update(self.equipment['sci_consoles'])
self.equipment['uni_consoles'].update(self.equipment['eng_consoles'])
store_json__new(self.equipment, self._folders['cache'] / 'equipment.json')
-
+
def cache_trait_data(self):
"""
Retrieves personal and reputation trait data and caches it.
@@ -211,9 +221,9 @@ def cache_trait_data(self):
else:
self.ground_traits[trait_type][name] = trait_data
if trait['icon_name'] is None:
- self.images_set.add(name)
+ self.image_set.add(name)
else:
- self.images_set.add(trait['icon_name'])
+ self.image_set.add(trait['icon_name'])
self.alt_images[f'{name}__{trait["environment"]}__{trait_type}'] = (
trait['icon_name'])
# catch wrong values in trait['environment'] (cargo issue)
@@ -221,7 +231,7 @@ def cache_trait_data(self):
pass
store_json__new(self.space_traits, 'space_traits.json')
store_json__new(self.ground_traits, 'ground_traits.json')
-
+
def cache_starship_trait_data(self):
"""
Retrieves starship trait data and caches it.
@@ -231,9 +241,9 @@ def cache_starship_trait_data(self):
for ship_trait in shiptrait_cargo:
name = ship_trait['name']
if ship_trait['icon_name'] is None:
- self.images_set.add(name)
+ self.image_set.add(name)
else:
- self.images_set.add(ship_trait['icon_name'])
+ self.image_set.add(ship_trait['icon_name'])
self.alt_images[f"{name}__space__starship_traits"] = ship_trait['icon_name']
self.starship_traits[name] = {
'Page': ship_trait['Page'],
@@ -245,7 +255,7 @@ def cache_starship_trait_data(self):
f"{ship_trait['short']}{parse_wikitext(ship_trait['detailed'], styles)}")
}
store_json__new(self.starship_traits, 'starship_traits.json')
-
+
def cache_boff_data(self):
"""
Retrieves bridge officer data and caches it.
@@ -280,14 +290,15 @@ def cache_boff_data(self):
f"{desc}
{desc_long}
"
f"{parse_wikitext(dewikify(boff_ability[f'rank{decimal}info']), styles)}")
self.boff_abilities['all'][boff_name] = ability_item
- self.images_set |= self.boff_abilities['all'].keys()
+ self.image_set |= self.boff_abilities['all'].keys()
store_json__new(self.boff_abilities, 'boff_abilities.json')
-
+
def cache_modifier_data(self):
"""
Retrieves modifier data and caches it.
"""
- mod_cargo_data: list[dict[str, str | list[str] | int | None]] = self.get_cargo_data('modifiers.json', MODIFIER_QUERY)
+ mod_cargo_data: list[dict[str, str | list[str] | int | None]] = self.get_cargo_data(
+ 'modifiers.json', MODIFIER_QUERY)
for modifier in mod_cargo_data:
try:
if modifier['available'][0] == '':
@@ -333,7 +344,7 @@ def cache_duty_officer_data(self):
self.cache_doff_single(self.ground_doffs, doff)
store_json__new(self.space_doffs, 'space_doffs.json')
store_json__new(self.ground_doffs, 'ground_doffs.json')
-
+
def cache_doff_single(self, cache: dict, doff: dict):
"""
Puts a single doff into cache.
@@ -394,6 +405,18 @@ def get_cargo_data(
store_json__new(cargo_data, cargo_file)
return cargo_data
+ def backup_cargo_data(self):
+ """
+ Saves current cargo data to backup folder.
+ """
+ cargo_files = (
+ 'boff_abilities.json', 'doffs.json', 'equipment.json', 'modifiers.json',
+ 'ship_list.json', 'starship_traits.json', 'traits.json')
+ cargo_folder = self._folders['cargo']
+ backups_folder = self._folders['backups']
+ for file_name in cargo_files:
+ (cargo_folder / file_name).copy_into(backups_folder)
+
def boff_dict(self):
return {
'Tactical': [dict(), dict(), dict(), dict()],
diff --git a/src/datafunctions.py b/src/datafunctions.py
deleted file mode 100644
index 9f0923e..0000000
--- a/src/datafunctions.py
+++ /dev/null
@@ -1,685 +0,0 @@
-from datetime import datetime
-from json import dumps as json__dumps, loads as json__loads, JSONDecodeError
-import os
-from pathlib import Path
-import sys
-from zlib import compress as zlib_compress, decompress as zlib_decompress
-from numpy import array, append, fromiter, packbits, uint8, unpackbits, zeros
-from PySide6.QtGui import QImage
-from requests import Session
-from requests.cookies import create_cookie
-from requests.exceptions import (
- ConnectionError as requests__ConnectionError, Timeout as requests__Timeout)
-from requests_html import Element
-from urllib.parse import unquote_plus
-
-from .buildupdater import get_boff_spec, load_build, load_skill_pages
-from .constants import (
- BOFF_RANKS, BUILD_CONVERSION, BUILD_VERSION, CAREERS, DOFF_QUERY_URL, EQUIPMENT_TYPES,
- ITEM_QUERY_URL, MODIFIER_QUERY, PRIMARY_SPECS, SHIP_QUERY_URL,
- STARSHIP_TRAIT_QUERY_URL, TRAIT_QUERY_URL, TRAYSKILL_QUERY, WIKI_IMAGE_URL)
-from .iofunc import (
- auto_backup_cargo_file, browse_path, cache_cargo_data, copy_file, download_image,
- download_images_fast, fetch_html, get_asset_path, get_cached_cargo_data, get_cargo_data,
- get_downloaded_icons, image, load_image, load_json, read_env_file, retrieve_image,
- store_json, store_to_cache)
-from .splash import enter_splash, exit_splash, splash_text
-from .textedit import (
- create_equipment_tooltip, create_trait_tooltip, dewikify, parse_wikitext,
- sanitize_equipment_name)
-from .widgets import exec_in_thread, notempty, TagStyles, ThreadObject
-
-
-def init_backend(self):
- """
- Loads cargo and build data.
- """
- def finish_backend_init():
- splash_text(self, 'Injecting Cargo Data')
- insert_cargo_data(self)
- slot_skill_images(self)
- splash_text(self, 'Loading Build')
- load_build(self)
- exec_in_thread(self, load_images, self)
- exit_splash(self)
-
- enter_splash(self)
- load_build_file(self, str(self.config.autosave_path), update_ui=False)
- self.downloader.default_session_from_env()
- exec_in_thread(
- self, populate_cache, self, finished=finish_backend_init,
- update_splash=lambda new_text: splash_text(self, new_text))
-
-
-def insert_cargo_data(self):
- """
- Updates UI elements depending on cargo data with the loaded data
- """
- self.ship_selector_window.set_ships(self.cache.ships.keys())
- space_doff_specs = [''] + sorted(self.cache.space_doffs.keys())
- for combobox in self.widgets.build['space']['doffs_spec']:
- combobox.addItems(space_doff_specs)
- ground_doff_specs = [''] + sorted(self.cache.ground_doffs.keys())
- for combobox in self.widgets.build['ground']['doffs_spec']:
- combobox.addItems(ground_doff_specs)
-
-
-def slot_skill_images(self):
- """
- Updates the ground and skill tree, slotting the correct images into the slots.
- """
- for career_block in self.widgets.build['space_skills'].values():
- for skill_button in career_block:
- skill_button.set_item(image(self, skill_button.skill_image_name))
- for skill_group in self.widgets.build['ground_skills']:
- for skill_button in skill_group:
- skill_button.set_item(image(self, skill_button.skill_image_name))
-
-
-def populate_cache(self, threaded_worker: ThreadObject):
- """
- Loads cargo data and images into cache
-
- Parameters:
- - :param threaded_worker: worker object supplying signals
- """
- success = load_cargo_cache(self, threaded_worker)
- if not success:
- self.cache.reset_cache(keep_static_data=True)
- load_cargo_data(self, threaded_worker)
- self.cache.empty_image = QImage()
- self.cache.images_failed = get_cached_cargo_data(self, 'images_failed.json')
-
- # temporary: until self.cache has been replaced
- self.images.image_set = self.cache.images_set
- self.images.failed_images = self.cache.images_failed
- self.cargo.boff_abilities = self.cache.boff_abilities
-
- threaded_worker.update_splash.emit('Loading: Images')
- self.images.download_images(self.cache.skills)
- store_to_cache(self, self.images.failed_images, 'images_failed.json')
- load_base_images(self, threaded_worker)
-
-
-def load_cargo_cache(self, threaded_worker: ThreadObject) -> bool:
- """
- Loads cargo data for all cargo tables from cached data and puts them into variables. Returns
- True when successful, False if cache is too old
-
- Parameters:
- - :param threaded_worker: worker object supplying signals
- """
- threaded_worker.update_splash.emit('Loading: Cargo Data')
- self.cache.ships = get_cached_cargo_data(self, 'ships.json')
- if len(self.cache.ships) == 0:
- return False
- self.cache.equipment = get_cached_cargo_data(self, 'equipment.json')
- if len(self.cache.equipment) == 0:
- return False
- self.cache.traits = get_cached_cargo_data(self, 'traits.json')
- if len(self.cache.traits) == 0:
- return False
- self.cache.starship_traits = get_cached_cargo_data(self, 'starship_traits.json')
- if len(self.cache.starship_traits) == 0:
- return False
- self.cache.boff_abilities = get_cached_cargo_data(self, 'boff_abilities.json')
- if len(self.cache.boff_abilities) == 0 or len(self.cache.boff_abilities.get('all', {})) == 0:
- return False
- self.cache.modifiers = get_cached_cargo_data(self, 'modifiers.json')
- if len(self.cache.modifiers) == 0:
- return False
- self.cache.space_doffs = get_cached_cargo_data(self, 'space_doffs.json')
- if len(self.cache.space_doffs) == 0:
- return False
- self.cache.ground_doffs = get_cached_cargo_data(self, 'ground_doffs.json')
- if len(self.cache.ground_doffs) == 0:
- return False
- self.cache.alt_images = get_cached_cargo_data(self, 'alt_images.json')
- if len(self.cache.alt_images) == 0:
- return False
- self.cache.images_set = set(get_cached_cargo_data(self, 'images_list.json'))
- if len(self.cache.images_set) == 0:
- return False
- return True
-
-
-def load_cargo_data(self, threaded_worker: ThreadObject):
- """
- Loads cargo data for all cargo tables and puts them into variables.
-
- Parameters:
- - :param threaded_worker: worker object supplying signals
- """
- threaded_worker.update_splash.emit('Loading: Starships')
- ship_cargo_data = get_cargo_data(self, 'ship_list.json', SHIP_QUERY_URL)
- self.cache.ships = {ship['Page']: ship for ship in ship_cargo_data}
- store_to_cache(self, self.cache.ships, 'ships.json')
-
- tags = TagStyles(
- self.theme['tooltip']['ul'], self.theme['tooltip']['li'],
- self.theme['tooltip']['indent'])
-
- threaded_worker.update_splash.emit('Loading: Equipment')
- equipment_cargo_data = get_cargo_data(self, 'equipment.json', ITEM_QUERY_URL)
- equipment_types = set(EQUIPMENT_TYPES.keys())
- head_s = self.theme['tooltip']['equipment_head']
- subhead_s = self.theme['tooltip']['equipment_subhead']
- who_s = self.theme['tooltip']['equipment_who']
- elite_hangar = {
- 'Hangar - Elite Federation Mission Scout Ships',
- 'Hangar - Elite Valor Fighters'
- }
- for item in equipment_cargo_data:
- if item['type'] in equipment_types:
- if item['type'] == 'Hangar Bay' and item['name'] not in elite_hangar and (
- item['name'].startswith('Hangar - Advanced')
- or item['name'].startswith('Hangar - Elite')):
- continue
- name = sanitize_equipment_name(item['name'])
- self.cache.equipment[EQUIPMENT_TYPES[item['type']]][name] = {
- 'Page': item['Page'],
- 'name': name,
- 'rarity': item['rarity'],
- 'type': item['type'],
- 'tooltip': create_equipment_tooltip(item, head_s, subhead_s, who_s, tags)
- }
- self.cache.images_set.add(name)
- self.cache.equipment['fore_weapons'].update(self.cache.equipment['ship_weapon'])
- self.cache.equipment['aft_weapons'].update(self.cache.equipment['ship_weapon'])
- del self.cache.equipment['ship_weapon']
- self.cache.equipment['tac_consoles'].update(self.cache.equipment['uni_consoles'])
- self.cache.equipment['sci_consoles'].update(self.cache.equipment['uni_consoles'])
- self.cache.equipment['eng_consoles'].update(self.cache.equipment['uni_consoles'])
- self.cache.equipment['uni_consoles'].update(self.cache.equipment['tac_consoles'])
- self.cache.equipment['uni_consoles'].update(self.cache.equipment['sci_consoles'])
- self.cache.equipment['uni_consoles'].update(self.cache.equipment['eng_consoles'])
- store_to_cache(self, self.cache.equipment, 'equipment.json')
-
- threaded_worker.update_splash.emit('Loading: Traits')
- trait_cargo_data = get_cargo_data(self, 'traits.json', TRAIT_QUERY_URL)
- head_s = self.theme['tooltip']['trait_header']
- subhead_s = self.theme['tooltip']['trait_subheader']
- for trait in trait_cargo_data:
- name = trait['name']
- if trait['type'] != 'doff' and trait['type'] != 'boff' and name is not None:
- if trait['type'] == 'reputation':
- trait_type = 'rep_traits'
- elif trait['type'] == 'activereputation':
- trait_type = 'active_rep_traits'
- else:
- trait_type = 'traits'
- try:
- self.cache.traits[trait['environment']][trait_type][name] = {
- 'Page': trait['Page'],
- 'name': name,
- 'tooltip': create_trait_tooltip(
- name, trait['description'], trait_type, trait['environment'], head_s,
- subhead_s, tags)
- }
- if trait['icon_name'] is None:
- self.cache.images_set.add(name)
- else:
- self.cache.images_set.add(trait['icon_name'])
- self.cache.alt_images[f'{name}__{trait["environment"]}__{trait_type}'] = (
- trait['icon_name'])
- # catch wrong values in trait['environment'] (cargo issue)
- except (KeyError, AttributeError):
- pass
- store_to_cache(self, self.cache.traits, 'traits.json')
-
- threaded_worker.update_splash.emit('Loading: Starship Traits')
- shiptrait_cargo = get_cargo_data(self, 'starship_traits.json', STARSHIP_TRAIT_QUERY_URL)
- for ship_trait in shiptrait_cargo:
- name = ship_trait['name']
- if ship_trait['icon_name'] is None:
- self.cache.images_set.add(name)
- else:
- self.cache.images_set.add(ship_trait['icon_name'])
- self.cache.alt_images[f"{name}__space__starship_traits"] = (
- ship_trait['icon_name'])
- self.cache.starship_traits[name] = {
- 'Page': ship_trait['Page'],
- 'name': name,
- 'obtained': ship_trait['obtained'],
- 'tooltip': (
- f"{name}
"
- f"Starship Trait
"
- f"{ship_trait['short']}
{parse_wikitext(ship_trait['detailed'], tags)}")
- }
- self.cache.images_set |= self.cache.starship_traits.keys()
- store_to_cache(self, self.cache.starship_traits, 'starship_traits.json')
- store_to_cache(self, self.cache.alt_images, 'alt_images.json')
-
- threaded_worker.update_splash.emit('Loading: Bridge Officers')
- boff_head = self.theme['tooltip']['boff_header']
- boff_subhead = self.theme['tooltip']['boff_subheader']
- boff_cargo = get_cargo_data(self, 'boff_abilities.json', TRAYSKILL_QUERY)
- boff_types = CAREERS | PRIMARY_SPECS
- rank_numbers = ((1, 'I'), (2, 'II'), (3, 'III'))
- for boff_ability in boff_cargo:
- boff_region = boff_ability['region'].lower()
- boff_type = boff_ability['type']
- if boff_type not in boff_types or boff_region != 'space' and boff_region != 'ground':
- continue
- boff_name = boff_ability['name']
- ability_item = {
- 'Page': boff_ability['_pageName'],
- 'name': boff_name,
- 'I': '',
- 'II': '',
- 'III': ''
- }
- desc = boff_ability['description']
- desc_long = boff_ability['description long']
- for decimal, roman in rank_numbers:
- rank_id = BOFF_RANKS.get(boff_ability[f'rank{decimal}rank'], 0) - 1
- if rank_id >= 0:
- self.cache.boff_abilities[boff_region][boff_type][rank_id].append(
- boff_name + ' ' + roman)
- ability_item[roman] = (
- f"{boff_name} {roman}
"
- f"{desc}
{desc_long}
"
- f"{parse_wikitext(dewikify(boff_ability[f'rank{decimal}info']), tags)}")
- self.cache.boff_abilities['all'][boff_name] = ability_item
- self.cache.images_set |= self.cache.boff_abilities['all'].keys()
- store_to_cache(self, self.cache.boff_abilities, 'boff_abilities.json')
-
- threaded_worker.update_splash.emit('Loading: Modifiers')
- mod_cargo_data = get_cargo_data(self, 'modifiers.json', MODIFIER_QUERY)
- for modifier in mod_cargo_data:
- try:
- if modifier['available'][0] == '':
- modifier['available'] = list()
- except (IndexError, TypeError):
- modifier['available'] = list()
- for mod_type in modifier['type']:
- mod_name = modifier['modifier'].replace('>', '>')
- try:
- epic = bool(modifier['isepic'])
- self.cache.modifiers[EQUIPMENT_TYPES[mod_type]][mod_name] = {
- 'stats': modifier['stats'],
- 'available': modifier['available'],
- 'epic': epic,
- 'isunique': False if epic else bool(modifier['isunique']),
- }
- except KeyError:
- pass
- self.cache.modifiers['fore_weapons'].update(self.cache.modifiers['ship_weapon'])
- self.cache.modifiers['aft_weapons'].update(self.cache.modifiers['ship_weapon'])
- del self.cache.modifiers['ship_weapon']
- self.cache.modifiers['uni_consoles'].update(self.cache.modifiers['sci_consoles'])
- self.cache.modifiers['uni_consoles'].update(self.cache.modifiers['eng_consoles'])
- self.cache.modifiers['uni_consoles'].update(self.cache.modifiers['tac_consoles'])
- store_to_cache(self, self.cache.modifiers, 'modifiers.json')
-
- threaded_worker.update_splash.emit('Loading: Duty Officers')
- doff_cargo_data = get_cargo_data(self, 'doffs.json', DOFF_QUERY_URL)
- for doff in doff_cargo_data:
- doff['description'] = dewikify(doff['description'], remove_formatting=True)
- for rarity in ('white', 'green', 'blue', 'purple', 'violet', 'gold'):
- if isinstance(doff[rarity], str):
- doff[rarity] = dewikify(doff[rarity], remove_formatting=True)
- if doff['shipdutytype'] == 'Space':
- cache_doff_single(self, self.cache.space_doffs, doff)
- elif doff['shipdutytype'] == 'Ground':
- cache_doff_single(self, self.cache.ground_doffs, doff)
- elif doff['shipdutytype'] is not None:
- cache_doff_single(self, self.cache.space_doffs, doff)
- cache_doff_single(self, self.cache.ground_doffs, doff)
- store_to_cache(self, self.cache.space_doffs, 'space_doffs.json')
- store_to_cache(self, self.cache.ground_doffs, 'ground_doffs.json')
- store_to_cache(self, list(self.cache.images_set), 'images_list.json')
-
-
-def load_base_images(self, threaded_worker: ThreadObject):
- """
- Loads all images that are required for the app to start (skills, overlays)
-
- Parameters:
- - :param threaded_worker: worker object supplying signals
- """
- threaded_worker.update_splash.emit('Loading: Images (Overlays)')
- self.cache.images = {image_name: QImage() for image_name in self.cache.images_set}
- self.cache.overlays.common = QImage(get_asset_path('Common_icon.png', self.app_dir))
- self.cache.overlays.uncommon = QImage(get_asset_path('Uncommon_icon.png', self.app_dir))
- self.cache.overlays.rare = QImage(get_asset_path('Rare_icon.png', self.app_dir))
- self.cache.overlays.veryrare = QImage(get_asset_path('Very_rare_icon.png', self.app_dir))
- self.cache.overlays.ultrarare = QImage(get_asset_path('Ultra_rare_icon.png', self.app_dir))
- self.cache.overlays.epic = QImage(get_asset_path('Epic_icon.png', self.app_dir))
- self.cache.overlays.check = QImage(get_asset_path('check_overlay.png', self.app_dir))
-
- threaded_worker.update_splash.emit('Loading: Images (Skills)')
- img_folder = self.config.config_subfolders['images']
- for rank_group in self.cache.skills['space']:
- for skill_group in rank_group:
- for skill_node in skill_group['nodes']:
- self.cache.images[skill_node['image']] = retrieve_image(
- self, skill_node['image'], img_folder, threaded_worker.update_splash,
- f'{WIKI_IMAGE_URL}{skill_node['image']}.png')
- for skill_group in self.cache.skills['ground']:
- for skill_node in skill_group['nodes']:
- self.cache.images[skill_node['image']] = retrieve_image(
- self, skill_node['image'], img_folder, threaded_worker.update_splash,
- f'{WIKI_IMAGE_URL}{skill_node['image']}.png')
- self.cache.images['arrow-up'] = QImage(get_asset_path('arrow-up.png', self.app_dir))
- self.cache.images['arrow-down'] = QImage(get_asset_path('arrow-down.png', self.app_dir))
- self.cache.images['Focused Frenzy'] = retrieve_image(
- self, 'Focused Frenzy', img_folder)
- self.cache.images['Probability Manipulation'] = retrieve_image(
- self, 'Probability Manipulation', img_folder)
- self.cache.images['EPS Corruption'] = retrieve_image(
- self, 'EPS Corruption', img_folder)
-
-
-def load_images(self, threaded_worker=None):
- """
-
- Parameters:
- - :param threaded_worker: (unused; required for compatability with employed threading method)
- """
- img_folder = self.config.config_subfolders['images']
- for img_name, img in self.cache.images.items():
- if img.isNull():
- load_image(img_name, img, img_folder)
-
-
-def download_images(self, threaded_worker: ThreadObject):
- """
- Downloads all images not already in the images folder and puts them into cache. Returns set of
- images not to be retried in this cycle.
- """
- no_retry_images = set()
- now = datetime.now()
- for img, timestamp in self.cache.images_failed.items():
- if (datetime.fromtimestamp(timestamp) - now).days < 7:
- no_retry_images.add(img)
- else:
- self.cache.images_failed.pop(img)
- images = self.cache.images_set - no_retry_images - get_downloaded_icons(
- Path(self.config.config_subfolders['images']))
- img_folder = self.config.config_subfolders['images']
-
- images_to_download = images - self.cache.boff_abilities['all'].keys()
- for image_name in images_to_download:
- threaded_worker.update_splash.emit(f'Downloading Image: {image_name}')
- download_image(self, image_name, img_folder)
-
- boff_images_to_download = images & self.cache.boff_abilities['all'].keys()
- for image_name in boff_images_to_download:
- threaded_worker.update_splash.emit(f'Downloading Image: {image_name}')
- image_url = f'{WIKI_IMAGE_URL}{image_name.replace(' ', '_')}_icon_(Federation).png'
- download_image(self, image_name, img_folder, image_url)
- return no_retry_images
-
-
-def cache_doff_single(self, cache: dict, doff: dict):
- """
- Puts a single doff into cache.
-
- Parameters:
- - :param cache: cache dictionary to store doff into
- - :param doff: the doff itself
- """
- try:
- cache[doff['spec']][doff['description']] = doff
- except KeyError:
- cache[doff['spec']] = dict()
- cache[doff['spec']][doff['description']] = doff
-
-
-def cache_skills(skill_cache: dict[str, dict], app_directory: str):
- """
- Loads skills into cache.
- """
- space_skill_data = load_json(get_asset_path('space_skills.json', app_directory))
- skill_cache['space'] = space_skill_data['space']
- skill_cache['space_unlocks'] = space_skill_data['space_unlocks']
- ground_skill_data = load_json(get_asset_path('ground_skills.json', app_directory))
- skill_cache['ground'] = ground_skill_data['ground']
- skill_cache['ground_unlocks'] = ground_skill_data['ground_unlocks']
-
-
-def empty_build(self, build_type: str = 'full') -> dict:
- """
- Creates empty build and returns it.
-
- Parameters:
- - :param build_type: `build` -> space and ground build; `skills` -> space and ground skills;
- `full` -> space and ground build and skills
- """
- # None means not available on the build; empty string means empty slot
- new_build = {
- '_version': BUILD_VERSION,
- 'space': {
- 'active_rep_traits': [None] * 5,
- 'aft_weapons': [None] * 5,
- 'boffs': [[None] * 4, [None] * 4, [None] * 4, [None] * 4, [None] * 4, [None] * 4],
- 'boff_specs': [[None, None]] * 6,
- 'core': [''],
- 'deflector': [''],
- 'devices': [None] * 6,
- 'doffs_spec': [''] * 6,
- 'doffs_variant': [''] * 6,
- 'eng_consoles': [None] * 5,
- 'engines': [''],
- 'experimental': [None],
- 'fore_weapons': [None] * 5,
- 'hangars': [None] * 2,
- 'rep_traits': [None] * 5,
- 'sci_consoles': [None] * 5,
- 'sec_def': [None],
- 'shield': [''],
- 'ship': '',
- 'ship_name': '',
- 'ship_desc': '',
- 'starship_traits': [None] * 7,
- 'tac_consoles': [None] * 5,
- 'tier': '',
- 'traits': ['', '', '', '', '', '', '', '', '', None, None, ''],
- 'uni_consoles': [None] * 3,
- },
- 'ground': {
- 'active_rep_traits': [None] * 5,
- 'armor': [''],
- 'boffs': [[''] * 4, [''] * 4, [''] * 4, [''] * 4],
- 'boff_profs': ['Tactical'] * 4,
- 'boff_specs': ['Command'] * 4,
- 'ground_desc': '',
- 'ground_devices': ['', '', '', '', None],
- 'doffs_spec': [''] * 6,
- 'doffs_variant': [''] * 6,
- 'ev_suit': [''],
- 'kit': [''],
- 'kit_modules': ['', '', '', '', '', None],
- 'rep_traits': [''] * 5,
- 'personal_shield': [''],
- 'traits': ['', '', '', '', '', '', '', '', '', None, None, ''],
- 'weapons': [''] * 2,
- },
- 'captain': {
- 'career': '',
- 'elite': False,
- 'faction': '',
- 'name': '',
- 'primary_spec': '',
- 'secondary_spec': '',
- 'species': '',
- },
- }
-
- new_skills = {
- '_version': BUILD_VERSION,
- 'space_skills': {
- 'eng': [False] * 30,
- 'sci': [False] * 30,
- 'tac': [False] * 30,
- },
- 'skill_unlocks': {
- 'eng': [None] * 5,
- 'sci': [None] * 5,
- 'tac': [None] * 5,
- 'ground': [None] * 5
- },
- 'ground_skills': [
- [False] * 6,
- [False] * 6,
- [False] * 4,
- [False] * 4
- ],
- 'skill_desc': {
- 'space': '',
- 'ground': ''
- }
- }
-
- if build_type == 'build':
- return new_build
- elif build_type == 'full':
- new_build.update(new_skills)
- return new_build
- elif build_type == 'skills':
- return new_skills
-
-
-def backup_cargo_data(self):
- """
- Saves current cargo data to backup folder.
- """
- cargo_files = (
- 'boff_abilities.json', 'doffs.json', 'equipment.json', 'modifiers.json',
- 'ship_list.json', 'starship_traits.json', 'traits.json')
- cargo_folder = self.config.config_subfolders['cargo']
- backups_folder = self.config.config_subfolders['backups']
- for file_name in cargo_files:
- cargo_path = str(cargo_folder / file_name)
- backups_path = str(backups_folder / file_name)
- copy_file(cargo_path, backups_path)
-
-
-def get_icon_set(cargo_dir: Path) -> set[str]:
- """
- Creates set of all required icons from cargo data and required static images.
-
- Parameters:
- - :param cargo_dir: path to cargo data directory
- """
- images_set = set()
- equipment_cargo_data = load_json(str(cargo_dir / 'equipment.json'))
- equipment_types = set(EQUIPMENT_TYPES.keys())
- elite_hangar = {
- 'Hangar - Elite Federation Mission Scout Ships',
- 'Hangar - Elite Valor Fighters'
- }
- for item in equipment_cargo_data:
- if item['type'] in equipment_types:
- if item['type'] == 'Hangar Bay' and item['name'] not in elite_hangar and (
- item['name'].startswith('Hangar - Advanced')
- or item['name'].startswith('Hangar - Elite')):
- continue
- images_set.add(sanitize_equipment_name(item['name']))
- trait_cargo_data = load_json(str(cargo_dir / 'traits.json'))
- for trait in trait_cargo_data:
- if trait['type'] != 'doff' and trait['type'] != 'boff' and trait['name'] is not None:
- if trait['icon_name'] is None:
- images_set.add(trait['name'])
- else:
- images_set.add(trait['icon_name'])
- shiptrait_cargo_data = load_json(str(cargo_dir / 'starship_traits.json'))
- for ship_trait in shiptrait_cargo_data:
- if ship_trait['icon_name'] is None:
- images_set.add(ship_trait['name'])
- else:
- images_set.add(ship_trait['icon_name'])
- return images_set
-
-
-def get_skill_icons(skill_cache: dict[str, dict]) -> set[str]:
- """
- """
- icons = set()
- for rank_group in skill_cache['space']:
- for skill_group in rank_group:
- for skill_node in skill_group['nodes']:
- icons.add(skill_node['image'])
- for skill_group in skill_cache['ground']:
- for skill_node in skill_group['nodes']:
- icons.add(skill_node['image'])
- return icons
-
-
-def get_boff_icons(boff_cache: dict[str, dict]) -> set[str]:
- """
- """
- return set(boff_cache['all'].keys())
-
-
-def get_ship_icons(ship_list: list[dict[str]]) -> set[str]:
- """
- """
- icon_set = set()
- for ship in ship_list:
- try:
- icon_set.add(ship['image'][5:])
- except TypeError:
- pass
- return icon_set
-
-
-def build_cache(app_dir: Path) -> int:
- """
- Builds cache in config folder indicated by `config_path`. Returns status: success: `0`,
- failure: `1`
-
- Parameters:
- - :param config_path: path to build cache into
- """
- config_path = app_dir / '.config'
- env_variables = read_env_file(config_path / '.env', ['SETS_CF_CLEARANCE', 'SETS_USER_AGENT'])
- requests_session = Session()
- if 'SETS_CF_CLEARANCE' in env_variables:
- print(f'[Info] "SETS_CF_CLEARANCE" variable: "{env_variables["SETS_CF_CLEARANCE"][:10]}"')
- print(f'[Info] "SETS_CF_CLEARANCE" variable: "{env_variables["SETS_CF_CLEARANCE"][-10:]}"')
- requests_session.cookies.set_cookie(
- create_cookie(name='cf_clearance', value=env_variables['SETS_CF_CLEARANCE']))
- if 'SETS_USER_AGENT' in env_variables:
- print(f'[Info] "SETS_USER_AGENT" variable: "{env_variables["SETS_USER_AGENT"][:10]}"')
- print(f'[Info] "SETS_USER_AGENT" variable: "{env_variables["SETS_USER_AGENT"][-10:]}"')
- requests_session.headers['User-Agent'] = env_variables['SETS_USER_AGENT']
- cargo_dir = config_path / 'cargo'
- success = list()
- success.append(cache_cargo_data(cargo_dir / 'ship_list.json', SHIP_QUERY_URL, requests_session))
- success.append(cache_cargo_data(cargo_dir / 'equipment.json', ITEM_QUERY_URL, requests_session))
- success.append(cache_cargo_data(cargo_dir / 'traits.json', TRAIT_QUERY_URL, requests_session))
- success.append(cache_cargo_data(
- cargo_dir / 'starship_traits.json', STARSHIP_TRAIT_QUERY_URL, requests_session))
- success.append(cache_cargo_data(cargo_dir / 'modifiers.json', MODIFIER_QUERY, requests_session))
- success.append(cache_cargo_data(cargo_dir / 'doffs.json', DOFF_QUERY_URL, requests_session))
-
- image_dir = config_path / 'images'
- downloaded_images = get_downloaded_icons(image_dir)
- ultimate_icons = {'Focused Frenzy', 'Probability Manipulation', 'EPS Corruption'}
- images_set = (get_icon_set(cargo_dir) | ultimate_icons) - downloaded_images
- if len(images_set) > 0:
- download_images_fast(list(images_set), env_variables, image_dir)
- skill_cache = dict()
- cache_skills(skill_cache, app_dir)
- skill_images = get_skill_icons(skill_cache) - downloaded_images
- if len(skill_images) > 0:
- download_images_fast(list(skill_images), env_variables, image_dir, image_suffix='.png')
- boff_cache = load_json(cargo_dir / 'boff_abilities.json')
- boff_images = get_boff_icons(boff_cache) - downloaded_images
- if len(boff_images) > 0:
- download_images_fast(
- list(boff_images), env_variables, image_dir, image_suffix='_icon_(Federation).png')
-
- downloaded_ship_images = set(
- map(lambda x: unquote_plus(x), os.listdir(str(config_path / 'ship_images'))))
- ship_list = load_json(str(cargo_dir / 'ship_list.json'))
- ship_images = get_ship_icons(ship_list) - downloaded_ship_images
- if len(ship_images) > 0:
- download_images_fast(
- list(ship_images), env_variables, config_path / 'ship_images', image_suffix='')
-
- if False in success:
- return 1
- return 0
diff --git a/src/imagemanager.py b/src/imagemanager.py
index 17154dd..f92ec6a 100644
--- a/src/imagemanager.py
+++ b/src/imagemanager.py
@@ -102,7 +102,6 @@ def download_images(self, skill_cache: dict[str, dict]):
del self.failed_images[image_name]
available_images = self.get_downloaded_icons() | no_retry_images
- # TODO have all icons (including skills) in image_set from the start
ultimate_skill_icons = {'Focused Frenzy', 'Probability Manipulation', 'EPS Corruption'}
image_set = self.image_set | ultimate_skill_icons
images = image_set - available_images - self._cargo_cache.boff_abilities['all'].keys()
From c72f07b581b447ce6868203ada562572bf5f2092 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Tue, 12 May 2026 22:12:32 +0200
Subject: [PATCH 21/44] updaing splash
---
src/app.py | 16 +++++++-
src/splash.py | 101 ++++++++++++++++++++++++++++++++++++++++----------
2 files changed, 97 insertions(+), 20 deletions(-)
diff --git a/src/app.py b/src/app.py
index 4dd67b2..5522570 100644
--- a/src/app.py
+++ b/src/app.py
@@ -196,20 +196,31 @@ def init_backend(self):
"""
Sets up downloader and provides cargo data and images.
"""
+ self.splash.show_splash(True)
+ self.splash.set_loading_text('Loading Cargo Data...')
+ self.splash.init_progress('Steps completed:', 3)
self.downloader.default_session_from_env()
self.cargo.provision_cargo_data()
self.images.image_set = self.cargo.image_set
self.images.failed_images = self.cargo.failed_images
+ self.splash.increment_progress()
+ self.splash.set_loading_text('Downloading Images...')
self.images.download_images(self.cargo.skills)
self.cargo.store_failed_images()
+ self.splash.increment_progress()
+ self.splash.set_loading_text('Loading Base Images...')
self.images.load_base_images()
+ self.splash.increment_progress()
def complete_app_init(self):
"""
Updates ui and starts thread to load images.
"""
+ self.splash.set_loading_text('Populating UI...')
self.init_ui()
+ self.splash.set_loading_text('Loading Build...')
self.build_loader.load_build_file(self.config.autosave_path)
+ self.splash.set_loading_text('Loading Images...')
self._backend_thread = Thread(self.images.load_images)
self._backend_thread.start()
@@ -1368,7 +1379,7 @@ def setup_splash(self, frame: QFrame):
"""
layout = GridLayout()
layout.setRowStretch(0, 1)
- layout.setRowStretch(3, 1)
+ layout.setRowStretch(4, 1)
layout.setColumnStretch(0, 3)
layout.setColumnStretch(1, 2)
layout.setColumnStretch(2, 3)
@@ -1377,6 +1388,9 @@ def setup_splash(self, frame: QFrame):
loading_label = create_label2(self.theme2, 'Loading: ...', 'label_subhead')
self.splash.loading_label = loading_label
layout.addWidget(loading_label, 2, 0, 1, 3, alignment=AHCENTER)
+ progress_label = create_label2(self.theme2, '', 'label_subhead')
+ self.splash.progress_label = progress_label
+ layout.addWidget(progress_label, 3, 0, 1, 3, alignment=AHCENTER)
frame.setLayout(layout)
def hide_tooltips(self):
diff --git a/src/splash.py b/src/splash.py
index 86e279a..e27dee4 100644
--- a/src/splash.py
+++ b/src/splash.py
@@ -5,34 +5,97 @@
class SplashScreen(QObject):
"""Manages splash screen"""
- show_splash: Signal = Signal(bool)
+ show: Signal = Signal(bool)
+ loading_text: Signal = Signal(str)
+ progress_init: Signal = Signal(str, int)
+ progress_step: Signal = Signal()
def __init__(self):
super().__init__()
self.loading_label: QLabel
+ self.progress_label: QLabel
self.tabber: QTabWidget
+ self._progress_text: str = 'Progress:'
+ self._progress_total: int = 0
+ self._progress_current: int = 0
+ self.show.connect(self._show_splash)
+ self.loading_text.connect(self._set_loading_text)
+ self.progress_init.connect(self._init_progress)
+ self.progress_step.connect(self._increment_progress)
+ def show_splash(self, visible: bool):
+ """
+ Shows/hides splash.
-def enter_splash(self):
- """
- Shows splash screen
- """
- self.widgets.loading_label.setText('Loading: ...')
- self.widgets.splash_tabber.setCurrentIndex(1)
+ Parameters:
+ - :param visible: `True` to show splash, `False` to hide it
+ """
+ self.show.emit(visible)
+ def init_progress(self, message: str, total_progress: int):
+ """
+ Makes progress label ready.
-def exit_splash(self):
- """
- Leaves splash screen
- """
- self.widgets.splash_tabber.setCurrentIndex(0)
+ Parameters:
+ - :param message: progress message
+ - :param total_progress: total number of steps
+ """
+ self.progress_init.emit(message, total_progress)
+ def increment_progress(self):
+ """
+ Increments progress count by 1.
+ """
+ self.progress_step.emit()
-def splash_text(self, new_text: str):
- """
- Updates the label of the splash screen with new text
+ def set_loading_text(self, message: str):
+ """
+ Sets loading labels' text.
- Parameters:
- - :param new_text: will be displayed on the splsh screen
- """
- self.widgets.loading_label.setText(new_text)
+ Parameters:
+ - :param message: message to show
+ """
+ self.loading_text.emit(message)
+
+ def _show_splash(self, visible: bool):
+ """
+ Shows/hides splash.
+
+ Parameters:
+ - :param visible: `True` to show splash, `False` to hide it
+ """
+ if visible:
+ self.tabber.setCurrentIndex(1)
+ else:
+ self.tabber.setCurrentIndex(0)
+
+ def _init_progress(self, message: str, total_progress: int):
+ """
+ Makes progress label ready.
+
+ Parameters:
+ - :param message: progress message
+ - :param total_progress: total number of steps
+ """
+ self._progress_text = message
+ self._progress_total = total_progress
+ self._progress_current = 0
+ self.progress_label.setText(
+ f'{self._progress_text} ({self._progress_current:>4}/{self._progress_total:>4})')
+
+ def _increment_progress(self):
+ """
+ Increments progress count by 1.
+ """
+ self._progress_current += 1
+ self.progress_label.setText(
+ f'{self._progress_text} ({self._progress_current:>4}/{self._progress_total:>4})')
+
+ def _set_loading_text(self, message: str):
+ """
+ Sets loading labels' text.
+
+ Parameters:
+ - :param message: message to show
+ """
+ self.loading_label.setText(message)
From 356311ee879c1b183c659f5d84663aa6ff5b396f Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 08:07:51 +0200
Subject: [PATCH 22/44] removing unused code
---
main.py | 610 ++------------------------------------------
src/app.py | 88 +++----
src/buildhelpers.py | 2 +
src/downloader.py | 3 +
src/iofunc.py | 477 +---------------------------------
src/style.py | 174 -------------
src/textedit.py | 176 -------------
src/widgets.py | 295 +--------------------
8 files changed, 58 insertions(+), 1767 deletions(-)
delete mode 100644 src/style.py
diff --git a/main.py b/main.py
index 63a22c5..34e8afc 100644
--- a/main.py
+++ b/main.py
@@ -1,617 +1,39 @@
from argparse import ArgumentParser
-import os
-from pathlib import Path
+from os.path import abspath as os__abspath, dirname as os__dirname
import sys
from src import SETS
-from src.datafunctions import build_cache
class Launcher():
- version = '2026.03b250'
- __version__ = '2.4'
-
- # holds the style of the app
- theme = {
- # general style
- 'app': {
- 'bg': '#1a1a1a',
- 'fg': '#eeeeee',
- 'sets': '#c59129',
- 'font': ('Overpass', 11, 'normal'),
- 'heading': ('Overpass', 14, 'bold'),
- 'subhead': ('Overpass', 12, 'medium'),
- 'font-fallback': ('Yu Gothic UI', 'Nirmala UI', 'Microsoft YaHei UI', 'sans-serif'),
- 'frame_thickness': 8,
- # this styles every item of the given type
- 'style': {
- # scroll bar trough (invisible)
- 'QScrollBar': {
- 'background': 'none',
- 'border-style': 'none',
- 'border-radius': 0,
- 'margin': 0
- },
- 'QScrollBar:vertical': {
- 'width': 8,
- },
- 'QScrollBar:horizontal': {
- 'height': 8,
- },
- # space above and below the scrollbar handle
- 'QScrollBar::add-page, QScrollBar::sub-page': {
- 'background': 'none'
- },
- # scroll bar handle
- 'QScrollBar::handle': {
- 'background-color': 'rgba(100,100,100,.75)',
- 'border-radius': 4,
- 'border': 'none'
- },
- # scroll bar arrow buttons
- 'QScrollBar::add-line, QScrollBar::sub-line': {
- 'height': 0 # hiding the arrow buttons
- }
- }
- },
- # shortcuts, @bg -> means bg in this sub-dictionary
- 'defaults': {
- 'bg': '#1a1a1a', # background
- 'mbg': '#242424', # medium background
- 'lbg': '#404040', # light background
- 'sets': '#c59129', # accent
- 'lsets': '#60c59129', # light accent
- 'font': ('Overpass', 11, 'normal'),
- 'heading': ('Overpass', 14, 'bold'),
- 'subhead': ('Overpass', 12, 'medium'),
- 'small_text': ('Overpass', 10, 'normal'),
- 'fg': '#eeeeee', # foreground (usually text)
- 'mfg': '#bbbbbb', # medium foreground
- 'bc': '#888888', # border color
- 'bw': 1, # border width
- 'br': 2, # border radius
- 'sep': 2, # seperator -> width of major seperating lines
- 'margin': 10, # default margin between widgets
- 'csp': 5, # child spacing -> content margin
- 'isp': 15, # item spacing
- },
- # dark frame
- 'frame': {
- 'background-color': '@bg',
- 'border-style': 'none',
- 'margin': 0,
- 'padding': 0
- },
- # medium frame
- 'medium_frame': {
- 'background-color': '@mbg',
- 'margin': 0,
- 'padding': 0
- },
- # light frame
- 'light_frame': {
- 'background': '@lbg',
- 'margin': 0,
- 'padding': 0
- },
- # default text (non-button, non-entry, non table)
- 'label': {
- 'color': '@fg',
- 'margin': (3, 0, 3, 0),
- 'qproperty-indent': '0', # disables auto-indent
- 'border-style': 'none',
- 'font': '@font'
- },
- # default text (non-button, non-entry, non table)
- 'hint_label': {
- 'color': '@mfg',
- 'margin': (3, 0, 3, 0),
- 'qproperty-indent': '0', # disables auto-indent
- 'border-style': 'none',
- 'font': '@font'
- },
- # heading label
- 'label_heading': {
- 'color': '@fg',
- 'qproperty-indent': '0',
- 'border-style': 'none',
- 'font': '@heading'
- },
- # label for subheading
- 'label_subhead': {
- 'color': '@fg',
- 'qproperty-indent': '0',
- 'border-style': 'none',
- 'margin-bottom': 3,
- 'font': '@subhead'
- },
- # default button
- 'button': {
- 'background-color': 'none',
- 'color': '@fg',
- 'text-decoration': 'none',
- 'border-width': '@bw',
- 'border-style': 'solid',
- 'border-color': '@sets',
- 'margin': (3, 3, 3, 3),
- 'padding': (2, 5, 0, 5),
- 'font': ('Overpass', 13, 'medium'),
- ':hover': {
- 'border-color': '@bc'
- },
- ':disabled': {
- 'color': '@bc'
- },
- # Tooltip
- '~QToolTip': {
- 'background-color': '@mbg',
- 'border-style': 'solid',
- 'border-color': '@lbg',
- 'border-width': '@bw',
- 'padding': (0, 0, 0, 0),
- 'color': '@fg',
- 'font': 'Overpass'
- }
- },
- # heavy button
- 'heavy_button': {
- 'background-color': '@sets',
- 'color': '@fg',
- 'text-decoration': 'none',
- 'border-width': '@bw',
- 'border-style': 'solid',
- 'border-color': '@sets',
- 'margin': (3, 3, 3, 3),
- 'padding': (2, 5, 0, 5),
- 'font': ('Overpass', 13, 'bold'),
- ':hover': {
- 'background-color': '@mbg'
- },
- ':disabled': {
- 'color': '@bc'
- }
- },
- # build item button
- 'item': {
- 'background-color': '#242424',
- 'border-width': 1,
- 'border-color': '#888888',
- 'border-highlight-color': '#ffd700'
- },
- # build item button
- 'item_dark': {
- 'background-color': '#1a1a1a',
- 'border-width': 1,
- 'border-color': '#404040',
- },
- # checkbox
- 'checkbox': {
- '::indicator': {
- 'width': 16,
- 'height': 16,
- 'border-style': 'solid',
- 'border-width': '@bw',
- 'border-color': '@bc',
- 'background-color': '@lbg',
- },
- '::indicator:hover': {
- 'border-color': '@sets'
- },
- '::indicator:checked': {
- 'image': 'url(local/check.svg)'
- },
- '::indicator:unchecked': {
- 'image': 'url(local/uncheck.svg)',
- }
- },
- # holds sub-pages
- 'tabber': {
- 'background-color': 'none',
- 'border': 'none',
- 'margin': 0,
- 'padding': 0,
- '::pane': {
- 'border': 'none',
- }
- },
- # default tabber buttons (hidden)
- 'tabber_tab': {
- '::tab': {
- 'height': 0,
- 'width': 0
- }
- },
- # combo box
- 'combobox': {
- 'border-style': 'solid',
- 'border-width': '@bw',
- 'border-color': '@bc',
- 'background-color': '@bg',
- 'padding': (1, 5, 1, 5),
- 'color': '@fg',
- 'font': '@subhead',
- '::down-arrow': {
- 'image': 'url(local/thick-chevron-down.svg)',
- 'width': '@margin',
- },
- '::drop-down': {
- 'border-style': 'none',
- 'padding': (2, 2, 2, 2)
- },
- '~QAbstractItemView': {
- 'background-color': '@mbg',
- 'border-style': 'solid',
- 'border-color': '@bc',
- 'border-width': '@bw',
- 'border-radius': '@br',
- 'color': '@fg',
- 'outline': '0',
- '::item': {
- 'border-width': '@bw',
- 'border-style': 'solid',
- 'border-color': '@mbg',
- },
- '::item:hover': {
- 'border-color': '@sets',
- },
- }
- },
- # additional style for doff combobox
- 'doff_combo': {
- 'color': '@fg',
- 'border-style': 'none',
- 'border-width': 0,
- 'margin': 0,
- 'font': '@small_text'
- },
- # additional style for boff combobox
- 'boff_combo': {
- 'font': '@font',
- ':disabled': {
- 'border-color': '@bg',
- 'border-left-width': 0,
- 'padding-left': 0
- },
- '::down-arrow:disabled': {
- 'image': 'none',
- 'width': '@margin',
- },
- },
- # auto-completion popup of combobox
- 'popup': {
- 'background-color': '@mbg',
- 'border-style': 'solid',
- 'border-color': '@bc',
- 'border-width': '@bw',
- 'border-radius': '@br',
- 'color': '@fg',
- 'outline': '0',
- '::item': {
- 'border-width': '@bw',
- 'border-style': 'solid',
- 'border-color': '@mbg',
- },
- '::item:hover': {
- 'border-color': '@sets',
- },
- },
- # line of user-editable text
- 'entry': {
- 'background-color': '@mbg',
- 'color': '@fg',
- 'border-width': '@bw',
- 'border-style': 'solid',
- 'border-color': '@bc',
- 'font': '@subhead',
- 'selection-background-color': '@lsets',
- # cursor is inside the line
- ':focus': {
- 'border-color': '@sets'
- },
- ':hover': {
- 'background-color': '@lbg'
- }
- },
- # for item tooltips
- 'infobox': {
- 'background-color': '#000000',
- 'border-style': 'none',
- 'color': '@fg',
- # 'margin': 0,
- # 'padding': 0,
- },
- 'infobox_frame': {
- 'background-color': '#000000',
- 'border-style': 'solid',
- 'border-width': '@bw',
- 'border-color': '@mbg',
- 'border-radius': '@br',
- # 'margin': 0,
- # 'padding': '@sep',
- },
- # tooltip for TooltipLabel
- 'label_tooltip': {
- 'color': '@fg',
- 'background-color': '@bg',
- 'border-color': '@lbg',
- 'border-radius': '@br',
- 'border-style': 'solid',
- 'border-width': '@bw',
- 'font': '@font',
- 'padding': 2,
- 'qproperty-indent': '0', # disables auto-indent
- },
- # for formatting tooltip text, will contain css from tooltip_def
- 'tooltip': {},
- 'tooltip_def': {
- 'indent': {
- 'margin': (0, 0, 0, 20),
- },
- 'ul': {
- 'margin': (0, 0, 0, 20),
- '-qt-list-indent': '0',
- },
- 'li': {
- 'margin-bottom': 1,
- },
- 'boff_header': {
- 'color': '#42afca',
- 'font-size': 'large',
- 'font-weight': 'bold',
- 'margin': 0
- },
- 'boff_subheader': {
- 'font-size': 10,
- 'margin': (0, 0, 20, 0)
- },
- 'trait_header': {
- 'color': '#42afca',
- 'font-size': 'large',
- 'font-weight': 'bold',
- 'margin': 0, # padding: 0
- },
- 'trait_subheader': {
- 'color': '#42afca',
- 'font-size': 10,
- 'margin': (0, 0, 20, 0),
- },
- 'equipment_name': {
- 'font-size': 'large',
- 'font-weight': 'bold',
- 'margin': 0
- },
- 'equipment_type_subheader': {
- 'font-size': 10,
- 'margin': (0, 0, 20, 0),
- },
- 'equipment_head': {
- 'color': '#42afca',
- 'font-size': 12,
- 'margin': (10, 0, 0, 0)
- },
- 'equipment_subhead': {
- 'color': '#f4f400',
- 'font-size': 10,
- 'margin': 0
- },
- 'equipment_who': {
- 'color': '#ff6347',
- 'font-size': 10,
- 'margin': (0, 0, 10, 0)
- },
- 'skill_ultimate_name': {
- 'color': '#ffd700;',
- 'font-size': 12,
- 'margin': (10, 0, 0, 0)
- },
- },
- # picker window
- 'picker': {
- 'background-color': '@bg',
- 'border-color': '@sets',
- 'border-width': 3,
- 'border-style': 'solid',
- 'border-radius': '@br'
- },
- # list widget displaying items in picker
- 'picker_list': {
- 'background-color': '@bg',
- 'color': '@fg',
- 'border-style': 'none',
- 'margin': 0,
- 'font': '@font',
- 'outline': '0', # removes dotted line around clicked item
- '::item': {
- 'border-width': '@bw',
- 'border-style': 'solid',
- 'border-color': '@bg',
- },
- '::item:selected': {
- 'background-color': '@bg',
- 'border-width': '@bw',
- 'border-style': 'solid',
- 'border-color': '@bg',
- },
- # selected but not the last click of the user
- '::item:selected:!active': {
- 'color': '@fg'
- },
- '::item:hover': {
- 'background-color': '@lbg',
- },
- '~QScrollBar': {
- 'border-style': 'none',
- 'border': 'none',
- 'border-radius': 0
- }
- },
- # large text editor
- 'textedit': {
- 'background-color': '@mbg',
- 'border-style': 'solid',
- 'border-width': '@bw',
- 'border-color': '@bc',
- 'font': '@font',
- 'color': '@fg',
- 'padding': 3,
- 'selection-background-color': '@lsets'
- },
- # context menu
- 'context_menu': {
- 'background-color': '@bg',
- 'border-color': '@lbg',
- 'border-width': '@bw',
- 'border-style': 'solid',
- 'border-radius': '@br',
- 'padding': '@sep',
- '::item': {
- 'color': '@fg',
- 'font': '@font',
- 'border-color': '@bg',
- 'border-radius': 0,
- 'border-style': 'solid',
- 'border-width': '@bw',
- 'padding': (3, 3, 1, 10),
- },
- '::icon': {
- 'padding': (1, 1, 1, 10),
- },
- '::item:selected': {
- 'border-color': '@sets',
- },
- '::item:disabled': {
- 'color': '@mfg'
- },
- '::item:disabled:selected': {
- 'border-color': '@bg'
- }
- },
- # frame for duty officers
- 'doff_frame': {
- 'background-color': '@bg',
- 'border-style': 'solid',
- 'border-width': '@bw',
- 'border-color': '@bc',
- 'padding': 2
- },
- # segment of the bonus bar
- 'bonus_bar': {
- ':disabled': {
- 'border-style': 'solid',
- 'border-top-style': 'none',
- 'border-bottom-style': 'none',
- 'border-width': '@bw',
- 'border-color': '@bc',
- 'background-color': '@bg',
- },
- ':checked': {
- 'background-color': '@sets'
- }
- },
- # label holding career / ground icon
- 'unlock_label': {
- 'border-style': 'none',
- 'border-top-style': 'solid',
- 'border-top-width': 1,
- 'border-top-color': '@bc',
- 'margin': (0, 0, 3, 0),
- 'padding': (3, 10, 0, 10)
- },
- # horizontal seperator
- 'hr': {
- 'background-color': '@lbg',
- 'border-style': 'none',
- 'height': 1
- },
- # horizontal sliding selector
- 'slider': {
- 'font': ('Roboto Mono', 11, 'Normal'),
- 'color': '@fg',
- '::groove:horizontal': {
- 'border-style': 'none',
- 'background-color': '@lbg',
- 'border-radius': '@bw',
- 'height': 3
- },
- '::handle:horizontal': {
- 'border-style': 'solid',
- 'border-width': '@bw',
- 'border-color': '@bc',
- 'background-color': '@bc',
- 'width': 6,
- 'margin-top': -7,
- 'margin-bottom': -7
- },
- '::handle:horizontal:hover': {
- 'border-color': '@sets'
- },
- '::handle:horizontal:pressed': {
- 'background-color': '#666666'
- },
- },
- # small window
- 'dialog_window': {
- 'background-color': '@sets'
- },
- }
+ __version__ = '3.0'
@staticmethod
def base_path() -> str:
"""initialize the base path"""
- if getattr(sys, 'frozen', False):
- base_path = os.path.dirname(sys.executable)
- else:
- base_path = os.path.abspath(os.path.dirname(__file__))
+ try:
+ base_path = sys._MEIPASS
+ except Exception:
+ if getattr(sys, 'frozen', False):
+ # The application is frozen
+ base_path = os__dirname(sys.executable)
+ else:
+ base_path = os__abspath(os__dirname(__file__))
return base_path
- @staticmethod
- def app_config() -> dict:
- config = {
- 'settings_path': '.SETS_settings.ini',
- 'config_folder_path': '.config',
- 'config_subfolders': {
- 'library': 'library',
- 'cache': 'cache',
- 'cargo': 'cargo',
- 'images': 'images',
- 'ship_images': 'ship_images',
- 'backups': 'backups',
- 'auto_backups': 'auto_backups'
- },
- 'autosave_filename': '.autosave.json',
- 'box_width': 49,
- 'box_height': 64,
- 'link_website': 'https://stobuilds.com/apps/sets',
- 'link_github': 'https://github.com/STOCD',
- 'link_discord': 'https://discord.gg/kxwHxbsqzF',
- 'link_downloads': 'https://github.com/STOCD/SETS/releases',
- 'default_settings': {
- 'ui_scale': 1.0,
- 'default_mark': '',
- 'default_rarity': 'Common',
- 'picker_relative': 0,
- 'default_save_format': 'JSON',
- 'geometry': None,
- 'pref_backup': 0
- }
- }
- return config
-
@staticmethod
def launch():
argparser = ArgumentParser(prog='SETS', description='STO Equipment and Trait Selector')
+ # argparser.add_argument(
+ # '--build-cache', action='store_true', required=False,
+ # help='Provide this flag to build the cache instead of starting the app.')
argparser.add_argument(
- '--build-cache', action='store_true', required=False,
- help='Provide this flag to build the cache instead of starting the app.')
+ '--config_dir', type=str, required=False,
+ help='Change configuration directory (must be readable and writable)')
args, _ = argparser.parse_known_args()
- if args.build_cache:
- exit_code = build_cache(Path(Launcher.base_path()))
- sys.exit(exit_code)
exit_code = SETS(
- theme=Launcher.theme, args=args,
- path=Launcher.base_path(), config=Launcher.app_config(),
- versions=(Launcher.__version__, Launcher.version)).run()
+ args=args, app_dir_path=Launcher.base_path(), version=Launcher.__version__).run()
sys.exit(exit_code)
diff --git a/src/app.py b/src/app.py
index 5522570..46dc13e 100644
--- a/src/app.py
+++ b/src/app.py
@@ -28,8 +28,8 @@
create_annotated_slider2, create_button2, create_button_series2, create_checkbox2,
create_combo_box2, create_entry2, create_frame2, create_item_button2, create_label2)
from .widgets import (
- Cache, DoffCombobox, GridLayout, HBoxLayout, ImageLabel, ItemButton, ShipButton, ShipImage,
- Tabbers, Thread, TooltipLabel, VBoxLayout, WidgetStorage)
+ DoffCombobox, GridLayout, HBoxLayout, ImageLabel, ItemButton, ShipButton, ShipImage,
+ Tabbers, Thread, TooltipLabel, VBoxLayout)
# only for developing; allows to terminate the qt event loop with keyboard interrupt
# from signal import signal, SIGINT, SIG_DFL
@@ -38,49 +38,25 @@
class SETS():
- app_dir = None
- # (release version, dev version)
- versions = ('', '')
- # stores widgets that need to be accessed from outside their creating function
- widgets: WidgetStorage
- # stores refined cargo data
- cache: Cache
- # stores current build
- build: dict
- # for picking items
- picker_window: Picker
- # for selecting ships
- ship_selector_window: ShipSelector
- # for editing equipment items
- edit_window: ItemEditor
- # context menu for equipment
- context_menu: ContextMenu
- # shows markdown export
- export_window: ExportWindow
-
- def __init__(self, theme, args, path, config, versions):
+ def __init__(self, args, app_dir_path: str, version: str):
"""
Creates new Instance of SETS
Parameters:
+ - :param args: command line arguments, following arguments must be accessible
+ - `args.config_dir`: contains override for config dir, `str` or `None`
+ - :param app_dir_path: absolute path to install directory
- :param version: version of the app
- - :param theme: dict -> default theme
- - :param args: command line arguments
- - :param path: absolute path to directory containing the main.py file
- - :param config: app configuration (!= settings these are not changed by the user)
"""
- self.versions = versions
- self.theme = theme
+ self.version: str = version
self.args = args
- self.app_dir = path
- self.app_dir2: Path = Path(path)
- self.widgets = WidgetStorage()
- self.cache = Cache()
+ self.app_dir: Path = Path(app_dir_path)
+ self.app_dir2: Path = Path(app_dir_path)
self.config: SETSConfig = SETSConfig()
self.config.config_dir = self.get_config_dir_path()
self.settings = SETSSettings(self.config.config_dir / self.config.settings_file)
self.init_config()
- QDir.addSearchPath('local_folder', os.path.join(path, 'local'))
+ QDir.addSearchPath('local_folder', self.app_dir / 'local')
self.theme2: AppTheme = AppTheme(self.config.ui_scale)
self.init_environment()
self.downloader = Downloader(
@@ -99,8 +75,6 @@ def __init__(self, theme, args, path, config, versions):
self.tabbers: Tabbers = Tabbers()
self.app, self.window = self.create_main_window()
self.cache_icons()
- self.building = True
- self.build = self.empty_build()
self.cargo.load_static_data()
self.setup_main_layout()
self.build_loader: BuildLoader = BuildLoader(
@@ -181,9 +155,6 @@ def init_config(self):
"""
self.config.autosave_path = self.config.config_dir / self.config.autosave_filename
self.config.ui_scale = self.settings.ui_scale
- # TODO move these to new theme
- self.box_width = self.config.box_width * self.config.ui_scale * 0.8
- self.box_height = self.config.box_height * self.config.ui_scale * 0.8
def init_environment(self):
"""
@@ -228,23 +199,22 @@ def cache_icons(self):
"""
Loads static icons.
"""
- self.cache.icons['copy'] = load_icon('copy.png', self.app_dir2)
- self.cache.icons['paste'] = load_icon('paste.png', self.app_dir2)
- self.cache.icons['clear'] = load_icon('clear.png', self.app_dir2)
- self.cache.icons['edit'] = load_icon('edit.png', self.app_dir2)
- self.cache.icons['link'] = load_icon('external_link.png', self.app_dir2)
- self.cache.icons['dual_cannons'] = load_icon('DC_icon.svg', self.app_dir2, size=(16, 24.5))
+ self.theme2.icons['copy'] = load_icon('copy.png', self.app_dir2)
+ self.theme2.icons['paste'] = load_icon('paste.png', self.app_dir2)
+ self.theme2.icons['clear'] = load_icon('clear.png', self.app_dir2)
+ self.theme2.icons['edit'] = load_icon('edit.png', self.app_dir2)
+ self.theme2.icons['link'] = load_icon('external_link.png', self.app_dir2)
+ self.theme2.icons['dual_cannons'] = load_icon('DC_icon.svg', self.app_dir2, size=(16, 24.5))
icon_size = (self.theme2.opt.box_width * 1.2, self.theme2.opt.box_width * 1.2)
- self.cache.icons['ground'] = load_icon('ground_icon.png', self.app_dir2, icon_size)
+ self.theme2.icons['ground'] = load_icon('ground_icon.png', self.app_dir2, icon_size)
icon_size = (self.theme2.opt.box_width, self.theme2.opt.box_width)
- self.cache.icons['tac'] = load_icon('tac_icon.png', self.app_dir2, icon_size)
- self.cache.icons['sci'] = load_icon('sci_icon.png', icon_size)
- self.cache.icons['eng'] = load_icon('eng_icon.png', icon_size)
- self.cache.icons['tac-small'] = load_icon('tac-small.svg', self.app_dir2, size=(25, 25))
- self.cache.icons['sci-small'] = load_icon('sci-small.svg', self.app_dir2, size=(25, 25))
+ self.theme2.icons['tac'] = load_icon('tac_icon.png', self.app_dir2, icon_size)
+ self.theme2.icons['sci'] = load_icon('sci_icon.png', icon_size)
+ self.theme2.icons['eng'] = load_icon('eng_icon.png', icon_size)
+ self.theme2.icons['tac-small'] = load_icon('tac-small.svg', self.app_dir2, size=(25, 25))
+ self.theme2.icons['sci-small'] = load_icon('sci-small.svg', self.app_dir2, size=(25, 25))
icon_size = (self.theme2.opt.box_height, self.theme2.opt.box_width * 182 / 106)
- self.cache.icons['STOCD'] = load_icon('stocd.png', self.app_dir2, icon_size)
- self.theme2.icons = self.cache.icons
+ self.theme2.icons['STOCD'] = load_icon('stocd.png', self.app_dir2, icon_size)
def main_window_close_callback(self, event: QCloseEvent):
"""
@@ -289,7 +259,7 @@ def init_ui(self):
space_doff_specs = [''] + sorted(self.cargo.space_doffs.keys())
for combobox in self.build2.space.doffs_spec:
combobox.addItems(space_doff_specs)
- ground_doff_specs = [''] + sorted(self.cache.ground_doffs.keys())
+ ground_doff_specs = [''] + sorted(self.cargo.ground_doffs.keys())
for combobox in self.build2.ground.doffs_spec:
combobox.addItems(ground_doff_specs)
for career_block in self.build2.skills.space.values():
@@ -325,11 +295,11 @@ def picker(
if specialization == 'Temporal Operative':
specialization = 'Temporal'
else:
- profession = self.build['ground']['boff_profs'][boff_id]
- specialization = self.build['ground']['boff_specs'][boff_id]
+ profession = self.build2['ground']['boff_profs'][boff_id]
+ specialization = self.build2['ground']['boff_specs'][boff_id]
items = self.cargo.boff_abilities[environment][profession][build_subkey]
if specialization != '':
- items = items + self.cache.boff_abilities[environment][specialization][build_subkey]
+ items = items + self.cargo.boff_abilities[environment][specialization][build_subkey]
elif build_key == 'starship_traits':
items = self.cargo.starship_traits.keys()
image_suffix = '__space__starship_traits'
@@ -1568,9 +1538,9 @@ def setup_settings_frame(self):
footer_frame = self.tabbers.character_frames[2]
footer_layout = GridLayout(margins=csp, spacing=isp)
version_label = create_label2(
- self.theme2, f"Version: {self.versions[0]}\n({self.versions[1]})", 'hint_label')
+ self.theme2, f"Version: {self.version}", 'hint_label')
footer_layout.addWidget(version_label, 0, 0, alignment=ALEFT | ABOTTOM)
stocd_label = create_label2(self.theme2, '')
- stocd_label.setPixmap(self.cache.icons['STOCD'])
+ stocd_label.setPixmap(self.theme2.icons['STOCD'])
footer_layout.addWidget(stocd_label, 0, 1, alignment=ARIGHT | ABOTTOM)
footer_frame.setLayout(footer_layout)
diff --git a/src/buildhelpers.py b/src/buildhelpers.py
index 2ad1b31..a05f9a4 100644
--- a/src/buildhelpers.py
+++ b/src/buildhelpers.py
@@ -102,6 +102,7 @@ def empty_build(build_type: str = 'full') -> dict[str, int | dict[str]]:
elif build_type == 'skills':
return new_skills
+
def get_variable_slot_counts(ship_data: dict[str], ship_tier: str) -> tuple[int]:
"""
returns the number of universal consoles, devices and starship traits the given ship build
@@ -149,6 +150,7 @@ def get_variable_slot_counts(ship_data: dict[str], ship_tier: str) -> tuple[int]
tac_consoles += 1
return uni_consoles, eng_consoles, sci_consoles, tac_consoles, devices, starship_traits
+
def get_boff_spec(seat_details: str) -> tuple[int, str, str]:
"""
Returns rank, profession and specialization from cargo string
diff --git a/src/downloader.py b/src/downloader.py
index ba9fa67..6b479ef 100644
--- a/src/downloader.py
+++ b/src/downloader.py
@@ -5,6 +5,7 @@
from requests.exceptions import Timeout
from time import time
from threading import Thread
+from typing import Callable
from urllib.parse import quote_plus
from .constants import GITHUB_CACHE_URL, WIKI_IMAGE_URL
@@ -14,6 +15,8 @@
class ReturnValueThread(Thread):
def __init__(self, target, args: tuple = tuple()):
super().__init__(target=target, args=args)
+ self._target: Callable
+ self._args: tuple
self._return = None
def run(self):
diff --git a/src/iofunc.py b/src/iofunc.py
index 9d99c5b..b76778a 100644
--- a/src/iofunc.py
+++ b/src/iofunc.py
@@ -1,36 +1,16 @@
-from datetime import datetime
import json
from json import dump as json__dump, load as json__load, JSONDecodeError
import os
from pathlib import Path
-from shutil import copyfile as shutil__copyfile, rmtree as shutil__rmtree
+from shutil import rmtree as shutil__rmtree
import sys
-from threading import Thread
-from urllib.parse import quote_plus, unquote_plus
+from urllib.parse import quote_plus
from webbrowser import open as webbrowser_open
-from PySide6.QtGui import QIcon, QImage, QPixmap
+from PySide6.QtGui import QIcon, QPixmap
from PySide6.QtWidgets import QFileDialog, QWidget
-import requests
-from requests.cookies import create_cookie as requests__create_cookie
-from requests_html import HTMLSession
-from .constants import WIKI_IMAGE_URL, WIKI_URL
-from .textedit import compensate_json
-
-
-class ReturnValueThread(Thread):
- def __init__(self, target, args: tuple = tuple()):
- super().__init__(target=target, args=args)
- self._return = None
-
- def run(self):
- if self._target is not None:
- self._return = self._target(*self._args)
-
- def join(self):
- super().join()
- return self._return
+from .constants import WIKI_URL
def browse_path(
@@ -65,233 +45,6 @@ def browse_path(
return None
-def get_cargo_data(self, filename: str, url: str, ignore_cache_age=False) -> dict | list:
- """
- Retrieves cargo data for specific table. Downloads cargo data from wiki if cargo cache is empty.
- Updates cargo cache.
-
- Parameters:
- - :param filename: filename of cache file
- - :param url: url to cargo table
- - :param ignore_cache_age: True if cache of any age should be accepted
- """
- filepath = str(self.config.config_subfolders['cargo'] / filename)
- cargo_data = None
-
- # try loading from cache
- if os.path.exists(filepath) and os.path.isfile(filepath):
- last_modified = os.path.getmtime(filepath)
- if (datetime.now() - datetime.fromtimestamp(last_modified)).days < 7 or ignore_cache_age:
- try:
- return load_json(filepath)
- except json.JSONDecodeError:
- pass
-
- # download cargo data if loading from cache failed or data should be updated
- try:
- cargo_data = self.downloader.download_cargo_table(url, filename)
- if cargo_data is not None:
- auto_backup_cargo_file(self, filename)
- store_json(cargo_data, filepath)
- return cargo_data
- except (requests.exceptions.RequestException, json.JSONDecodeError):
- if ignore_cache_age:
- backup_path = str(self.config.config_subfolders['backups'] / filename)
- auto_backup_path = str(self.config.config_subfolders['auto_backups'] / filename)
- if self.settings.pref_backup == 0:
- backup_paths = (auto_backup_path, backup_path)
- else:
- backup_paths = (backup_path, auto_backup_path)
- for path in backup_paths:
- if os.path.exists(path) and os.path.isfile(path):
- try:
- cargo_data = load_json(path)
- store_json(cargo_data, filepath)
- return cargo_data
- except json.JSONDecodeError:
- pass
- sys.stderr.write(f'[Error] Cargo table could not be retrieved ({filename})\n')
- sys.exit(1)
- else:
- return get_cargo_data(self, filename, url, ignore_cache_age=True)
-
-
-def get_cached_cargo_data(self, filename: str) -> dict | list:
- """
- Retrieves cached cargo data from filename. Returns empty dict when cache is too old or
- corrupted.
-
- Parameters:
- - :param filename: name of the cache file
- """
- filepath = str(self.config.config_subfolders['cache'] / filename)
- if os.path.exists(filepath) and os.path.isfile(filepath):
- last_modified = os.path.getmtime(filepath)
- if (datetime.now() - datetime.fromtimestamp(last_modified)).days < 7:
- try:
- return load_json(filepath)
- except json.JSONDecodeError:
- pass
- return {}
-
-
-def store_to_cache(self, data, filename: str):
- """
- Stores data to cache file with filename.
-
- Parameters:
- - :param data: data that will be stored
- - :param filename: filename of the cache file
- """
- filepath = str(self.config.config_subfolders['cache'] / filename)
- store_json(data, filepath)
-
-
-def retrieve_image(
- self, name: str, image_folder_path: str, signal=None, url_override: str = '') -> QImage:
- """
- Downloads image or fetches image from cache.
-
- Parameters:
- - :param name: name of the item
- - :param image_folder_path: path to the image folder
- - :param signal: signal that is emitted to chance splash when downloading image (optional)
- - :param url_override: non default image url (optional)
- """
- filename = get_image_file_name(name)
- filepath = os.path.join(image_folder_path, filename)
- image = QImage(filepath)
- if image.isNull():
- if signal is not None:
- signal.emit(f'Downloading Image: {name}')
- image = download_image(self, name, image_folder_path, url_override)
- return image
-
-
-def download_image(self, name: str, image_folder_path: str, url_override: str = ''):
- """
- Downloads image from wiki and stores it in images folder. Returns the image.
-
- Parameters:
- - :param name: name of the item
- - :param image_folder_path: path to the image folder
- - :param url_override: non default image url (optional)
- """
- filepath = os.path.join(image_folder_path, get_image_file_name(name))
- if url_override == '':
- image_url = f'{WIKI_IMAGE_URL}{name.replace(' ', '_')}_icon.png'
- else:
- image_url = url_override
- image_response = requests.get(image_url)
- image = QImage()
- if image_response.ok:
- image.loadFromData(image_response.content, 'png')
- image.save(filepath)
- else:
- self.cache.images_failed[name] = int(datetime.now().timestamp())
- return image
-
-
-def get_ship_image(self, image_name: str, threaded_worker):
- """
- Tries to fetch ship image from local filesystem, downloads it otherwise. Returns the image.
-
- Parameters:
- - :image_name: filename of the image
- - :param threaded_worker: thread object supplying signals
- """
- image_url = WIKI_IMAGE_URL + image_name.replace(' ', '_')
- image_path = str(
- self.config.config_subfolders['ship_images'] / quote_plus(image_name))
- _, _, fmt = image_name.rpartition('.')
- image = QImage(image_path)
- if image.isNull():
- image_response = requests.get(image_url)
- if image_response.ok:
- image.loadFromData(image_response.content, fmt)
- image.save(image_path)
- # else: returns null image
- threaded_worker.result.emit((image,))
-
-
-def load_image(image_name: str, image: QImage, image_folder_path: str) -> QImage:
- """
- Retrieves image from images folder and returns it. Assumes the image exists.
-
- Parameters:
- - :param image_name: name of the image
- - :param image: preconstructed (empty) Image
- - :param image_folder_path: path to the image folder
- """
- image_path = os.path.join(image_folder_path, get_image_file_name(image_name))
- image.load(image_path)
-
-
-def image(self, image_name: str) -> QImage:
- """
- Returns image from cache if cached, loads and returns image if not cached.
-
- Parameters:
- - :param image_name: name of the image
- """
- img = self.cache.images[image_name]
- if img.isNull():
- img_folder = self.config.config_subfolders['images']
- load_image(image_name, img, img_folder)
- return img
-
-
-def alt_image(self, image_name: str, image_suffix: str) -> QImage:
- """
- Returns image from cache if cached, loads and returns image if not cached. If `image_suffix` is
- not empty, tries to get alternate image first.
-
- Parameters:
- - :param image_name: name of the image
- - :param image_suffix: suffix to check in self.cache.alt_images
- """
- if image_name + image_suffix in self.cache.alt_images:
- return image(self, self.cache.alt_images[image_name + image_suffix])
- else:
- return image(self, image_name)
-
-
-def auto_backup_cargo_file(self, filename: str):
- """
- Backs up given cargo data file to the auto backups folder
-
- Parameters:
- - :param filename: name of the file to back up
- """
- source_path = str(self.config.config_subfolders['cargo'] / filename)
- if os.path.exists(source_path):
- target_path = str(self.config.config_subfolders['auto_backups'] / filename)
- shutil__copyfile(source_path, target_path)
-
-
-# --------------------------------------------------------------------------------------------------
-# static functions
-# --------------------------------------------------------------------------------------------------
-
-
-def get_downloaded_icons(images_dir: Path) -> set:
- """
- Returns set containing all images currently in the images folder.
- """
- return set(map(lambda x: unquote_plus(x)[:-4], os.listdir(str(images_dir))))
-
-
-def create_folder(path_to_folder):
- """
- Creates the folder at path_to_folder in case it does not exist.
-
- Parameters:
- - :param path_to_folder: absolute path to folder
- """
- if not os.path.exists(path_to_folder) and not os.path.isdir(path_to_folder):
- os.mkdir(path_to_folder)
-
-
def delete_folder_contents(path_to_folder):
"""
Delets all files and folders within a folder.
@@ -304,33 +57,6 @@ def delete_folder_contents(path_to_folder):
os.mkdir(path_to_folder)
-def copy_file(source_path, target_path):
- """
- Tries to copy file from `source_path` to `target_path`
-
- Parameters:
- - :param source_path: file to copy
- - :param target_path: location and name of the target file
- """
- if os.path.exists(source_path) and os.path.isfile(source_path):
- shutil__copyfile(source_path, target_path)
-
-
-def get_asset_path(asset_name: str, app_directory: str) -> str:
- """
- returns the absolute path to a file in the asset folder
-
- Parameters:
- - :param asset_name: filename of the asset
- - :param app_directory: absolute path to app directory
- """
- fp = os.path.join(app_directory, 'local', asset_name)
- if os.path.exists(fp):
- return fp
- else:
- return ''
-
-
def load_icon(filename: str, app_directory: Path, size: tuple = tuple()) -> QIcon | QPixmap:
"""
Loads icon from path and returns it.
@@ -408,85 +134,6 @@ def store_json(data: dict | list, path: str):
sys.stdout.write(f'[Error] Data could not be saved: {e}')
-def fetch_json(url: str) -> dict | list:
- """
- Fetches json from url and returns parsed object. Raises `requests.exceptions.JSONDecodeError` if
- result cannot be decoded. Raises `requests.exceptions.Timeout` or 2 download attempts failed.
-
- Parameters:
- - :param url: URL to file
- """
- try:
- r = requests.get(url, timeout=10)
- except requests.exceptions.Timeout:
- r = requests.get(url, timeout=10)
- r.encoding = 'utf-8'
- return json.loads(compensate_json(r.text))
-
-
-def fetch_html(url: str):
- """
- Fetches html from url and returns plain text. Raises requests.exceptions.Timeout if
- 2 download attempts failed.
-
- Parameters:
- - :param url: URL to file
- """
- session = HTMLSession()
- r = session.get(url)
- return r.html
-
-
-def sanitize_file_name(txt, chr_set='extended') -> str:
- """
- Converts txt to a valid filename.
-
- Parameters:
- - :param txt: The path to convert.
- - :param chr_set:
- - 'printable': Any printable character except those disallowed on Windows/*nix.
- - 'extended': 'printable' + extended ASCII character codes 128-255
- - 'universal': For almost *any* file system.
- """
- FILLER = '-'
- MAX_LEN = 255 # Maximum length of filename is 255 bytes in Windows and some *nix flavors.
-
- # Step 1: Remove excluded characters.
- BLACK_LIST = set(chr(127) + r'<>:"/\|?*')
- white_lists = {
- 'universal': {'-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'},
- 'printable': {chr(x) for x in range(32, 127)} - BLACK_LIST, # 0-32, 127 are unprintable,
- 'extended': {chr(x) for x in range(32, 256)} - BLACK_LIST,
- }
- white_list = white_lists[chr_set]
- result = ''.join(x if x in white_list else FILLER for x in txt)
-
- # Step 2: Device names, '.', and '..' are invalid filenames in Windows.
- DEVICE_NAMES = (
- 'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6',
- 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8',
- 'LPT9', 'CONIN$', 'CONOUT$', '..', '.')
- if '.' in txt:
- name, _, ext = result.rpartition('.')
- ext = f'.{ext}'
- else:
- name = result
- ext = ''
- if name in DEVICE_NAMES:
- result = f'-{result}-{ext}'
-
- # Step 3: Truncate long files while preserving the file extension.
- if len(result) > MAX_LEN:
- result = result[:MAX_LEN - len(ext)] + ext
-
- # Step 4: Windows does not allow filenames to end with '.' or ' ' or begin with ' '.
- result = result.strip()
- while len(result) > 0 and result[-1] == '.':
- result = result[:-1]
-
- return result
-
-
def get_image_file_name(name: str) -> str:
"""
Converts image name to valid file name
@@ -506,119 +153,3 @@ def open_wiki_page(page_name: str):
Converts page name to URL and opens page in webbrowser.
"""
open_url(WIKI_URL + page_name.replace(' ', '_'))
-
-
-def read_env_file(path: Path, names: list[str]) -> dict[str, str]:
- """
- Reads given `names` from env file at `path` and returns dictionary containing them.
-
- Parameters:
- - :param path: path to env file
- - :param names: variables to read from env file
- """
- env_variables = dict()
- if not path.exists():
- return env_variables
- with path.open(encoding='utf-8') as env_file:
- for line in env_file:
- for identifier in names:
- if line.startswith(f'{identifier}='):
- if line[-1] == '\n':
- env_variables[identifier] = line[len(identifier) + 1:-1]
- else:
- env_variables[identifier] = line[len(identifier) + 1:]
- break
- return env_variables
-
-
-def cache_cargo_data(cache_file: Path, url: str, session: requests.Session) -> bool:
- """
- Obtains cargo data from `url` and stores it. Returns `True` on success, `False` on failure.
-
- Parameters:
- - :param cache_file: path to file that the cargo data should be stored to
- - :param url: url to request data from
- - :param session: request session to use for the request
- """
- try:
- response = session.get(url, timeout=10)
- except requests.exceptions.Timeout:
- sys.stdout.write(f'[Error] Requesting the following URL timed out:\n[Error] {url}\n')
- return False
- if response.ok:
- response.encoding = 'utf-8'
- try:
- cargo_data = json.loads(compensate_json(response.text))
- store_json(cargo_data, str(cache_file))
- return True
- except json.JSONDecodeError:
- sys.stdout.write(
- f'[Error] Decoding the response failed for the following URL:\n[Error] {url}\n')
- return False
-
-
-def download_image_session(
- session: requests.Session, name: str, image_folder_path: Path,
- failed_images: dict[str, int], image_suffix: str = '_icon.png'):
- """
- """
- if image_suffix == '':
- # exception for ship images
- filepath = image_folder_path / quote_plus(name)
- image_type = None
- else:
- filepath = image_folder_path / get_image_file_name(name)
- image_type = 'png'
- image_url = WIKI_IMAGE_URL + name.replace(' ', '_') + image_suffix
- image_response = session.get(image_url)
- image = QImage()
- if image_response.ok:
- image.loadFromData(image_response.content, image_type)
- image.save(str(filepath))
- else:
- failed_images[name] = int(datetime.now().timestamp())
-
-
-def download_images_list(
- images_list: list[str], env_variables: dict[str, str], images_path: Path,
- image_suffix: str = '_icon.png') -> dict[str, int]:
- """
- """
- requests_session = requests.Session()
- if 'SETS_CF_CLEARANCE' in env_variables:
- requests_session.cookies.set_cookie(
- requests__create_cookie(name='cf_clearance', value=env_variables['SETS_CF_CLEARANCE']))
- if 'SETS_USER_AGENT' in env_variables:
- requests_session.headers['User-Agent'] = env_variables['SETS_USER_AGENT']
- failed_images = dict()
- for image_name in images_list:
- download_image_session(
- requests_session, image_name, images_path, failed_images, image_suffix)
- return failed_images
-
-
-def download_images_fast(
- images_list: list[str], env_variables: dict[str, str], images_dir: Path,
- image_suffix: str = '_icon.png'):
- """
- Downloads images using multiple threads.
- """
- total_threads = 16
- image_chunk_size = len(images_list) // total_threads
- while image_chunk_size < 4 and total_threads > 1:
- total_threads -= 1
- image_chunk_size = len(images_list) // total_threads
- threads: list[ReturnValueThread] = list()
- for thread_num in range(total_threads):
- if thread_num == total_threads - 1:
- images = images_list[image_chunk_size * thread_num:]
- else:
- images = images_list[image_chunk_size * thread_num:image_chunk_size * (thread_num + 1)]
- thread = ReturnValueThread(
- target=download_images_list, args=(images, env_variables, images_dir, image_suffix))
- thread.start()
- threads.append(thread)
- failed_images = dict()
- for thread in threads:
- failed_images.update(thread.join())
- print(failed_images)
diff --git a/src/style.py b/src/style.py
deleted file mode 100644
index 0d642fd..0000000
--- a/src/style.py
+++ /dev/null
@@ -1,174 +0,0 @@
-import copy
-
-from PySide6.QtGui import QFont
-
-WEIGHT_CONVERSION = {
- 'normal': QFont.Weight.Normal,
- 'bold': QFont.Weight.Bold,
- 'extrabold': QFont.Weight.ExtraBold,
- 'medium': QFont.Weight.Medium
-}
-
-
-def get_style(self, widget, override: dict = {}) -> str:
- """
- Returns style sheet according to default style of widget with override style.
-
- Parameters:
- - :param widget: None or str -> name of the widget style in self.theme (may be empty or None if
- only the style in override should be applied)
- - :param override: dict -> contains additional style (optional)
-
- :return: str containing css style sheet
- """
- if widget is None or widget == '':
- return get_css(self, override)
- elif widget != 'app' and widget != 'defaults' and widget != 's.c' and widget in self.theme:
- if len(override) > 0:
- style = merge_style(self, self.theme[widget], override)
- else:
- style = self.theme[widget]
- return get_css(self, style)
-
-
-def get_style_class(self, class_name: str, widget, override={}) -> str:
- """
- Returns style sheet according to default style of widget with override style. Style only
- applies to class_name. Sub-controls, pseudo-states and descendant selectors (marked with "~")
- defined in self.theme and override are correctly handled.
-
- Parameters:
- - :param class_name: str -> name of the widget to be styled
- - :param widget: None or str -> name of the widget style in self.theme (may be empty or None if
- only the style in override should be applied)
- - :param override: dict -> contains additional style (optional)
-
- :return: str containing css style sheet
- """
- if widget is None or widget == '':
- style = override
- elif widget != 'app' and widget != 'defaults' and widget != 's.c' and widget in self.theme:
- if len(override) > 0:
- style = merge_style(self, self.theme[widget], override)
- else:
- style = self.theme[widget]
- else:
- raise KeyError(
- f'Parameter widget=`{widget}` must be None or key of self.theme '
- 'except `app` or `defaults`.')
- main = f'{class_name} {{{get_css(self, style)}}}'
- for k, v in style.items():
- if k.startswith(':'):
- main += f''' {class_name}{k} {{{get_css(self, v)}}}'''
- elif k.startswith('~'):
- main += f' {get_style_class(self, f"{class_name} {k[1:]}", None, v)}'
- return main
-
-
-def merge_style(self, s1: dict, s2: dict) -> dict:
- """
- Returns new dictionary where the given styles are merged.
- Up to one sub-dictionary is merged recursively.
-
- Parameters:
- - :param s1: Style-dict 1
- - :param s2: Style-dict 2
-
- :return: merged dictionary
- """
- result = copy.deepcopy(s1)
- for k, v in s2.items():
- if k in result.keys() and isinstance(result[k], dict) and isinstance(v, dict):
- result[k].update(v)
- else:
- result[k] = v
- return result
-
-
-def get_css(self, style: dict) -> str:
- """
- Converts style dictionary into css style sheet. Escapes '@' - shortcuts with their respective
- values.
- """
- css = str()
- ui_scale = self.config.ui_scale
- for key, val in style.items():
- if isinstance(val, str) and val.startswith('@'):
- v = self.theme['defaults'][val[1:]]
- else:
- v = val
- if key.startswith(':') or key.startswith('~') or key == 'font':
- continue
- elif isinstance(v, int):
- css += f'{key}:{v * ui_scale}px;'
- elif isinstance(v, tuple):
- css += f'''{key}:{'px '.join(map(lambda s: str(s * ui_scale), v))}px;'''
- else:
- css += f'{key}:{v};'
- return css
-
-
-def theme_font(self, key=None, font_spec=()) -> QFont:
- """
- Returns QFont object with font specified in self.theme or font_spec. Adds default fallback font
- families.
-
- Parameters:
- - :param key: key in self.theme to access font tuple like: self.theme[key]['font']
- - :param font_spec: font tuple consisting of family, size and weight OR font shortcut (optional)
-
- :return: configured QFont object
- """
- try:
- if len(font_spec) != 3 and isinstance(font_spec, tuple):
- font_spec = self.theme[key]['font']
- if isinstance(font_spec, str) and font_spec.startswith('@'):
- font = self.theme['defaults'][font_spec[1:]]
- else:
- font = font_spec
- except KeyError:
- font = self.theme['app']['font']
- font_family = (font[0], *self.theme['app']['font-fallback'])
- font_size = int(font[1] * self.config.ui_scale)
- try:
- font_weight = WEIGHT_CONVERSION[font[2]]
- except KeyError:
- font_weight = QFont.Weight.Normal
- font = QFont(font_family, font_size, font_weight)
- font.setHintingPreference(QFont.HintingPreference.PreferNoHinting)
- font.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
- return font
-
-
-def create_style_sheet(self, d: dict) -> str:
- """
- Creates Stylesheet from dictionary. Dictionary keys represent css selector.
-
- Parameters:
- - :param d: dict -> style dictionary
-
- :return: string containing css sheet
- """
- style = str()
- for s, v in d.items():
- style += f'{s} {{{get_css(self, v)}}}'
- return style
-
-
-def prepare_tooltip_css(self):
- """
- Converts dictionaries containing tooltip style to css
- """
- ui_scale = self.config.ui_scale
- tooltips = self.theme['tooltip']
- for tag, style in self.theme['tooltip_def'].items():
- css = ''
- for prop, val in style.items():
- if isinstance(val, int):
- unit = 'pt' if prop == 'font-size' else 'px'
- css += f'{prop}:{val * ui_scale}{unit};'
- elif isinstance(val, tuple):
- css += f'''{prop}:{'px '.join(map(lambda s: str(s * ui_scale), val))}px;'''
- else:
- css += f'{prop}:{val};'
- tooltips[tag] = css
diff --git a/src/textedit.py b/src/textedit.py
index 858662d..f4eedb7 100644
--- a/src/textedit.py
+++ b/src/textedit.py
@@ -4,46 +4,6 @@
from .theme import TooltipCSS
-def get_tooltip(self, name: str, type_: str, environment: str = 'space') -> str:
- """
- Returns tooltip for trait.
-
- Parameters:
- - :param name: name of the trait
- - :param type_: type of the trait ("rep_traits", "traits", "starship_traits", ...)
- - :param environment: "space" / "ground"
- """
- if type_ == 'starship_traits':
- return self.cache.starship_traits[name]['tooltip']
- else:
- return self.cache.traits[environment][type_][name]['tooltip']
-
-
-def add_equipment_tooltip_header(self, item: dict, tooltip_body: str, item_type: str) -> str:
- """
- Adds equipment header including name, mark, modifiers, rarity and item type to the tooltip body
- and returns the complete tooltip.
-
- Parameters:
- - :param item: item to create the tooltip for
- - :param tooltip_body: already created tooltip body
- - :param item_type: type of the item to add to the subtitle
- """
- rarity_color = f'color:{RARITY_COLORS[item['rarity']]};'
- head_style = self.theme['tooltip']['equipment_name'] + rarity_color
- subhead_style = self.theme['tooltip']['equipment_type_subheader'] + rarity_color
- item_title = item['item']
- if item['mark'] != '' and item['mark'] is not None:
- item_title += ' ' + item['mark']
- mods = ' '.join(mod for mod in item['modifiers'] if mod != '' and mod is not None)
- if mods != '':
- item_title += ' ' + mods
- tooltip = (
- f"{item_title}
"
- f"{item['rarity']} {self.cache.equipment[item_type][item['item']]['type']}
")
- return tooltip + tooltip_body
-
-
def add_equipment_tooltip_header__new(
item: dict[str, str], item_data: dict[str], tooltip_styles: TooltipCSS) -> str:
"""
@@ -109,39 +69,6 @@ def format_skill_tooltip(
f"{skill_data['gdesc']}
{skill_data['nodes'][node_index]['desc']}
")
-def get_skill_unlock_tooltip_ground(self, unlock_id: int, unlock_choice: int):
- """
- gets tooltip for ground unlock from cache and formats it
-
- Parameters:
- - :param unlock_id: id of the unlock, counted from the unlock with the lowest requirement
- - :param unlock_choice: `0` (first choice; "down") or `1` (second choice; "up")
- """
- unlock = self.cache.skills['ground_unlocks'][unlock_id]['nodes'][unlock_choice]
- head_style = f"{self.theme['tooltip']['equipment_name']}color:#ffd700;"
- subhead_style = f"{self.theme['tooltip']['equipment_type_subheader']}color:#ffd700;"
- return (
- f"{unlock['name']}
"
- f"Ground Skill
{unlock['desc']}
")
-
-
-def get_skill_unlock_tooltip_space(self, career: str, unlock_id: int, unlock_choice: int):
- """
- gets tooltip for space unlock from cache and formats it
-
- Parameters:
- - :param career: "eng" / "sci" / "tac"
- - :param unlock_id: id of the unlock, counted from the unlock with the lowest requirement
- - :param unlock_choice: `0` (first choice; "down") or `1` (second choice; "up")
- """
- unlock = self.cache.skills['space_unlocks'][career][unlock_id]['nodes'][unlock_choice]
- head_style = f"{self.theme['tooltip']['equipment_name']}color:#ffd700;"
- subhead_style = f"{self.theme['tooltip']['equipment_type_subheader']}color:#ffd700;"
- return (
- f"{unlock['name']}
"
- f"Space Skill
{unlock['desc']}
")
-
-
def get_ultimate_skill_unlock_tooltip__new(
unlock: dict[str], unlock_choice: int, enhancements: int, tooltip_styles: TooltipCSS):
"""
@@ -178,74 +105,6 @@ def get_ultimate_skill_unlock_tooltip__new(
return tooltip
-def get_ultimate_skill_unlock_tooltip(self, career: str, unlock_choice: int, enhancements: int):
- """
- gets tooltip for space unlock from cache and formats it
-
- Parameters:
- - :param unlock_id: id of the unlock, counted from the unlock with the lowest requirement
- - :param unlock_choice: `0` (first choice; "down") or `1` (second choice; "up")
- """
- unlock = self.cache.skills['space_unlocks'][career][4]
- head_style = f"{self.theme['tooltip']['equipment_name']}color:#ffd700;"
- subhead_style = f"{self.theme['tooltip']['equipment_type_subheader']}color:#ffd700;"
- enhancement_style = self.theme['tooltip']['skill_ultimate_name']
- tooltip = (
- f"{unlock['name']}
"
- f"Space Skill
{unlock['desc']}
")
- if enhancements == 1:
- tooltip += (
- f"{unlock['options'][unlock_choice]['name']}
"
- f"{unlock['options'][unlock_choice]['desc']}
")
- elif enhancements == 2:
- e1 = (unlock_choice - 1) % 3
- e2 = (unlock_choice + 1) % 3
- tooltip += (
- f"{unlock['options'][e1]['name']}
"
- f"{unlock['options'][e1]['desc']}
"
- f"{unlock['options'][e2]['name']}
"
- f"{unlock['options'][e2]['desc']}
")
- elif enhancements != 0:
- for i in range(3):
- tooltip += (
- f"{unlock['options'][i]['name']}
"
- f"{unlock['options'][i]['desc']}
")
- return tooltip
-
-# --------------------------------------------------------------------------------------------------
-# static functions
-# --------------------------------------------------------------------------------------------------
-
-
-def create_equipment_tooltip(
- item: dict, head_style: str, subhead_style: str, who_style: str, tags) -> str:
- """
- Creates tooltip for equipment from raw item data.
-
- Parameters:
- - :param item: item data (from cargo table)
- - :param head_style: css style for head
- - :param subhead_style: css style for subhead
- - :param who_style: css style for ship/career/... restriction information
- - :param tags: css styles for the wikitext parser
- """
- tooltip = ''
- if item['who'] is not None:
- tooltip += f"{item['who']}
"
- for i in range(1, 10, 1):
- if item[f'head{i}'] is not None:
- tooltip += f"{format_wikitext(dewikify(item[f'head{i}']))}
"
- if item[f'subhead{i}'] is not None:
- tooltip += (
- f""
- f"{format_wikitext(dewikify(item[f'subhead{i}']))}
")
- if item[f'text{i}'] is not None:
- tooltip += (
- f""
- f"{parse_wikitext(dewikify(item[f'text{i}']), tags)}
")
- return tooltip
-
-
def create_equipment_tooltip__new(item: dict, tooltip_style: TooltipCSS) -> str:
"""
Creates tooltip for equipment from raw item data.
@@ -273,41 +132,6 @@ def create_equipment_tooltip__new(item: dict, tooltip_style: TooltipCSS) -> str:
return tooltip
-def create_trait_tooltip(
- name: str, description: str, type_: str, environment: str, head_style: str,
- subhead_style: str, tags) -> str:
- """
- Creates tooltip for trait from trait description.
-
- Parameters:
- - :param name: name of the trait
- - :param description: description of the trait
- - :param type_: type of the trait; one of "traits", "rep_traits", "active_rep_traits"
- - :param environment: "space" / "ground"
- - :param head_style: css style for head
- - :param subhead_style: css style for subhead
- - :param tags: css styles for the wikitext parser
- """
- if type_ == 'traits':
- tooltip = (
- f"{name}
"
- f"Personal {environment.capitalize()} Trait
"
- f"{parse_wikitext(dewikify(description), tags)}
")
- elif type_ == 'rep_traits':
- tooltip = (
- f"{name}
"
- f"{environment.capitalize()} Reputation Trait
"
- f"{parse_wikitext(dewikify(description), tags)}
")
- elif type_ == 'active_rep_traits':
- tooltip = (
- f"{name}
"
- f"Active {environment.capitalize()} Reputation Trait
"
- f"{parse_wikitext(dewikify(description), tags)}
")
- else:
- tooltip = ''
- return tooltip
-
-
def create_trait_tooltip__new(
name: str, description: str, type_: str, environment: str,
styles: TooltipCSS) -> str:
diff --git a/src/widgets.py b/src/widgets.py
index b2a52af..284d188 100644
--- a/src/widgets.py
+++ b/src/widgets.py
@@ -2,14 +2,14 @@
from pathlib import Path
from typing import Callable
-from PySide6.QtCore import QEvent, QObject, QPoint, QRect, QSize, Qt, QThread, Signal, Slot
+from PySide6.QtCore import QEvent, QPoint, QRect, QSize, Qt, QThread, Signal, Slot
from PySide6.QtGui import (
QBrush, QColor, QCursor, QEnterEvent, QImage, QMouseEvent, QPainter, QPaintEvent, QPen)
from PySide6.QtWidgets import (
- QCheckBox, QComboBox, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPlainTextEdit,
- QSizePolicy, QTabWidget, QVBoxLayout, QWidget)
+ QComboBox, QFrame, QGridLayout, QHBoxLayout, QLabel, QSizePolicy, QTabWidget, QVBoxLayout,
+ QWidget)
-from .constants import AHCENTER, ATOP, EQUIPMENT_TYPES, SMINMIN
+from .constants import AHCENTER, ATOP, SMINMIN
CHAR_TAB_MAP = {
0: 0,
@@ -45,218 +45,6 @@ def switch(self, index):
self.character_tabber.setCurrentIndex(CHAR_TAB_MAP[index])
-class WidgetStorage():
- """
- Stores Widgets
- """
- def __init__(self):
- self.splash_tabber: QTabWidget
- self.loading_label: QLabel
-
- self.build_tabber: QTabWidget
- self.build_frames: list[QFrame] = list()
- self.sidebar: QFrame
- self.sidebar_tabber: QTabWidget
- self.sidebar_frames: list[QFrame] = list()
- self.ship: dict = {
- 'image': ShipImage,
- 'button': ShipButton,
- 'tier': QComboBox,
- 'dc': TooltipLabel,
- 'name': QLineEdit,
- 'desc': QPlainTextEdit
- }
- self.character_tabber: QTabWidget
- self.character_frames: list[QFrame] = list()
-
- self.character: dict = {
- 'name': QLineEdit,
- 'elite': QCheckBox,
- 'career': QComboBox,
- 'faction': QComboBox,
- 'species': QComboBox,
- 'primary': QComboBox,
- 'secondary': QComboBox,
- }
- self.ground_desc: QPlainTextEdit
-
- self.skill_bonus_bars = {
- 'eng': [None] * 24,
- 'sci': [None] * 24,
- 'tac': [None] * 24,
- 'ground': [None] * 10,
- }
- self.skill_count_ground: QLabel
- self.skill_counts_space: dict = {
- 'eng': None,
- 'sci': None,
- 'tac': None
- }
-
- self.build: dict = {
- 'space': {
- 'active_rep_traits': [None] * 5,
- 'aft_weapons': [None] * 5,
- 'aft_weapons_label': None,
- 'boffs': [[None] * 4, [None] * 4, [None] * 4, [None] * 4, [None] * 4, [None] * 4],
- 'boff_labels': [None] * 6,
- 'boff_label_icons': [None] * 6,
- # 'boff_specs': [None] * 6,
- 'core': [''],
- 'deflector': [''],
- 'devices': [None] * 6,
- 'doffs_spec': [''] * 6,
- 'doffs_variant': [''] * 6,
- 'eng_consoles': [None] * 5,
- 'eng_consoles_label': None,
- 'engines': [''],
- 'experimental': [None],
- 'experimental_label': None,
- 'fore_weapons': [None] * 5,
- 'hangars': [None] * 2,
- 'hangars_label': None,
- 'rep_traits': [None] * 5,
- 'sci_consoles': [None] * 5,
- 'sci_consoles_label': None,
- 'sec_def': [None],
- 'sec_def_label': None,
- 'shield': [''],
- # 'ship': '',
- # 'ship_desc': '',
- # 'ship_name': '',
- 'starship_traits': [None] * 7,
- 'tac_consoles': [None] * 5,
- 'tac_consoles_label': None,
- # 'tier': '',
- 'traits': [None] * 12,
- 'uni_consoles': [None] * 3,
- 'uni_consoles_label': None
- },
- 'ground': {
- 'active_rep_traits': [None] * 5,
- 'armor': [''],
- 'boffs': [[''] * 4, [''] * 4, [''] * 4, [''] * 4],
- 'boff_profs': [''] * 4,
- 'boff_specs': [''] * 4,
- 'ground_devices': [None] * 5,
- 'doffs_spec': [''] * 6,
- 'doffs_variant': [''] * 6,
- 'ev_suit': [''],
- 'kit': [''],
- 'kit_modules': [None] * 6,
- 'rep_traits': [None] * 5,
- 'personal_shield': [''],
- 'traits': [None] * 12,
- 'weapons': [''] * 2,
- },
- 'space_skills': {
- 'eng': [None] * 30,
- 'sci': [None] * 30,
- 'tac': [None] * 30
- },
- 'ground_skills': [
- [False] * 6,
- [False] * 6,
- [False] * 4,
- [False] * 4,
- ],
- 'skill_unlocks': {
- 'eng': [None] * 5,
- 'sci': [None] * 5,
- 'tac': [None] * 5,
- 'ground': [None] * 5
- },
- 'skill_desc': {
- 'space': None,
- 'ground': None
- }
- }
-
-
-class Cache():
- """
- Stores data
- """
- def __init__(self):
- self.reset_cache()
-
- def reset_cache(self, keep_static_data: bool = False):
- self.ships: dict = dict()
- self.equipment: dict = {type_: dict() for type_ in set(EQUIPMENT_TYPES.values())}
- self.starship_traits: dict = dict()
- self.traits: dict = {
- 'space': {
- 'traits': dict(),
- 'rep_traits': dict(),
- 'active_rep_traits': dict()
- },
- 'ground': {
- 'traits': dict(),
- 'rep_traits': dict(),
- 'active_rep_traits': dict()
- }
- }
- self.ground_doffs: dict = dict()
- self.space_doffs: dict = dict()
- self.boff_abilities: dict = {
- 'space': self.boff_dict(),
- 'ground': self.boff_dict(),
- 'all': dict()
- }
-
- if not keep_static_data:
- self.item_aliases: dict = dict()
- self.skills = {
- 'space': dict(),
- 'space_unlocks': dict(),
- 'ground': dict(),
- 'ground_unlocks': dict(),
- 'space_points_total': 0,
- 'space_points_eng': 0,
- 'space_points_sci': 0,
- 'space_points_tac': 0,
- 'space_points_rank': [0] * 5,
- 'ground_points_total': 0,
- }
-
- self.modifiers: dict = {type_: dict() for type_ in set(EQUIPMENT_TYPES.values())}
-
- self.empty_image: QImage
- self.overlays: OverlayCache = OverlayCache()
- self.icons: dict = dict()
- self.images: dict = dict()
- self.alt_images: dict = dict()
- self.images_set: set = set()
- self.images_populated: bool = False
- self.images_failed: dict = dict()
-
- def boff_dict(self):
- return {
- 'Tactical': [list(), list(), list(), list()],
- 'Engineering': [list(), list(), list(), list()],
- 'Science': [list(), list(), list(), list()],
- 'Intelligence': [list(), list(), list(), list()],
- 'Command': [list(), list(), list(), list()],
- 'Pilot': [list(), list(), list(), list()],
- 'Temporal': [list(), list(), list(), list()],
- 'Miracle Worker': [list(), list(), list(), list()],
- }
-
- def __getitem__(self, key: str):
- return getattr(self, key)
-
-
-class OverlayCache():
- def __init__(self):
- self.common: QImage
- self.uncommon: QImage
- self.rare: QImage
- self.veryrare: QImage
- self.ultrarare: QImage
- self.epic: QImage
- self.check: QImage
-
-
class ImageLabel(QWidget):
"""
Label displaying image that resizes according to its parents width while preserving aspect
@@ -534,79 +322,6 @@ def run(self):
self.done.emit()
-class PySideThread(QThread):
- def __init__(self, parent, finished_func, worker):
- self.finished_func = finished_func
- self.worker = worker
- super().__init__(parent)
-
- def worker_finished(self):
- if self.finished_func is not None:
- self.finished_func()
- self.quit()
-
-
-class ThreadObject(QObject):
-
- start = Signal(tuple)
- result = Signal(object)
- update_splash = Signal(str)
- finished = Signal()
-
- def __init__(self, func, *args, **kwargs) -> None:
- self._func = func
- self._args = args
- self._kwargs = kwargs
- super().__init__()
-
- @Slot()
- def run(self, start_args=tuple()):
- self._func(*self._args, *start_args, threaded_worker=self, **self._kwargs)
- self.finished.emit()
-
-
-def exec_in_thread(
- self, func, *args, result=None, update_splash=None, finished=None, start_later=False,
- **kwargs):
- """
- Executes function `func` in separate thread. All positional and keyword parameters not listed
- are passed to the function. The function must take a parameter `threaded_worker` which will
- contain the worker object holding the signals: `start` (tuple), `result` (object),
- `update_splash` (str), `finished` (no data)
-
- Parameters:
- - :param func: function to execute
- - :param *args: positional parameters passed to the function [optional]
- - :param result: callable that is executed when signal result is emitted (takes object)
- [optional]
- - :param update_splash: callable that is executed when signal update_splash is emitted
- (takes str) [optional]
- - :param finished: callable that is executed after `func` returns (takes no parameters)
- [optional]
- - :param start_later: set to True to defer execution of the function; makes this function
- return signal that can be emitted to start execution. That signal takes a tuple with additional
- positional parameters passed to `func` [optional]
- - :param **kwargs: keyword parameters passed to the function [optional]
- """
- worker = ThreadObject(func, *args, **kwargs)
- thread = PySideThread(self.app, finished, worker)
- if result is not None:
- worker.result.connect(result)
- if update_splash is not None:
- worker.update_splash.connect(update_splash)
- worker.moveToThread(thread)
- if start_later:
- worker.start.connect(worker.run)
- else:
- thread.started.connect(worker.run)
- worker.finished.connect(thread.worker_finished)
- thread.finished.connect(worker.deleteLater)
- thread.finished.connect(thread.deleteLater)
- thread.start(QThread.Priority.LowestPriority)
- if start_later:
- return worker.start
-
-
class ShipButton(QLabel):
"""
Button with word wrap
@@ -628,8 +343,6 @@ def mousePressEvent(self, ev: QMouseEvent):
super().mousePressEvent(ev)
-TagStyles = namedtuple('TagStyles', ('ul', 'li', 'indent'))
-
ItemSlot = namedtuple('ItemSlot', ('environment', 'type', 'index', 'boff_id', 'is_equipment'))
From 16648fc9b294c91986bd017a8aa462f59b9501fd Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 08:13:34 +0200
Subject: [PATCH 23/44] updating dependencies
---
pyproject.toml | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 8bce299..bb34f20 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
name = "SETS"
description = "A Star Trek Online build tool in Python"
readme = "README.md"
-requires-python = ">=3.11"
+requires-python = ">=3.14"
license = {file = "LICENSE"}
classifiers = [
"Programming Language :: Python :: 3",
@@ -16,11 +16,9 @@ classifiers = [
"Development Status :: 4 - Beta"
]
dependencies = [
- "PySide6",
- "requests",
- "numpy",
- "requests_html",
- "lxml_html_clean"
+ "PySide6==6.11.0",
+ "requests==2.34.0",
+ "numpy==2.4.4"
]
dynamic = ["version"]
From 73878c03abb6897e37f4c25eb7d526c454647bb6 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 09:57:56 +0200
Subject: [PATCH 24/44] fixing various minor issues
---
src/app.py | 64 +++++++++++++++-------------
src/buildmanager.py | 45 ++++++++++----------
src/cargomanager.py | 99 ++++++++++++++++++++++++++------------------
src/imagemanager.py | 2 +-
src/iofunc.py | 2 +-
src/theme.py | 7 +++-
src/widgetbuilder.py | 3 ++
src/widgets.py | 1 +
8 files changed, 125 insertions(+), 98 deletions(-)
diff --git a/src/app.py b/src/app.py
index 46dc13e..08eaca3 100644
--- a/src/app.py
+++ b/src/app.py
@@ -28,7 +28,7 @@
create_annotated_slider2, create_button2, create_button_series2, create_checkbox2,
create_combo_box2, create_entry2, create_frame2, create_item_button2, create_label2)
from .widgets import (
- DoffCombobox, GridLayout, HBoxLayout, ImageLabel, ItemButton, ShipButton, ShipImage,
+ DoffCombobox, GridLayout, HBoxLayout, ImageLabel, ItemButton, ItemSlot, ShipButton, ShipImage,
Tabbers, Thread, TooltipLabel, VBoxLayout)
# only for developing; allows to terminate the qt event loop with keyboard interrupt
@@ -76,7 +76,6 @@ def __init__(self, args, app_dir_path: str, version: str):
self.app, self.window = self.create_main_window()
self.cache_icons()
self.cargo.load_static_data()
- self.setup_main_layout()
self.build_loader: BuildLoader = BuildLoader(
self.build2, self.cargo, self.config, self.settings, self.window)
self.export_window = ExportWindow(self.theme2, self.window, self.build2, self.cargo)
@@ -88,6 +87,7 @@ def __init__(self, args, app_dir_path: str, version: str):
self.ship_selector_window.dialog_result.connect(self.build2.finish_ship_pick)
self.context_menu: ContextMenu = ContextMenu(self.theme2, self.build2, self.cargo)
self.context_menu.edit_slot.connect(self.edit_window.edit_item)
+ self.setup_main_layout()
self.window.show()
self._backend_thread: Thread = Thread(self.init_backend)
self._backend_thread.done.connect(self.complete_app_init)
@@ -194,6 +194,7 @@ def complete_app_init(self):
self.splash.set_loading_text('Loading Images...')
self._backend_thread = Thread(self.images.load_images)
self._backend_thread.start()
+ self.splash.show_splash(False)
def cache_icons(self):
"""
@@ -209,8 +210,8 @@ def cache_icons(self):
self.theme2.icons['ground'] = load_icon('ground_icon.png', self.app_dir2, icon_size)
icon_size = (self.theme2.opt.box_width, self.theme2.opt.box_width)
self.theme2.icons['tac'] = load_icon('tac_icon.png', self.app_dir2, icon_size)
- self.theme2.icons['sci'] = load_icon('sci_icon.png', icon_size)
- self.theme2.icons['eng'] = load_icon('eng_icon.png', icon_size)
+ self.theme2.icons['sci'] = load_icon('sci_icon.png', self.app_dir2, icon_size)
+ self.theme2.icons['eng'] = load_icon('eng_icon.png', self.app_dir2, icon_size)
self.theme2.icons['tac-small'] = load_icon('tac-small.svg', self.app_dir2, size=(25, 25))
self.theme2.icons['sci-small'] = load_icon('sci-small.svg', self.app_dir2, size=(25, 25))
icon_size = (self.theme2.opt.box_height, self.theme2.opt.box_width * 182 / 106)
@@ -238,8 +239,9 @@ def create_main_window(self, argv=[]) -> tuple[QApplication, QWidget]:
"""
app = QApplication(argv)
font_database = QFontDatabase()
- font_database.addApplicationFont(self.app_dir2 / 'local' / 'Overpass-VariableFont_wght.ttf')
- font_database.addApplicationFont(self.app_dir2 / 'local' / 'RobotoMono-Regular.ttf')
+ font_database.addApplicationFont(
+ str(self.app_dir2 / 'local' / 'Overpass-VariableFont_wght.ttf'))
+ font_database.addApplicationFont(str(self.app_dir2 / 'local' / 'RobotoMono-Regular.ttf'))
app.setStyleSheet(self.theme2.create_style_sheet(self.theme2['app']['style']))
window = QWidget()
window.setWindowIcon(load_icon('SETS_icon_small.png', self.app_dir2))
@@ -315,12 +317,14 @@ def picker(
pos = button.mapToGlobal(QPoint(0, 0))
else:
pos = None
- self.picker_window.pick_item(items, pos, equipment, modifiers, image_suffix)
+ slot = ItemSlot(environment, build_key, build_subkey, boff_id, equipment)
+ self.picker_window.pick_item(items, pos, slot, modifiers, image_suffix)
def setup_main_layout(self):
"""
Creates the main layout and places it into the main window.
"""
+ self.build2._building = True
# master layout: banner, borders and splash screen
layout = VBoxLayout()
background_frame = create_frame2(
@@ -448,12 +452,13 @@ def setup_main_layout(self):
self.setup_settings_frame()
content_frame.setLayout(content_layout)
+ self.build2._building = False
def setup_ship_frame(self):
"""
Creates ship info frame
"""
- frame = self.widgets.sidebar_frames[0]
+ frame = self.tabbers.sidebar_frames[0]
csp = self.theme2['defaults']['csp'] * self.theme2.scale
layout = VBoxLayout(margins=csp, spacing=csp)
@@ -493,7 +498,7 @@ def setup_ship_frame(self):
dc_label.setSizePolicy(dc_label_size_policy)
self.build2.ship.dc = dc_label
ship_layout.addWidget(dc_label, 1, 2, alignment=ARIGHT)
- info_button = create_button2(self.theme, 'Ship Info', style_override={'margin': 0})
+ info_button = create_button2(self.theme2, 'Ship Info', style_override={'margin': 0})
info_button.clicked.connect(self.build2.ship_info_callback)
ship_layout.addWidget(info_button, 1, 3, alignment=ARIGHT)
name_label = create_label2(self.theme2, 'Ship Name:')
@@ -581,7 +586,7 @@ def create_boff_station_space(
label_layout.addWidget(icon_label, alignment=ALEFT)
icon_label.hide()
label = create_combo_box2(
- self.theme2, size_policy=SMAXMAX, style_override=self.theme['boff_combo'])
+ self.theme2, size_policy=SMAXMAX, style_override=self.theme2['boff_combo'])
label.currentTextChanged.connect(
lambda new: self.build2.boff_profession_callback_space(boff_id, new))
label.addItems(label_options)
@@ -614,17 +619,17 @@ def create_boff_station_ground(self, boff_id: int) -> VBoxLayout:
layout = VBoxLayout(spacing=m)
label_layout = HBoxLayout(spacing=m)
label_layout.setAlignment(ALEFT)
- prof_label = create_combo_box2(self.theme2, style_override=self.theme['boff_combo'])
+ prof_label = create_combo_box2(self.theme2, style_override=self.theme2['boff_combo'])
prof_label.currentTextChanged.connect(
lambda new: self.build2.boff_label_callback_ground(boff_id, 'boff_profs', new))
prof_label.addItems(CAREERS)
widget_storage.boff_profs[boff_id] = prof_label
label_layout.addWidget(prof_label)
- spec_label = create_combo_box2(self.theme2, style_override=self.theme['boff_combo'])
+ spec_label = create_combo_box2(self.theme2, style_override=self.theme2['boff_combo'])
spec_label.currentTextChanged.connect(
lambda new: self.build2.boff_label_callback_ground(boff_id, 'boff_specs', new))
spec_label.addItems(GROUND_BOFF_SPECS)
- widget_storage['boff_specs'][boff_id] = spec_label
+ widget_storage.boff_specs[boff_id] = spec_label
label_layout.addWidget(spec_label)
layout.addLayout(label_layout)
button_layout = HBoxLayout(spacing=m)
@@ -655,7 +660,7 @@ def create_personal_trait_section(self, environment: str) -> GridLayout:
for row in range(3):
for col in range(4):
i = row * 4 + col
- button = create_item_button2(self)
+ button = create_item_button2(self.theme2)
button.clicked.connect(
lambda subkey=i, bt=button: self.picker(environment, 'traits', subkey, bt))
button.rightclicked.connect(lambda event, subkey=i: self.context_menu.invoke(
@@ -708,13 +713,13 @@ def create_doff_section(self, environment: str) -> GridLayout:
doff_layout.setColumnStretch(1, 1)
widget_storage = self.build2.space if environment == 'space' else self.build2.ground
for i in range(6):
- spec_combo = create_combo_box2(self.theme2, style_override=self.theme['doff_combo'])
+ spec_combo = create_combo_box2(self.theme2, style_override=self.theme2['doff_combo'])
spec_combo.currentTextChanged.connect(
lambda spec, id=i: self.build2.doff_spec_callback(spec, environment, id))
doff_layout.addWidget(spec_combo, i, 0)
widget_storage.doffs_spec[i] = spec_combo
variant_combo = create_combo_box2(
- self.theme2, style_override=self.theme['doff_combo'], class_=DoffCombobox)
+ self.theme2, style_override=self.theme2['doff_combo'], class_=DoffCombobox)
variant_combo.currentTextChanged.connect(
lambda variant, id=i: self.build2.doff_variant_callback(variant, environment, id))
doff_layout.addWidget(variant_combo, i, 1)
@@ -727,9 +732,9 @@ def create_skill_group_space(self, group_data: dict, id_offset: int) -> GridLayo
Parameters:
- :param group_data: skill group data
- - :param id_offset: index of the first skill node in self.widgets and self.build
+ - :param id_offset: index of the first skill node in self.build
"""
- layout = GridLayout(spacing=self.theme['defaults']['csp'] * self.config.ui_scale)
+ layout = GridLayout(spacing=self.theme2['defaults']['csp'] * self.config.ui_scale)
# one skill with 3 ranks
if group_data['grouping'] == 'column':
for index, node in enumerate(group_data['nodes']):
@@ -819,7 +824,7 @@ def create_bonus_bar_space(self, career: str, layout: GridLayout, column: int):
layout.addWidget(segment, row, column, alignment=AHCENTER)
segment_index += 1
button = create_item_button2(self.theme2)
- button.clicked.connect(lambda: self.build2.skill_unlock_callback(self, career, 4))
+ button.clicked.connect(lambda: self.build2.skill_unlock_callback(career, 4))
layout.addWidget(button, 1, column, alignment=AHCENTER)
self.build2.skills.unlocks[career][4] = button
@@ -829,7 +834,7 @@ def create_skill_button_ground(self, group_data: dict, id: int, node_id: int) ->
Parameters:
- :param group_data: skill group data
- - :param id: index of the skill node in self.widgets and self.build
+ - :param id: index of the skill node in self.build
- :param node_id: 0 or 1 for first or second node
"""
button = create_item_button2(self.theme2)
@@ -963,7 +968,7 @@ def setup_ground_build_frame(self):
"""
Creates Ground build frame
"""
- frame = self.widgets.build_frames[1]
+ frame = self.tabbers.build_frames[1]
isp = self.theme2['defaults']['isp'] * 2 * self.theme2.scale
layout = GridLayout(margins=isp, spacing=isp)
layout.setColumnStretch(0, 1)
@@ -1227,10 +1232,10 @@ def setup_space_skill_frame(self):
self.build2.skills.space_desc = desc_edit
sidebar_layout.addWidget(desc_edit, 1, 0, 1, 2)
load_skills_button = create_button2(self.theme2, 'Load Skills')
- load_skills_button.clicked.connect(self.load_skills_callback)
+ load_skills_button.clicked.connect(self.build_loader.load_skills_callback)
sidebar_layout.addWidget(load_skills_button, 2, 0, alignment=AHCENTER)
save_skills_button = create_button2(self.theme2, 'Save Skills')
- save_skills_button.clicked.connect(self.save_skills_callback)
+ save_skills_button.clicked.connect(self.build_loader.save_skills_callback)
sidebar_layout.addWidget(save_skills_button, 2, 1, alignment=AHCENTER)
sidebar_frame.setLayout(sidebar_layout)
@@ -1314,10 +1319,11 @@ def setup_ground_skill_frame(self):
self.build2.skills.unlocks['ground'][i] = button
row -= 3
icon_label = create_label2(self.theme2, '', style='unlock_label')
- icon_label.setPixmap(self.cache.icons['ground'])
+ icon_label.setPixmap(self.theme2.icons['ground'])
bonus_bar_layout.addWidget(icon_label, 16, 1, alignment=AHCENTER)
- self.build2.skills.count_labels['ground'] = create_label2(self.theme2, '0', 'label_subhead')
- bonus_bar_layout.addWidget(self.widgets.skill_count_ground, 17, 1, alignment=AHCENTER)
+ count_label = create_label2(self.theme2, '0', 'label_subhead')
+ bonus_bar_layout.addWidget(count_label, 17, 1, alignment=AHCENTER)
+ self.build2.skills.count_labels['ground'] = count_label
bonus_bar_container.setLayout(bonus_bar_layout)
col_layout.addWidget(bonus_bar_container, 0, 2)
frame.setLayout(col_layout)
@@ -1333,13 +1339,13 @@ def setup_ground_skill_frame(self):
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
desc_edit.textChanged.connect(lambda: self.build2.set(
'ground', 'skill_desc', value=desc_edit.toPlainText(), autosave=False))
- self.widgets.build['skill_desc']['ground'] = desc_edit
+ self.build2.skills.ground_desc = desc_edit
sidebar_layout.addWidget(desc_edit, 1, 0, 1, 2)
load_skills_button = create_button2(self.theme2, 'Load Skills')
- load_skills_button.clicked.connect(self.load_skills_callback)
+ load_skills_button.clicked.connect(self.build_loader.load_skills_callback)
sidebar_layout.addWidget(load_skills_button, 2, 0, alignment=AHCENTER)
save_skills_button = create_button2(self.theme2, 'Save Skills')
- save_skills_button.clicked.connect(self.save_skills_callback)
+ save_skills_button.clicked.connect(self.build_loader.save_skills_callback)
sidebar_layout.addWidget(save_skills_button, 2, 1, alignment=AHCENTER)
sidebar_frame.setLayout(sidebar_layout)
diff --git a/src/buildmanager.py b/src/buildmanager.py
index 8221d55..c0744ba 100644
--- a/src/buildmanager.py
+++ b/src/buildmanager.py
@@ -35,7 +35,7 @@ def __init__(self):
self.engines: list[ItemButton] = [None]
self.experimental: list[ItemButton] = [None]
self.experimental_label: QLabel = [None]
- self.fore_weapons: list[ItemButton] = [None]
+ self.fore_weapons: list[ItemButton] = [None] * 5
self.hangars: list[ItemButton] = [None] * 2
self.hangars_label: QLabel = None
self.rep_traits: list[ItemButton] = [None] * 5
@@ -44,7 +44,7 @@ def __init__(self):
self.sec_def: list[ItemButton] = [None]
self.sec_def_label: QLabel = [None]
self.shield: list[ItemButton] = [None]
- self.starship_traits: list[ItemButton] = [None]
+ self.starship_traits: list[ItemButton] = [None] * 7
self.tac_consoles: list[ItemButton] = [None] * 5
self.tac_consoles_label: QLabel = None
self.traits: list[ItemButton] = [None] * 12
@@ -249,10 +249,7 @@ def load_build(self):
self.character.secondary.setCurrentText(self._build_data['captain']['secondary_spec'])
# Space Build Section
- if ship == '' or ship == '':
- self.align_space_frame(ship_data, clear=True)
- else:
- self.align_space_frame(ship_data)
+ self.align_space_frame(ship_data)
self.load_equipment_cat('fore_weapons', 'space')
self.load_equipment_cat('aft_weapons', 'space')
self.load_equipment_cat('experimental', 'space')
@@ -350,7 +347,7 @@ def align_space_frame(self, ship_data: dict, clear: bool = False):
self.update_starship_traits(starship_traits, clear)
- boff_specs = map(lambda s: get_boff_spec(self, s), ship_data['boffs'])
+ boff_specs = map(lambda s: get_boff_spec(s), ship_data['boffs'])
if 'Science Destroyer' in ship_data['type']:
for boff_num, boff_details in enumerate(sorted(boff_specs, reverse=True)):
if (boff_details[0] == 3 and boff_details[1] == 'Tactical'
@@ -567,7 +564,7 @@ def update_equipment_cat(
only
Parameters:
- - :param build_key: key to self.build and self.widgets
+ - :param build_key: key to self._build_data
- :param target_quantity: number of slots that should be available in this category
- :param clear: True to clear build
- :param can_hide: hides/shows category label when target_quantity is 0/None
@@ -591,7 +588,7 @@ def update_equipment_cat(
def update_starship_traits(self, target_quantity: int, clear: bool = False):
"""
- Shows/hides appropriate amount of starship trait buttons; updates `self.build`
+ Shows/hides appropriate amount of starship trait buttons; updates `self._build_data`
Parameters:
- :param target_quantity: number of slots that should be available in this category
@@ -691,7 +688,7 @@ def clear_equipment_cat_ground(self, build_key: str):
Clears buttons and build; ground build only
Parameters:
- - :param build_key: key to self.build and self.widgets
+ - :param build_key: key to self._build_data
"""
category: list[ItemButton] = getattr(self.ground, build_key)
for subkey, button in enumerate(category):
@@ -706,7 +703,7 @@ def load_trait_cat(self, build_key: str, environment: str):
- :param build_key: trait category
- :param environment: space/ground
"""
- for subkey, item in enumerate(self.build[environment][build_key]):
+ for subkey, item in enumerate(self._build_data[environment][build_key]):
if item is not None and item != '':
self.slot_trait_item(item, environment, build_key, subkey)
else:
@@ -720,7 +717,7 @@ def slot_equipment_item(
Parameters:
- :param item: item to be slotted
- :param environment: space/ground
- - :param build_key: key to self.build[environment]
+ - :param build_key: key to self._build_data[environment]
- :param build_subkey: index of the item within its build_key (category)
"""
self._build_data[environment][build_key][build_subkey] = item
@@ -738,7 +735,7 @@ def slot_trait_item(
Parameters:
- :param item: item to be slotted
- :param environment: space/ground
- - :param build_key: key to self.build[environment]
+ - :param build_key: key to self._build_data[environment]
- :param build_subkey: index of the item within its build_key (category)
"""
item_name = item['item']
@@ -764,7 +761,7 @@ def unslot_item(self, environment: str, build_key: str, build_subkey: int, boff_
Parameters:
- :param item: item to be slotted
- :param environment: space/ground
- - :param build_key: key to self.build[environment]
+ - :param build_key: key to self._build_data[environment]
- :param build_subkey: index of the item within its build_key (category)
- :param boff_id: id of the boff seat; assumes non-boff item when `-1` or not supplied
"""
@@ -796,11 +793,11 @@ def handle_picker_result(self, new_item: dict[str, str | list[str]], slot: ItemS
for i, mod in enumerate(new_item['modifiers']):
if mod not in self._cache.modifiers[type_]:
new_item['modifiers'][i] = ''
- self.slot_equipment_item(self, new_item, slot.environment, slot.type, slot.index)
+ self.slot_equipment_item(new_item, slot.environment, slot.type, slot.index)
else:
if slot.boff_id is None:
self.slot_trait_item(
- self, {'item': new_item['item']}, slot.environment, slot.type, slot.index)
+ {'item': new_item['item']}, slot.environment, slot.type, slot.index)
elif slot.type == 'boffs':
ability_name, _, ability_rank = new_item['item'].rpartition(' ')
self._build_data[slot.environment]['boffs'][slot.boff_id][slot.index] = {
@@ -911,7 +908,7 @@ def clear_boff_seat_ground(self, boff_id: int):
def load_doffs(self, environment: str):
"""
- Updates UI to show doffs in self.build
+ Updates UI to show doffs in self._build_data
Parameters:
- :param environment: "space" / "ground"
@@ -933,7 +930,7 @@ def load_doffs(self, environment: str):
def load_skill_pages(self):
"""
- Updates UI to show skill trees in self.build
+ Updates UI to show skill trees in self._build_data
"""
self.skills.space_desc.setPlainText(self._build_data['skill_desc']['space'])
self._skill_state['space_points_eng'] = 0
@@ -957,7 +954,7 @@ def load_skill_pages(self):
skill_points = self._skill_state[f'space_points_{career}']
self.skills.count_labels[career].setText(str(skill_points))
for unlock_id, unlock_choice in enumerate(self._build_data['skill_unlocks'][career]):
- self.set_skill_unlock_space(self, career, unlock_id, unlock_choice, skill_points)
+ self.set_skill_unlock_space(career, unlock_id, unlock_choice, skill_points)
if skill_points > 24:
skill_points = 24
for i in range(skill_points):
@@ -1314,9 +1311,9 @@ def toggle_space_skill(self, current_state: bool, career: str, skill_id: int):
self.skills.space[career][skill_id].clear_overlay()
self.skills.space[career][skill_id].highlight = False
self._build_data['space_skills'][career][skill_id] = False
- self._cache.skills['space_points_total'] -= 1
- self._cache.skills[f'space_points_{career}'] -= 1
- self._cache.skills['space_points_rank'][int(skill_id / 6)] -= 1
+ self._skill_state['space_points_total'] -= 1
+ self._skill_state[f'space_points_{career}'] -= 1
+ self._skill_state['space_points_rank'][int(skill_id / 6)] -= 1
segment_index: int = self._skill_state[f'space_points_{career}']
if segment_index < 24:
self.skills.bonus_bars[career][segment_index].setChecked(False)
@@ -1397,7 +1394,7 @@ def toggle_ground_skill(self, current_state: bool, skill_group: int, skill_id: i
self.skills.bonus_bars['ground'][segment_index].setChecked(False)
if segment_index % 2 == 1:
button_index = (segment_index - 1) // 2
- self.set_skill_unlock_ground(self, button_index, None)
+ self.set_skill_unlock_ground(button_index, None)
else:
self.skills.ground[skill_group][skill_id].set_overlay(self._images.overlays.check)
self.skills.ground[skill_group][skill_id].highlight = True
@@ -1417,7 +1414,7 @@ def skill_callback_space(self, career: str, skill_id: int, grouping: str):
Parameters:
- :param career: "eng" / "tac" / "sci"
- - :param skill_id: id of the skill node (index in self.build and self.widgets.build)
+ - :param skill_id: id of the skill node (index in self._build_data)
- :param grouping: type of skill grouping: "column" / "pair+1" / "separate"
"""
space_skills = self._build_data['space_skills']
diff --git a/src/cargomanager.py b/src/cargomanager.py
index 0c98fde..f8b8c3a 100644
--- a/src/cargomanager.py
+++ b/src/cargomanager.py
@@ -47,7 +47,7 @@ def __init__(
}
self.ground_doffs: dict[str, dict[str, dict[str]]] = dict()
self.space_doffs: dict[str, dict[str, dict[str]]] = dict()
- self.boff_abilities: dict[str, dict[str, dict[str, list[str]] | dict[str, str]]] = {
+ self.boff_abilities: dict[str, dict[str, list[list[str]] | dict[str, str]]] = {
'space': self.boff_dict(),
'ground': self.boff_dict(),
'all': dict()
@@ -81,42 +81,58 @@ def provision_cargo_data(self):
(Down-) loads cargo data or gets cached cargo data.
"""
images_updated = False
+ force_image_update = False
+ all_images = self.get_cached_data('images_list.json')
+ if all_images is None:
+ image_set = set()
+ force_image_update = True
+ else:
+ image_set = set(all_images)
+ alt_images = self.get_cached_data('alt_images.json')
+ if alt_images is None:
+ alt_images = set()
+ force_image_update = True
self.ships = self.get_cached_data('ships.json')
if self.ships is None:
self.cache_ship_data()
- images_updated = True
- self.equipment = self.get_cached_data('equipment.json')
- if self.equipment is None:
+ equipment_data = self.get_cached_data('equipment.json')
+ if equipment_data is None or force_image_update:
self.cache_equipment_data()
images_updated = True
- self.space_traits = self.get_cached_data('space_traits.json')
- self.ground_traits = self.get_cached_data('ground_traits.json')
- if self.space_traits is None or self.ground_traits is None:
+ else:
+ self.equipment = equipment_data
+ space_trait_data = self.get_cached_data('space_traits.json')
+ ground_trait_data = self.get_cached_data('ground_traits.json')
+ if space_trait_data is None or ground_trait_data is None or force_image_update:
self.cache_trait_data()
images_updated = True
- self.starship_traits = self.get_cached_data('starship_traits.json')
- if self.starship_traits is None:
+ else:
+ self.space_traits = space_trait_data
+ self.ground_traits = ground_trait_data
+ starship_trait_data = self.get_cached_data('starship_traits.json')
+ if starship_trait_data is None or force_image_update:
self.cache_starship_trait_data()
images_updated = True
- self.boff_abilities = self.get_cached_data('boff_abilities.json')
- if self.boff_abilities is None:
+ else:
+ self.starship_traits = starship_trait_data
+ boff_data = self.get_cached_data('boff_abilities.json')
+ if boff_data is None or force_image_update:
self.cache_boff_data()
images_updated = True
- self.modifiers = self.get_cached_data('modifiers.json')
- if self.modifiers is None:
+ else:
+ self.boff_abilities = boff_data
+ modifier_data = self.get_cached_data('modifiers.json')
+ if modifier_data is None:
self.cache_modifier_data()
- self.space_doffs = self.get_cached_data('space_doffs.json')
- self.ground_doffs = self.get_cached_data('ground_doffs.json')
- if self.space_doffs is None or self.ground_doffs is None:
+ else:
+ self.modifiers = modifier_data
+ space_doff_data = self.get_cached_data('space_doffs.json')
+ ground_doff_data = self.get_cached_data('ground_doffs.json')
+ if space_doff_data is None or ground_doff_data is None:
self.cache_duty_officer_data()
- alt_images = self.get_cached_data('alt_images.json')
- if alt_images is None:
- alt_images = dict()
- all_images = self.get_cached_data('images_list.json')
- if all_images is None:
- image_set = set()
else:
- image_set = set(all_images)
+ self.space_doffs = space_doff_data
+ self.ground_doffs = ground_doff_data
if images_updated:
alt_images.update(self.alt_images)
store_json__new(alt_images, self._folders['cache'] / 'alt_images.json')
@@ -136,9 +152,10 @@ def get_cached_data(self, file_name: str) -> dict | list | None:
- :param file_name: name of the cache file to load
"""
file_path = self._folders['cache'] / file_name
- last_modified = file_path.stat().st_mtime
- if time() - last_modified < SEVEN_DAYS_IN_SECONDS:
- return load_json__new(file_path)
+ if file_path.is_file():
+ last_modified = file_path.stat().st_mtime
+ if time() - last_modified < SEVEN_DAYS_IN_SECONDS:
+ return load_json__new(file_path)
return None
def store_failed_images(self):
@@ -229,8 +246,8 @@ def cache_trait_data(self):
# catch wrong values in trait['environment'] (cargo issue)
except (KeyError, AttributeError):
pass
- store_json__new(self.space_traits, 'space_traits.json')
- store_json__new(self.ground_traits, 'ground_traits.json')
+ store_json__new(self.space_traits, self._folders['cache'] / 'space_traits.json')
+ store_json__new(self.ground_traits, self._folders['cache'] / 'ground_traits.json')
def cache_starship_trait_data(self):
"""
@@ -254,7 +271,7 @@ def cache_starship_trait_data(self):
f"Starship Trait
"
f"{ship_trait['short']}
{parse_wikitext(ship_trait['detailed'], styles)}")
}
- store_json__new(self.starship_traits, 'starship_traits.json')
+ store_json__new(self.starship_traits, self._folders['cache'] / 'starship_traits.json')
def cache_boff_data(self):
"""
@@ -291,7 +308,7 @@ def cache_boff_data(self):
f"{parse_wikitext(dewikify(boff_ability[f'rank{decimal}info']), styles)}")
self.boff_abilities['all'][boff_name] = ability_item
self.image_set |= self.boff_abilities['all'].keys()
- store_json__new(self.boff_abilities, 'boff_abilities.json')
+ store_json__new(self.boff_abilities, self._folders['cache'] / 'boff_abilities.json')
def cache_modifier_data(self):
"""
@@ -323,7 +340,7 @@ def cache_modifier_data(self):
self.modifiers['uni_consoles'].update(self.modifiers['sci_consoles'])
self.modifiers['uni_consoles'].update(self.modifiers['eng_consoles'])
self.modifiers['uni_consoles'].update(self.modifiers['tac_consoles'])
- store_json__new(self.modifiers, 'modifiers.json')
+ store_json__new(self.modifiers, self._folders['cache'] / 'modifiers.json')
def cache_duty_officer_data(self):
"""
@@ -342,8 +359,8 @@ def cache_duty_officer_data(self):
elif doff['shipdutytype'] is not None:
self.cache_doff_single(self.space_doffs, doff)
self.cache_doff_single(self.ground_doffs, doff)
- store_json__new(self.space_doffs, 'space_doffs.json')
- store_json__new(self.ground_doffs, 'ground_doffs.json')
+ store_json__new(self.space_doffs, self._folders['cache'] / 'space_doffs.json')
+ store_json__new(self.ground_doffs, self._folders['cache'] / 'ground_doffs.json')
def cache_doff_single(self, cache: dict, doff: dict):
"""
@@ -419,12 +436,12 @@ def backup_cargo_data(self):
def boff_dict(self):
return {
- 'Tactical': [dict(), dict(), dict(), dict()],
- 'Engineering': [dict(), dict(), dict(), dict()],
- 'Science': [dict(), dict(), dict(), dict()],
- 'Intelligence': [dict(), dict(), dict(), dict()],
- 'Command': [dict(), dict(), dict(), dict()],
- 'Pilot': [dict(), dict(), dict(), dict()],
- 'Temporal': [dict(), dict(), dict(), dict()],
- 'Miracle Worker': [dict(), dict(), dict(), dict()],
+ 'Tactical': [list(), list(), list(), list()],
+ 'Engineering': [list(), list(), list(), list()],
+ 'Science': [list(), list(), list(), list()],
+ 'Intelligence': [list(), list(), list(), list()],
+ 'Command': [list(), list(), list(), list()],
+ 'Pilot': [list(), list(), list(), list()],
+ 'Temporal': [list(), list(), list(), list()],
+ 'Miracle Worker': [list(), list(), list(), list()],
}
diff --git a/src/imagemanager.py b/src/imagemanager.py
index f92ec6a..2fe0009 100644
--- a/src/imagemanager.py
+++ b/src/imagemanager.py
@@ -60,7 +60,7 @@ def get(self, image_name: str) -> QImage:
"""
image = self._images[image_name]
if image.isNull():
- image.load(self._images_dir / get_image_file_name(image_name))
+ image.load(str(self._images_dir / get_image_file_name(image_name)))
return image
def get_alt(self, image_name: str, image_suffix: str = '') -> QImage:
diff --git a/src/iofunc.py b/src/iofunc.py
index b76778a..88ae2d1 100644
--- a/src/iofunc.py
+++ b/src/iofunc.py
@@ -65,7 +65,7 @@ def load_icon(filename: str, app_directory: Path, size: tuple = tuple()) -> QIco
- :param path: path to icon
- :param app_directory: absolute path to the app directory
"""
- icon = QIcon(app_directory / 'local' / filename)
+ icon = QIcon(str(app_directory / 'local' / filename))
if len(size) == 2:
return icon.pixmap(*size)
return icon
diff --git a/src/theme.py b/src/theme.py
index 2e6c34e..35b8e98 100644
--- a/src/theme.py
+++ b/src/theme.py
@@ -549,6 +549,7 @@ def get_default_theme(self) -> dict[str, dict]:
'background-color': '#000000',
'border-style': 'none',
'color': '@fg',
+ 'font': '@font'
# 'margin': 0,
# 'padding': 0,
},
@@ -747,8 +748,10 @@ def get_default_theme(self) -> dict[str, dict]:
'border-top-style': 'solid',
'border-top-width': 1,
'border-top-color': '@bc',
+ 'font': '@font',
'margin': (0, 0, 3, 0),
- 'padding': (3, 10, 0, 10)
+ 'padding': (3, 10, 0, 10),
+
},
# horizontal seperator
'hr': {
@@ -758,7 +761,7 @@ def get_default_theme(self) -> dict[str, dict]:
},
# horizontal sliding selector
'slider': {
- 'font': ('Roboto Mono', 11, 'Normal'),
+ 'font': ('Roboto Mono', 11, 'normal'),
'color': '@fg',
'::groove:horizontal': {
'border-style': 'none',
diff --git a/src/widgetbuilder.py b/src/widgetbuilder.py
index d92bccd..05ceca0 100644
--- a/src/widgetbuilder.py
+++ b/src/widgetbuilder.py
@@ -71,6 +71,7 @@ def create_button_series2(
be a normal button; the bool value indicates the default state of the button
- "stretch": stretch value for the button
- "align": alignment flag for button
+ - "size": size policy for button
- :param style: key for theme, determines style preset
- :param shape: row / column
- :param separator: string seperator displayed between buttons (optional)
@@ -106,6 +107,8 @@ def create_button_series2(
bt.clicked[bool].connect(detail['callback'])
else:
bt.clicked.connect(detail['callback'])
+ if 'size' in detail:
+ bt.setSizePolicy(detail['size'])
stretch = detail['stretch'] if 'stretch' in detail else 0
if 'align' in detail:
layout.addWidget(bt, stretch, detail['align'])
diff --git a/src/widgets.py b/src/widgets.py
index 284d188..dc8fa3a 100644
--- a/src/widgets.py
+++ b/src/widgets.py
@@ -298,6 +298,7 @@ class Thread(QThread):
done: Signal = Signal()
def __init__(self, target: Callable, args: tuple = (), kwargs: dict[str] = {}):
+ super().__init__()
self._target: Callable = target
self._args: tuple = args
self._kwargs: dict[str] = kwargs
From 38ae5161d9d4117dcf2f13abfa1f98b01f0e2cff Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 10:05:44 +0200
Subject: [PATCH 25/44] renaming theme2 to theme
---
src/app.py | 448 ++++++++++++++++++++++++++---------------------------
1 file changed, 224 insertions(+), 224 deletions(-)
diff --git a/src/app.py b/src/app.py
index 08eaca3..f0ebff9 100644
--- a/src/app.py
+++ b/src/app.py
@@ -57,20 +57,20 @@ def __init__(self, args, app_dir_path: str, version: str):
self.settings = SETSSettings(self.config.config_dir / self.config.settings_file)
self.init_config()
QDir.addSearchPath('local_folder', self.app_dir / 'local')
- self.theme2: AppTheme = AppTheme(self.config.ui_scale)
+ self.theme: AppTheme = AppTheme(self.config.ui_scale)
self.init_environment()
self.downloader = Downloader(
self.config.config_subfolders['images'],
self.config.config_subfolders['ship_images'])
self.cargo: CargoManager = CargoManager(
self.config.config_subfolders, self.app_dir2, self.downloader, self.settings,
- self.theme2)
+ self.theme)
self.images: ImageManager = ImageManager(
Path(self.config.config_subfolders['images']),
Path(self.config.config_subfolders['ship_images']),
self.app_dir2, self.cargo, self.downloader)
self.build2: BuildManager = BuildManager(
- self.cargo, self.images, self.config.autosave_path, self.theme2.tooltips)
+ self.cargo, self.images, self.config.autosave_path, self.theme.tooltips)
self.splash: SplashScreen = SplashScreen()
self.tabbers: Tabbers = Tabbers()
self.app, self.window = self.create_main_window()
@@ -78,14 +78,14 @@ def __init__(self, args, app_dir_path: str, version: str):
self.cargo.load_static_data()
self.build_loader: BuildLoader = BuildLoader(
self.build2, self.cargo, self.config, self.settings, self.window)
- self.export_window = ExportWindow(self.theme2, self.window, self.build2, self.cargo)
- self.picker_window: Picker = Picker(self.theme2, self.window, self.settings, self.images)
+ self.export_window = ExportWindow(self.theme, self.window, self.build2, self.cargo)
+ self.picker_window: Picker = Picker(self.theme, self.window, self.settings, self.images)
self.picker_window.dialog_result.connect(self.build2.handle_picker_result)
- self.edit_window: ItemEditor = ItemEditor(self.theme2, self.window)
+ self.edit_window: ItemEditor = ItemEditor(self.theme, self.window)
self.edit_window.dialog_result.connect(self.build2.finish_item_edit)
- self.ship_selector_window: ShipSelector = ShipSelector(self.theme2, self.window)
+ self.ship_selector_window: ShipSelector = ShipSelector(self.theme, self.window)
self.ship_selector_window.dialog_result.connect(self.build2.finish_ship_pick)
- self.context_menu: ContextMenu = ContextMenu(self.theme2, self.build2, self.cargo)
+ self.context_menu: ContextMenu = ContextMenu(self.theme, self.build2, self.cargo)
self.context_menu.edit_slot.connect(self.edit_window.edit_item)
self.setup_main_layout()
self.window.show()
@@ -200,22 +200,22 @@ def cache_icons(self):
"""
Loads static icons.
"""
- self.theme2.icons['copy'] = load_icon('copy.png', self.app_dir2)
- self.theme2.icons['paste'] = load_icon('paste.png', self.app_dir2)
- self.theme2.icons['clear'] = load_icon('clear.png', self.app_dir2)
- self.theme2.icons['edit'] = load_icon('edit.png', self.app_dir2)
- self.theme2.icons['link'] = load_icon('external_link.png', self.app_dir2)
- self.theme2.icons['dual_cannons'] = load_icon('DC_icon.svg', self.app_dir2, size=(16, 24.5))
- icon_size = (self.theme2.opt.box_width * 1.2, self.theme2.opt.box_width * 1.2)
- self.theme2.icons['ground'] = load_icon('ground_icon.png', self.app_dir2, icon_size)
- icon_size = (self.theme2.opt.box_width, self.theme2.opt.box_width)
- self.theme2.icons['tac'] = load_icon('tac_icon.png', self.app_dir2, icon_size)
- self.theme2.icons['sci'] = load_icon('sci_icon.png', self.app_dir2, icon_size)
- self.theme2.icons['eng'] = load_icon('eng_icon.png', self.app_dir2, icon_size)
- self.theme2.icons['tac-small'] = load_icon('tac-small.svg', self.app_dir2, size=(25, 25))
- self.theme2.icons['sci-small'] = load_icon('sci-small.svg', self.app_dir2, size=(25, 25))
- icon_size = (self.theme2.opt.box_height, self.theme2.opt.box_width * 182 / 106)
- self.theme2.icons['STOCD'] = load_icon('stocd.png', self.app_dir2, icon_size)
+ self.theme.icons['copy'] = load_icon('copy.png', self.app_dir2)
+ self.theme.icons['paste'] = load_icon('paste.png', self.app_dir2)
+ self.theme.icons['clear'] = load_icon('clear.png', self.app_dir2)
+ self.theme.icons['edit'] = load_icon('edit.png', self.app_dir2)
+ self.theme.icons['link'] = load_icon('external_link.png', self.app_dir2)
+ self.theme.icons['dual_cannons'] = load_icon('DC_icon.svg', self.app_dir2, size=(16, 24.5))
+ icon_size = (self.theme.opt.box_width * 1.2, self.theme.opt.box_width * 1.2)
+ self.theme.icons['ground'] = load_icon('ground_icon.png', self.app_dir2, icon_size)
+ icon_size = (self.theme.opt.box_width, self.theme.opt.box_width)
+ self.theme.icons['tac'] = load_icon('tac_icon.png', self.app_dir2, icon_size)
+ self.theme.icons['sci'] = load_icon('sci_icon.png', self.app_dir2, icon_size)
+ self.theme.icons['eng'] = load_icon('eng_icon.png', self.app_dir2, icon_size)
+ self.theme.icons['tac-small'] = load_icon('tac-small.svg', self.app_dir2, size=(25, 25))
+ self.theme.icons['sci-small'] = load_icon('sci-small.svg', self.app_dir2, size=(25, 25))
+ icon_size = (self.theme.opt.box_height, self.theme.opt.box_width * 182 / 106)
+ self.theme.icons['STOCD'] = load_icon('stocd.png', self.app_dir2, icon_size)
def main_window_close_callback(self, event: QCloseEvent):
"""
@@ -242,7 +242,7 @@ def create_main_window(self, argv=[]) -> tuple[QApplication, QWidget]:
font_database.addApplicationFont(
str(self.app_dir2 / 'local' / 'Overpass-VariableFont_wght.ttf'))
font_database.addApplicationFont(str(self.app_dir2 / 'local' / 'RobotoMono-Regular.ttf'))
- app.setStyleSheet(self.theme2.create_style_sheet(self.theme2['app']['style']))
+ app.setStyleSheet(self.theme.create_style_sheet(self.theme['app']['style']))
window = QWidget()
window.setWindowIcon(load_icon('SETS_icon_small.png', self.app_dir2))
window.setWindowTitle('STO Equipment and Trait Selector')
@@ -328,24 +328,24 @@ def setup_main_layout(self):
# master layout: banner, borders and splash screen
layout = VBoxLayout()
background_frame = create_frame2(
- self.theme2, style_override={'background-color': '@sets'}, size_policy=SMINMIN)
+ self.theme, style_override={'background-color': '@sets'}, size_policy=SMINMIN)
layout.addWidget(background_frame)
self.window.setLayout(layout)
main_layout = VBoxLayout()
banner = ImageLabel(self.app_dir / 'local' / 'sets_banner.png', (2880, 126))
main_layout.addWidget(banner)
- frame_width = 8 * self.theme2.scale
+ frame_width = 8 * self.theme.scale
tabber_layout = VBoxLayout(margins=frame_width)
splash_tabber = QTabWidget()
- splash_tabber.setStyleSheet(self.theme2.get_style_class('QTabWidget', 'tabber'))
- splash_tabber.tabBar().setStyleSheet(self.theme2.get_style_class('QTabBar', 'tabber_tab'))
+ splash_tabber.setStyleSheet(self.theme.get_style_class('QTabWidget', 'tabber'))
+ splash_tabber.tabBar().setStyleSheet(self.theme.get_style_class('QTabBar', 'tabber_tab'))
splash_tabber.setSizePolicy(SMINMIN)
self.splash.tabber = splash_tabber
tabber_layout.addWidget(splash_tabber)
main_layout.addLayout(tabber_layout)
background_frame.setLayout(main_layout)
- content_frame = create_frame2(self.theme2)
- splash_frame = create_frame2(self.theme2)
+ content_frame = create_frame2(self.theme)
+ splash_frame = create_frame2(self.theme)
splash_tabber.addTab(content_frame, 'Main')
splash_tabber.addTab(splash_frame, 'Splash')
self.setup_splash(splash_frame)
@@ -354,7 +354,7 @@ def setup_main_layout(self):
content_layout.setColumnStretch(0, 1)
content_layout.setColumnStretch(1, 4)
- margin = 3 * self.theme2.scale
+ margin = 3 * self.theme.scale
menu_layout = GridLayout(margins=(margin, margin, margin, 0))
menu_layout.setColumnStretch(0, 2)
menu_layout.setColumnStretch(1, 5)
@@ -367,7 +367,7 @@ def setup_main_layout(self):
'Clear All Tabs': {'callback': self.build2.clear_all}
}
menu_layout.addLayout(
- create_button_series2(self.theme2, left_button_group), 0, 0, alignment=ALEFT | ATOP)
+ create_button_series2(self.theme, left_button_group), 0, 0, alignment=ALEFT | ATOP)
center_button_group = {
'default': {'font': ('Overpass', 16, 'medium')},
'SPACE': {'callback': lambda: self.tabbers.switch(0), 'stretch': 1, 'size': SMINMAX},
@@ -383,65 +383,65 @@ def setup_main_layout(self):
'size': SMINMAX
}
}
- center_buttons = create_button_series2(self.theme2, center_button_group, 'heavy_button')
+ center_buttons = create_button_series2(self.theme, center_button_group, 'heavy_button')
menu_layout.addLayout(center_buttons, 0, 1)
right_button_group = {
'Export': {'callback': self.export_window.invoke},
'Settings': {'callback': lambda: self.tabbers.switch(5)},
}
menu_layout.addLayout(
- create_button_series2(self.theme2, right_button_group), 0, 2, alignment=ARIGHT | ATOP)
+ create_button_series2(self.theme, right_button_group), 0, 2, alignment=ARIGHT | ATOP)
content_layout.addLayout(menu_layout, 0, 0, 1, 2)
# sidebar
- sidebar = create_frame2(self.theme2, size_policy=SMINMIN)
+ sidebar = create_frame2(self.theme, size_policy=SMINMIN)
sidebar_layout = GridLayout()
sidebar_tabber = QTabWidget()
- sidebar_tabber.setStyleSheet(self.theme2.get_style_class('QTabWidget', 'tabber'))
- sidebar_tabber.tabBar().setStyleSheet(self.theme2.get_style_class('QTabBar', 'tabber_tab'))
+ sidebar_tabber.setStyleSheet(self.theme.get_style_class('QTabWidget', 'tabber'))
+ sidebar_tabber.tabBar().setStyleSheet(self.theme.get_style_class('QTabBar', 'tabber_tab'))
sidebar_tabber.setSizePolicy(SMINMIN)
self.tabbers.sidebar_tabber = sidebar_tabber
for tab_name in ('space', 'ground', 'space_skills', 'ground_skills', 'empty', 'settings'):
- tab_frame = create_frame2(self.theme2)
+ tab_frame = create_frame2(self.theme)
sidebar_tabber.addTab(tab_frame, tab_name)
self.tabbers.sidebar_frames.append(tab_frame)
self.setup_ship_frame()
sidebar_layout.addWidget(sidebar_tabber, 0, 0)
character_tabber = QTabWidget()
- character_tabber.setStyleSheet(self.theme2.get_style_class('QTabWidget', 'tabber'))
+ character_tabber.setStyleSheet(self.theme.get_style_class('QTabWidget', 'tabber'))
character_tabber.tabBar().setStyleSheet(
- self.theme2.get_style_class('QTabBar', 'tabber_tab'))
+ self.theme.get_style_class('QTabBar', 'tabber_tab'))
character_tabber.setSizePolicy(SMINMAX)
self.tabbers.character_tabber = character_tabber
- char_frame = create_frame2(self.theme2)
+ char_frame = create_frame2(self.theme)
self.setup_character_frame(char_frame)
character_tabber.addTab(char_frame, 'char')
- empty_frame = create_frame2(self.theme2)
+ empty_frame = create_frame2(self.theme)
character_tabber.addTab(empty_frame, 'empty')
- settings_frame = create_frame2(self.theme2)
+ settings_frame = create_frame2(self.theme)
character_tabber.addTab(settings_frame, 'settings')
self.tabbers.character_frames = [char_frame, empty_frame, settings_frame]
sidebar_layout.addWidget(character_tabber, 1, 0)
- seperator = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ seperator = create_frame2(self.theme, size_policy=SMAXMIN, style_override={
'background-color': '@sets', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- seperator.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
+ seperator.setFixedWidth(self.theme['defaults']['sep'] * self.theme.scale)
sidebar_layout.addWidget(seperator, 0, 1, 2, 1)
sidebar.setLayout(sidebar_layout)
content_layout.addWidget(sidebar, 1, 0)
# build section
build_tabber = QTabWidget()
- build_tabber.setStyleSheet(self.theme2.get_style_class('QTabWidget', 'tabber'))
- build_tabber.tabBar().setStyleSheet(self.theme2.get_style_class('QTabBar', 'tabber_tab'))
+ build_tabber.setStyleSheet(self.theme.get_style_class('QTabWidget', 'tabber'))
+ build_tabber.tabBar().setStyleSheet(self.theme.get_style_class('QTabBar', 'tabber_tab'))
build_tabber.setSizePolicy(SMINMIN)
self.tabbers.build_tabber = build_tabber
build_tab_names = (
'space_build', 'ground_build', 'space_skills', 'ground_skills', 'library', 'settings')
for tab_name in build_tab_names:
- tab_frame = create_frame2(self.theme2)
+ tab_frame = create_frame2(self.theme)
build_tabber.addTab(tab_frame, tab_name)
self.tabbers.build_frames.append(tab_frame)
content_layout.addWidget(build_tabber, 1, 1)
@@ -459,10 +459,10 @@ def setup_ship_frame(self):
Creates ship info frame
"""
frame = self.tabbers.sidebar_frames[0]
- csp = self.theme2['defaults']['csp'] * self.theme2.scale
+ csp = self.theme['defaults']['csp'] * self.theme.scale
layout = VBoxLayout(margins=csp, spacing=csp)
- image_frame = create_frame2(self.theme2, size_policy=SMINMIN)
+ image_frame = create_frame2(self.theme, size_policy=SMINMIN)
image_layout = GridLayout()
ship_image = ShipImage()
ship_image.setSizePolicy(SMINMIN)
@@ -471,50 +471,50 @@ def setup_ship_frame(self):
image_frame.setLayout(image_layout)
layout.addWidget(image_frame, stretch=1)
- ship_frame = create_frame2(self.theme2, size_policy=SMINMIN)
+ ship_frame = create_frame2(self.theme, size_policy=SMINMIN)
ship_layout = GridLayout(spacing=csp)
ship_layout.setRowStretch(4, 1)
ship_layout.setColumnStretch(2, 1)
ship_selector = ShipButton('')
ship_selector.setSizePolicy(SMINMAX)
ship_selector.setStyleSheet(
- self.theme2.get_style_class('ShipButton', 'button', override={'margin': 0}))
- ship_selector.setFont(self.theme2.get_font(font_spec='@subhead'))
+ self.theme.get_style_class('ShipButton', 'button', override={'margin': 0}))
+ ship_selector.setFont(self.theme.get_font(font_spec='@subhead'))
ship_selector.clicked.connect(self.ship_selector_window.pick_ship)
self.build2.ship.button = ship_selector
ship_layout.addWidget(ship_selector, 0, 0, 1, 4, alignment=ATOP)
- tier_label = create_label2(self.theme2, 'Ship Tier:')
+ tier_label = create_label2(self.theme, 'Ship Tier:')
ship_layout.addWidget(tier_label, 1, 0)
- tier_combo = create_combo_box2(self.theme2)
+ tier_combo = create_combo_box2(self.theme)
tier_combo.currentTextChanged.connect(self.build2.tier_callback)
tier_combo.setSizePolicy(SMAXMAX)
self.build2.ship.tier = tier_combo
ship_layout.addWidget(tier_combo, 1, 1, alignment=ALEFT)
- dc_tooltip = create_label2(self.theme2, 'Can equip Dual Cannons', 'label_tooltip')
+ dc_tooltip = create_label2(self.theme, 'Can equip Dual Cannons', 'label_tooltip')
dc_label = TooltipLabel('', dc_tooltip)
- dc_label.setPixmap(self.theme2.icons['dual_cannons'])
+ dc_label.setPixmap(self.theme.icons['dual_cannons'])
dc_label_size_policy = dc_label.sizePolicy()
dc_label_size_policy.setRetainSizeWhenHidden(True)
dc_label.setSizePolicy(dc_label_size_policy)
self.build2.ship.dc = dc_label
ship_layout.addWidget(dc_label, 1, 2, alignment=ARIGHT)
- info_button = create_button2(self.theme2, 'Ship Info', style_override={'margin': 0})
+ info_button = create_button2(self.theme, 'Ship Info', style_override={'margin': 0})
info_button.clicked.connect(self.build2.ship_info_callback)
ship_layout.addWidget(info_button, 1, 3, alignment=ARIGHT)
- name_label = create_label2(self.theme2, 'Ship Name:')
+ name_label = create_label2(self.theme, 'Ship Name:')
ship_layout.addWidget(name_label, 2, 0)
- name_entry = create_entry2(self.theme2)
+ name_entry = create_entry2(self.theme)
name_entry.editingFinished.connect(
lambda: self.build2.set('space', 'ship_name', value=name_entry.text()))
self.build2.ship.name = name_entry
name_entry.setSizePolicy(SMINMAX)
ship_layout.addWidget(name_entry, 2, 1, 1, 3)
- desc_label = create_label2(self.theme2, 'Build Description:')
+ desc_label = create_label2(self.theme, 'Build Description:')
ship_layout.addWidget(desc_label, 3, 0, 1, 4)
desc_edit = QPlainTextEdit()
desc_edit.setSizePolicy(SMINMIN)
- desc_edit.setStyleSheet(self.theme2.get_style_class('QPlainTextEdit', 'textedit'))
- desc_edit.setFont(self.theme2.get_font('textedit'))
+ desc_edit.setStyleSheet(self.theme.get_style_class('QPlainTextEdit', 'textedit'))
+ desc_edit.setFont(self.theme.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
desc_edit.textChanged.connect(lambda: self.build2.set(
'space', 'ship_desc', value=desc_edit.toPlainText(), autosave=False))
@@ -538,8 +538,8 @@ def create_build_section(
- :param is_equipment: True when items are equipment, False if items are abilities or traits
- :param label_store: stores category label in self.widgets.build[`label_store`] if set
"""
- layout = GridLayout(spacing=self.theme2['defaults']['margin'] * self.theme2.scale)
- label = create_label2(self.theme2, label_text, style_override={'margin': (0, 0, 6, 0)})
+ layout = GridLayout(spacing=self.theme['defaults']['margin'] * self.theme.scale)
+ label = create_label2(self.theme, label_text, style_override={'margin': (0, 0, 6, 0)})
label_size_policy = label.sizePolicy()
label_size_policy.setRetainSizeWhenHidden(True)
label.setSizePolicy(label_size_policy)
@@ -548,7 +548,7 @@ def create_build_section(
if label_store != '':
setattr(widget_storage, label_store, label)
for i in range(button_count):
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.clicked.connect(lambda subkey=i, bt=button: self.picker(
environment, build_key, subkey, bt, is_equipment))
button.rightclicked.connect(lambda event, subkey=i: self.context_menu.invoke(
@@ -567,7 +567,7 @@ def create_boff_station_space(
- :param specialization: specialization of the seat; empty if it has no specialization
- :param boff_id: identifies the boff station
"""
- layout = GridLayout(spacing=self.theme2['defaults']['margin'] * self.theme2.scale)
+ layout = GridLayout(spacing=self.theme['defaults']['margin'] * self.theme.scale)
layout.setColumnStretch(3, 1)
if specialization != '':
specialization = f' / {specialization}'
@@ -581,12 +581,12 @@ def create_boff_station_space(
label_options = (profession + specialization,)
widget_storage = self.build2.space
label_layout = HBoxLayout(spacing=self.config.ui_scale * 3)
- icon_label = TooltipLabel('', create_label2(self.theme2, '', 'label_tooltip'))
+ icon_label = TooltipLabel('', create_label2(self.theme, '', 'label_tooltip'))
widget_storage.boff_label_icons[boff_id] = icon_label
label_layout.addWidget(icon_label, alignment=ALEFT)
icon_label.hide()
label = create_combo_box2(
- self.theme2, size_policy=SMAXMAX, style_override=self.theme2['boff_combo'])
+ self.theme, size_policy=SMAXMAX, style_override=self.theme['boff_combo'])
label.currentTextChanged.connect(
lambda new: self.build2.boff_profession_callback_space(boff_id, new))
label.addItems(label_options)
@@ -597,7 +597,7 @@ def create_boff_station_space(
label_layout.addWidget(label, alignment=ALEFT)
layout.addLayout(label_layout, 0, 0, 1, 4, alignment=ALEFT)
for i in range(4):
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.sizePolicy().setRetainSizeWhenHidden(True)
button.clicked.connect(lambda subkey=i, bt=button: self.picker(
'space', 'boffs', subkey, bt, boff_id=boff_id))
@@ -615,17 +615,17 @@ def create_boff_station_ground(self, boff_id: int) -> VBoxLayout:
- :param boff_id: identifies the boff station
"""
widget_storage = self.build2.ground
- m = self.theme2['defaults']['margin'] * self.theme2.scale
+ m = self.theme['defaults']['margin'] * self.theme.scale
layout = VBoxLayout(spacing=m)
label_layout = HBoxLayout(spacing=m)
label_layout.setAlignment(ALEFT)
- prof_label = create_combo_box2(self.theme2, style_override=self.theme2['boff_combo'])
+ prof_label = create_combo_box2(self.theme, style_override=self.theme['boff_combo'])
prof_label.currentTextChanged.connect(
lambda new: self.build2.boff_label_callback_ground(boff_id, 'boff_profs', new))
prof_label.addItems(CAREERS)
widget_storage.boff_profs[boff_id] = prof_label
label_layout.addWidget(prof_label)
- spec_label = create_combo_box2(self.theme2, style_override=self.theme2['boff_combo'])
+ spec_label = create_combo_box2(self.theme, style_override=self.theme['boff_combo'])
spec_label.currentTextChanged.connect(
lambda new: self.build2.boff_label_callback_ground(boff_id, 'boff_specs', new))
spec_label.addItems(GROUND_BOFF_SPECS)
@@ -635,7 +635,7 @@ def create_boff_station_ground(self, boff_id: int) -> VBoxLayout:
button_layout = HBoxLayout(spacing=m)
button_layout.setAlignment(ALEFT)
for i in range(4):
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.clicked.connect(lambda subkey=i, bt=button: self.picker(
'ground', 'boffs', subkey, bt, boff_id=boff_id))
button.rightclicked.connect(lambda event, subkey=i: self.context_menu.invoke(
@@ -652,15 +652,15 @@ def create_personal_trait_section(self, environment: str) -> GridLayout:
Parameters:
- :param environment: "space" / "ground"
"""
- layout = GridLayout(spacing=self.theme2['defaults']['margin'] * self.theme2.scale)
+ layout = GridLayout(spacing=self.theme['defaults']['margin'] * self.theme.scale)
label = create_label2(
- self.theme2, 'Personal Traits', style_override={'margin': (0, 0, 6, 0)})
+ self.theme, 'Personal Traits', style_override={'margin': (0, 0, 6, 0)})
layout.addWidget(label, 0, 0, 1, 4, alignment=ALEFT)
widget_storage = self.build2.space if environment == 'space' else self.build2.ground
for row in range(3):
for col in range(4):
i = row * 4 + col
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.clicked.connect(
lambda subkey=i, bt=button: self.picker(environment, 'traits', subkey, bt))
button.rightclicked.connect(lambda event, subkey=i: self.context_menu.invoke(
@@ -669,21 +669,21 @@ def create_personal_trait_section(self, environment: str) -> GridLayout:
widget_storage.traits[i] = button
# Last button is for innate trait and should not be clickable
button.setEnabled(False)
- button.set_style(self.theme2['item_dark'])
+ button.set_style(self.theme['item_dark'])
return layout
def create_starship_trait_section(self) -> GridLayout:
"""
Creates build section for starship traits
"""
- layout = GridLayout(spacing=self.theme2['defaults']['margin'] * self.theme2.scale)
+ layout = GridLayout(spacing=self.theme['defaults']['margin'] * self.theme.scale)
label = create_label2(
- self.theme2, 'Starship Traits', style_override={'margin': (0, 0, 6, 0)})
+ self.theme, 'Starship Traits', style_override={'margin': (0, 0, 6, 0)})
label.sizePolicy().setRetainSizeWhenHidden(True)
layout.addWidget(label, 0, 0, 1, 4, alignment=ALEFT)
widget_storage = self.build2.space
for col in range(5):
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.sizePolicy().setRetainSizeWhenHidden(True)
button.clicked.connect(
lambda subkey=col, bt=button: self.picker('space', 'starship_traits', subkey, bt))
@@ -692,7 +692,7 @@ def create_starship_trait_section(self) -> GridLayout:
layout.addWidget(button, 1, col, alignment=ALEFT)
widget_storage.starship_traits[col] = button
for col in range(2):
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.sizePolicy().setRetainSizeWhenHidden(True)
button.clicked.connect(lambda subkey=col + 5, bt=button: self.picker(
'space', 'starship_traits', subkey, bt))
@@ -709,17 +709,17 @@ def create_doff_section(self, environment: str) -> GridLayout:
Parameters:
- :param environment: "space" / "ground"
"""
- doff_layout = GridLayout(spacing=self.theme2['defaults']['bw'] * self.theme2.scale)
+ doff_layout = GridLayout(spacing=self.theme['defaults']['bw'] * self.theme.scale)
doff_layout.setColumnStretch(1, 1)
widget_storage = self.build2.space if environment == 'space' else self.build2.ground
for i in range(6):
- spec_combo = create_combo_box2(self.theme2, style_override=self.theme2['doff_combo'])
+ spec_combo = create_combo_box2(self.theme, style_override=self.theme['doff_combo'])
spec_combo.currentTextChanged.connect(
lambda spec, id=i: self.build2.doff_spec_callback(spec, environment, id))
doff_layout.addWidget(spec_combo, i, 0)
widget_storage.doffs_spec[i] = spec_combo
variant_combo = create_combo_box2(
- self.theme2, style_override=self.theme2['doff_combo'], class_=DoffCombobox)
+ self.theme, style_override=self.theme['doff_combo'], class_=DoffCombobox)
variant_combo.currentTextChanged.connect(
lambda variant, id=i: self.build2.doff_variant_callback(variant, environment, id))
doff_layout.addWidget(variant_combo, i, 1)
@@ -734,44 +734,44 @@ def create_skill_group_space(self, group_data: dict, id_offset: int) -> GridLayo
- :param group_data: skill group data
- :param id_offset: index of the first skill node in self.build
"""
- layout = GridLayout(spacing=self.theme2['defaults']['csp'] * self.config.ui_scale)
+ layout = GridLayout(spacing=self.theme['defaults']['csp'] * self.config.ui_scale)
# one skill with 3 ranks
if group_data['grouping'] == 'column':
for index, node in enumerate(group_data['nodes']):
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
skill_id = id_offset + index
button.clicked.connect(lambda id=skill_id: self.build2.skill_callback_space(
group_data['career'], id, 'column'))
button.skill_image_name = node['image']
button.tooltip = format_skill_tooltip(
- group_data['skill'], group_data, index, 'space', self.theme2.tooltips)
+ group_data['skill'], group_data, index, 'space', self.theme.tooltips)
self.build2.skills.space[group_data['career']][id_offset + index] = button
layout.addWidget(button, index, 0)
# == 'pair+1': one skill with 2 ranks and one sub-skill with 1 rank
# == 'separate': 3 separate skills
else:
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.clicked.connect(lambda id=id_offset: self.build2.skill_callback_space(
group_data['career'], id, group_data['grouping']))
button.skill_image_name = group_data['nodes'][0]['image']
button.tooltip = format_skill_tooltip(
- group_data['skill'][0], group_data, 0, 'space', self.theme2.tooltips)
+ group_data['skill'][0], group_data, 0, 'space', self.theme.tooltips)
layout.addWidget(button, 0, 0, 1, 2, alignment=AHCENTER | ABOTTOM)
self.build2.skills.space[group_data['career']][id_offset] = button
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.clicked.connect(lambda id=id_offset + 1: self.build2.skill_callback_space(
group_data['career'], id, group_data['grouping']))
button.skill_image_name = group_data['nodes'][1]['image']
button.tooltip = format_skill_tooltip(
- group_data['skill'][1], group_data, 1, 'space', self.theme2.tooltips)
+ group_data['skill'][1], group_data, 1, 'space', self.theme.tooltips)
layout.addWidget(button, 1, 0, alignment=ATOP)
self.build2.skills.space[group_data['career']][id_offset + 1] = button
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.clicked.connect(lambda id=id_offset + 2: self.build2.skill_callback_space(
group_data['career'], id, group_data['grouping']))
button.skill_image_name = group_data['nodes'][2]['image']
button.tooltip = format_skill_tooltip(
- group_data['skill'][2], group_data, 2, 'space', self.theme2.tooltips)
+ group_data['skill'][2], group_data, 2, 'space', self.theme.tooltips)
layout.addWidget(button, 1, 1, alignment=ATOP)
self.build2.skills.space[group_data['career']][id_offset + 2] = button
return layout
@@ -791,8 +791,8 @@ def create_bonus_bar_segment(
seg = QPushButton()
seg.setEnabled(False)
seg.setCheckable(True)
- seg.setStyleSheet(self.theme2.get_style_class('QPushButton', style, style_override))
- seg.setFixedSize(7 * self.theme2.scale, 17 * self.theme2.scale)
+ seg.setStyleSheet(self.theme.get_style_class('QPushButton', style, style_override))
+ seg.setFixedSize(7 * self.theme.scale, 17 * self.theme.scale)
self.build2.skills.bonus_bars[bar][index] = seg
return seg
@@ -809,7 +809,7 @@ def create_bonus_bar_space(self, career: str, layout: GridLayout, column: int):
button_index = 0
for row in range(29, 5, -1):
if row % 6 == 0:
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.clicked.connect(
lambda i=button_index: self.build2.skill_unlock_callback(career, i))
layout.addWidget(button, row, column, alignment=AHCENTER)
@@ -823,7 +823,7 @@ def create_bonus_bar_space(self, career: str, layout: GridLayout, column: int):
segment = self.create_bonus_bar_segment(career, segment_index)
layout.addWidget(segment, row, column, alignment=AHCENTER)
segment_index += 1
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.clicked.connect(lambda: self.build2.skill_unlock_callback(career, 4))
layout.addWidget(button, 1, column, alignment=AHCENTER)
self.build2.skills.unlocks[career][4] = button
@@ -837,12 +837,12 @@ def create_skill_button_ground(self, group_data: dict, id: int, node_id: int) ->
- :param id: index of the skill node in self.build
- :param node_id: 0 or 1 for first or second node
"""
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.clicked.connect(lambda: self.build2.skill_callback_ground(group_data['tree'], id))
button.skill_image_name = group_data['nodes'][node_id]['image']
button.tooltip = format_skill_tooltip(
group_data['nodes'][node_id]['name'], group_data, node_id, 'ground',
- self.theme2.tooltips)
+ self.theme.tooltips)
self.build2.skills.ground[group_data['tree']][id] = button
return button
@@ -851,7 +851,7 @@ def setup_space_build_frame(self):
Creates space build layout
"""
frame = self.tabbers.build_frames[0]
- isp = self.theme2['defaults']['isp'] * 2 * self.theme2.scale
+ isp = self.theme['defaults']['isp'] * 2 * self.theme.scale
layout = GridLayout(margins=isp, spacing=isp)
layout.setColumnStretch(0, 1)
layout.setColumnStretch(10, 1)
@@ -871,9 +871,9 @@ def setup_space_build_frame(self):
hangar_layout = self.create_build_section(
'Hangars', 2, 'space', 'hangars', True, 'hangars_label')
layout.addLayout(hangar_layout, 4, 1, alignment=ALEFT)
- sep1 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ sep1 = create_frame2(self.theme, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep1.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
+ sep1.setFixedWidth(self.theme['defaults']['sep'] * self.theme.scale)
layout.addWidget(sep1, 0, 2, 5, 1)
deflector_layout = self.create_build_section('Deflector', 1, 'space', 'deflector', True)
@@ -887,9 +887,9 @@ def setup_space_build_frame(self):
layout.addLayout(warp_layout, 3, 3, alignment=ALEFT)
shield_layout = self.create_build_section('Shield', 1, 'space', 'shield', True)
layout.addLayout(shield_layout, 4, 3, alignment=ALEFT)
- sep2 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ sep2 = create_frame2(self.theme, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep2.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
+ sep2.setFixedWidth(self.theme['defaults']['sep'] * self.theme.scale)
layout.addWidget(sep2, 0, 4, 5, 1)
uni_layout = self.create_build_section(
@@ -904,9 +904,9 @@ def setup_space_build_frame(self):
tac_layout = self.create_build_section(
'Tactical Consoles', 5, 'space', 'tac_consoles', True, 'tac_consoles_label')
layout.addLayout(tac_layout, 3, 5, alignment=ALEFT)
- sep3 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ sep3 = create_frame2(self.theme, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep3.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
+ sep3.setFixedWidth(self.theme['defaults']['sep'] * self.theme.scale)
layout.addWidget(sep3, 0, 6, 5, 1)
# Boffs
@@ -921,7 +921,7 @@ def setup_space_build_frame(self):
boff_5_layout = self.create_boff_station_space('Universal', 'Temporal', boff_id=4)
layout.addLayout(boff_5_layout, 4, 7, alignment=ALEFT)
boff_6_layout = self.create_boff_station_space('Universal', boff_id=5)
- width_placeholder = create_combo_box2(self.theme2, size_policy=SMAXMAX)
+ width_placeholder = create_combo_box2(self.theme, size_policy=SMAXMAX)
width_placeholder.addItem('Engineering / Miracle Worker')
width_placeholder_sizepolicy = width_placeholder.sizePolicy()
width_placeholder_sizepolicy.setRetainSizeWhenHidden(True)
@@ -946,14 +946,14 @@ def setup_space_build_frame(self):
layout.addLayout(trait_layout, 0, 9, 6, 1, alignment=ATOP)
# Doffs
- spacing = self.theme2['defaults']['bw'] * self.theme2.scale
- doff_container = create_frame2(self.theme2, size_policy=SMINMAX)
+ spacing = self.theme['defaults']['bw'] * self.theme.scale
+ doff_container = create_frame2(self.theme, size_policy=SMINMAX)
doff_container_layout = VBoxLayout(spacing=spacing * 2)
- doff_label = create_label2(self.theme2, 'Space Duty Officers')
+ doff_label = create_label2(self.theme, 'Space Duty Officers')
doff_container_layout.addWidget(doff_label, alignment=ALEFT)
- doff_frame = create_frame2(self.theme2, 'doff_frame', size_policy=SMINMAX)
+ doff_frame = create_frame2(self.theme, 'doff_frame', size_policy=SMINMAX)
doff_frame_layout = VBoxLayout()
- doff_style_nullifier = create_frame2(self.theme2, size_policy=SMINMAX)
+ doff_style_nullifier = create_frame2(self.theme, size_policy=SMINMAX)
doff_frame_layout.addWidget(doff_style_nullifier)
doff_layout = self.create_doff_section('space')
doff_style_nullifier.setLayout(doff_layout)
@@ -969,7 +969,7 @@ def setup_ground_build_frame(self):
Creates Ground build frame
"""
frame = self.tabbers.build_frames[1]
- isp = self.theme2['defaults']['isp'] * 2 * self.theme2.scale
+ isp = self.theme['defaults']['isp'] * 2 * self.theme.scale
layout = GridLayout(margins=isp, spacing=isp)
layout.setColumnStretch(0, 1)
layout.setColumnStretch(8, 1)
@@ -982,9 +982,9 @@ def setup_ground_build_frame(self):
layout.addLayout(weapons_layout, 1, 1, alignment=ALEFT)
devices_layout = self.create_build_section('Devices', 5, 'ground', 'ground_devices', True)
layout.addLayout(devices_layout, 2, 1, alignment=ALEFT)
- sep1 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ sep1 = create_frame2(self.theme, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep1.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
+ sep1.setFixedWidth(self.theme['defaults']['sep'] * self.theme.scale)
layout.addWidget(sep1, 0, 2)
kit_layout = self.create_build_section('Kit Frame', 1, 'ground', 'kit', True)
layout.addLayout(kit_layout, 0, 3, alignment=ALEFT)
@@ -994,9 +994,9 @@ def setup_ground_build_frame(self):
layout.addLayout(ev_layout, 2, 3, alignment=ALEFT)
shield_layout = self.create_build_section('Shield', 1, 'ground', 'personal_shield', True)
layout.addLayout(shield_layout, 3, 3, alignment=ALEFT)
- sep2 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ sep2 = create_frame2(self.theme, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep2.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
+ sep2.setFixedWidth(self.theme['defaults']['sep'] * self.theme.scale)
layout.addWidget(sep2, 0, 4)
# Boffs
@@ -1008,9 +1008,9 @@ def setup_ground_build_frame(self):
layout.addLayout(boff_3_layout, 2, 5, alignment=ALEFT)
boff_4_layout = self.create_boff_station_ground(boff_id=3)
layout.addLayout(boff_4_layout, 3, 5, alignment=ALEFT)
- sep3 = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ sep3 = create_frame2(self.theme, size_policy=SMAXMIN, style_override={
'background-color': '@bg', 'margin-top': '@isp', 'margin-bottom': '@isp'})
- sep3.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
+ sep3.setFixedWidth(self.theme['defaults']['sep'] * self.theme.scale)
layout.addWidget(sep3, 0, 6)
# Traits
@@ -1025,14 +1025,14 @@ def setup_ground_build_frame(self):
layout.addLayout(trait_layout, 0, 7, 4, 1, alignment=ATOP)
# Doffs
- spacing = self.theme2['defaults']['bw'] * self.theme2.scale
- doff_container = create_frame2(self.theme2, size_policy=SMINMAX)
+ spacing = self.theme['defaults']['bw'] * self.theme.scale
+ doff_container = create_frame2(self.theme, size_policy=SMINMAX)
doff_container_layout = VBoxLayout(spacing=spacing * 2)
- doff_label = create_label2(self.theme2, 'Ground Duty Officers')
+ doff_label = create_label2(self.theme, 'Ground Duty Officers')
doff_container_layout.addWidget(doff_label, alignment=ALEFT)
- doff_frame = create_frame2(self.theme2, 'doff_frame', size_policy=SMINMAX)
+ doff_frame = create_frame2(self.theme, 'doff_frame', size_policy=SMINMAX)
doff_frame_layout = VBoxLayout()
- doff_style_nullifier = create_frame2(self.theme2, size_policy=SMINMAX)
+ doff_style_nullifier = create_frame2(self.theme, size_policy=SMINMAX)
doff_frame_layout.addWidget(doff_style_nullifier)
doff_layout = self.create_doff_section('ground')
doff_style_nullifier.setLayout(doff_layout)
@@ -1045,14 +1045,14 @@ def setup_ground_build_frame(self):
# sidebar
sidebar_frame = self.tabbers.sidebar_frames[1]
- csp = self.theme2['defaults']['csp'] * self.theme2.scale
+ csp = self.theme['defaults']['csp'] * self.theme.scale
sidebar_layout = GridLayout(margins=(csp, isp, csp, csp), spacing=csp)
sidebar_layout.setColumnStretch(0, 1)
- desc_label = create_label2(self.theme2, 'Build Description:')
+ desc_label = create_label2(self.theme, 'Build Description:')
sidebar_layout.addWidget(desc_label, 0, 0)
desc_edit = QPlainTextEdit()
- desc_edit.setStyleSheet(self.theme2.get_style_class('QPlainTextEdit', 'textedit'))
- desc_edit.setFont(self.theme2.get_font('textedit'))
+ desc_edit.setStyleSheet(self.theme.get_style_class('QPlainTextEdit', 'textedit'))
+ desc_edit.setFont(self.theme.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
desc_edit.textChanged.connect(lambda: self.build2.set(
'ground', 'ground_desc', value=desc_edit.toPlainText(), autosave=False))
@@ -1064,60 +1064,60 @@ def setup_character_frame(self, frame: QFrame):
"""
Creates character customization area.
"""
- csp = self.theme2['defaults']['csp'] * self.theme2.scale
+ csp = self.theme['defaults']['csp'] * self.theme.scale
layout = GridLayout(margins=csp, spacing=csp)
layout.setColumnStretch(1, 1)
- seperator = create_frame2(self.theme2, size_policy=SMINMAX, style_override={
+ seperator = create_frame2(self.theme, size_policy=SMINMAX, style_override={
'background-color': '@sets', 'margin': '@isp'})
- seperator.setFixedHeight(self.theme2['defaults']['sep'] * self.theme2.scale)
+ seperator.setFixedHeight(self.theme['defaults']['sep'] * self.theme.scale)
layout.addWidget(seperator, 0, 0, 1, 2, alignment=ATOP) # ATOP makes it respect the margin?
- char_name = create_entry2(self.theme2, placeholder='NAME')
+ char_name = create_entry2(self.theme, placeholder='NAME')
char_name.setAlignment(AHCENTER)
char_name.setSizePolicy(SMINMAX)
char_name.editingFinished.connect(
lambda: self.build2.set('captain', 'name', value=char_name.text()))
layout.addWidget(char_name, 1, 0, 1, 2)
self.build2.character.name = char_name
- elite_label = create_label2(self.theme2, 'Elite Captain')
+ elite_label = create_label2(self.theme, 'Elite Captain')
layout.addWidget(elite_label, 2, 0, alignment=ARIGHT)
- elite_checkbox = create_checkbox2(self.theme2)
+ elite_checkbox = create_checkbox2(self.theme)
elite_checkbox.checkStateChanged.connect(self.build2.elite_callback)
layout.addWidget(elite_checkbox, 2, 1, alignment=ALEFT)
self.build2.character.elite = elite_checkbox
- career_label = create_label2(self.theme2, 'Captain Career')
+ career_label = create_label2(self.theme, 'Captain Career')
layout.addWidget(career_label, 3, 0, alignment=ARIGHT)
- career_combo = create_combo_box2(self.theme2)
+ career_combo = create_combo_box2(self.theme)
career_combo.addItems({''} | CAREERS)
career_combo.currentTextChanged.connect(
lambda new_career: self.build2.set('captain', 'career', value=new_career))
layout.addWidget(career_combo, 3, 1)
self.build2.character.career = career_combo
- faction_label = create_label2(self.theme2, 'Faction')
+ faction_label = create_label2(self.theme, 'Faction')
layout.addWidget(faction_label, 4, 0, alignment=ARIGHT)
- faction_combo = create_combo_box2(self.theme2)
+ faction_combo = create_combo_box2(self.theme)
faction_combo.addItems({''} | FACTIONS)
faction_combo.currentTextChanged.connect(self.build2.faction_combo_callback)
layout.addWidget(faction_combo, 4, 1)
self.build2.character.faction = faction_combo
- species_label = create_label2(self.theme2, 'Species')
+ species_label = create_label2(self.theme, 'Species')
layout.addWidget(species_label, 5, 0, alignment=ARIGHT)
- species_combo = create_combo_box2(self.theme2)
+ species_combo = create_combo_box2(self.theme)
species_combo.addItems({''})
species_combo.currentTextChanged.connect(self.build2.species_combo_callback)
layout.addWidget(species_combo, 5, 1)
self.build2.character.species = species_combo
- primary_label = create_label2(self.theme2, 'Primary Spec')
+ primary_label = create_label2(self.theme, 'Primary Spec')
layout.addWidget(primary_label, 6, 0, alignment=ARIGHT)
- primary_combo = create_combo_box2(self.theme2)
+ primary_combo = create_combo_box2(self.theme)
primary_combo.addItems({''} | PRIMARY_SPECS)
primary_combo.currentTextChanged.connect(
lambda new_spec: self.build2.spec_combo_callback(True, new_spec))
layout.addWidget(primary_combo, 6, 1)
self.build2.character.primary = primary_combo
secondary_label = create_label2(
- self.theme2, 'Secondary Spec', style_override={'margin-bottom': 0})
+ self.theme, 'Secondary Spec', style_override={'margin-bottom': 0})
layout.addWidget(secondary_label, 7, 0, alignment=ARIGHT)
- secondary_combo = create_combo_box2(self.theme2)
+ secondary_combo = create_combo_box2(self.theme)
secondary_combo.addItems({''} | PRIMARY_SPECS | SECONDARY_SPECS)
secondary_combo.currentTextChanged.connect(
lambda new_spec: self.build2.spec_combo_callback(False, new_spec))
@@ -1130,13 +1130,13 @@ def setup_space_skill_frame(self):
Creates Space skill GUI
"""
frame = self.tabbers.build_frames[2]
- isp = self.theme2['defaults']['isp'] * self.theme2.scale
- csp = self.theme2['defaults']['csp'] * self.theme2.scale
+ isp = self.theme['defaults']['isp'] * self.theme.scale
+ csp = self.theme['defaults']['csp'] * self.theme.scale
col_layout = GridLayout(margins=isp, spacing=csp)
col_layout.setRowStretch(0, 1)
col_layout.setColumnStretch(0, 3)
col_layout.setColumnStretch(2, 1)
- scroll_frame = create_frame2(self.theme2)
+ scroll_frame = create_frame2(self.theme)
scroll_area = QScrollArea()
scroll_area.setSizePolicy(SMINMIN)
scroll_area.setHorizontalScrollBarPolicy(SCROLLOFF)
@@ -1160,16 +1160,16 @@ def setup_space_skill_frame(self):
'Captain
(25 points required)',
'Admiral
(35 points required)'
)
- sep_height = self.theme2['hr']['height'] * self.theme2.scale
+ sep_height = self.theme['hr']['height'] * self.theme.scale
for rank, skill_groups in enumerate(self.cargo.skills['space']):
header_layout = GridLayout(spacing=isp)
- left_sep = create_frame2(self.theme2, 'hr', size_policy=SMINMAX)
+ left_sep = create_frame2(self.theme, 'hr', size_policy=SMINMAX)
left_sep.setFixedHeight(sep_height)
header_layout.addWidget(left_sep, 0, 0, alignment=AVCENTER)
- rank_label = create_label2(self.theme2, rank_texts[rank], 'label_subhead')
+ rank_label = create_label2(self.theme, rank_texts[rank], 'label_subhead')
rank_label.setAlignment(AHCENTER)
header_layout.addWidget(rank_label, 0, 1)
- right_sep = create_frame2(self.theme2, 'hr', size_policy=SMINMAX)
+ right_sep = create_frame2(self.theme, 'hr', size_policy=SMINMAX)
right_sep.setFixedHeight(sep_height)
header_layout.addWidget(right_sep, 0, 2, alignment=AVCENTER)
scroll_layout.addLayout(header_layout, rank * 3, 0, 1, 6)
@@ -1177,41 +1177,41 @@ def setup_space_skill_frame(self):
id_offset = rank * 6 + (group_id % 2) * 3
group_layout = self.create_skill_group_space(group_data, id_offset)
scroll_layout.addLayout(group_layout, rank * 3 + 1, group_id)
- spacer = create_frame2(self.theme2)
+ spacer = create_frame2(self.theme)
spacer.setFixedHeight(isp)
scroll_layout.addWidget(spacer, rank * 3 + 2, 0)
VBoxLayout().addWidget(spacer)
scroll_frame.setLayout(scroll_layout)
scroll_area.setWidget(scroll_frame)
- seperator = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ seperator = create_frame2(self.theme, size_policy=SMAXMIN, style_override={
'background-color': '@sets'})
- seperator.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
+ seperator.setFixedWidth(self.theme['defaults']['sep'] * self.theme.scale)
col_layout.addWidget(seperator, 0, 1)
- bonus_bar_container = create_frame2(self.theme2, size_policy=SMINMIN)
+ bonus_bar_container = create_frame2(self.theme, size_policy=SMINMIN)
# bonus bars
bonus_bar_layout = GridLayout(margins=isp)
bonus_bar_layout.setRowStretch(0, 1)
bonus_bar_layout.setRowStretch(32, 1)
self.create_bonus_bar_space('eng', bonus_bar_layout, 1)
- eng_label = create_label2(self.theme2, '', style='unlock_label')
- eng_label.setPixmap(self.theme2.icons['eng'])
+ eng_label = create_label2(self.theme, '', style='unlock_label')
+ eng_label.setPixmap(self.theme.icons['eng'])
bonus_bar_layout.addWidget(eng_label, 30, 1, alignment=AHCENTER)
- eng_count = create_label2(self.theme2, '0', 'label_subhead')
+ eng_count = create_label2(self.theme, '0', 'label_subhead')
bonus_bar_layout.addWidget(eng_count, 31, 1, alignment=AHCENTER)
self.build2.skills.count_labels['eng'] = eng_count
self.create_bonus_bar_space('sci', bonus_bar_layout, 2)
- sci_label = create_label2(self.theme2, '', style='unlock_label')
- sci_label.setPixmap(self.theme2.icons['sci'])
+ sci_label = create_label2(self.theme, '', style='unlock_label')
+ sci_label.setPixmap(self.theme.icons['sci'])
bonus_bar_layout.addWidget(sci_label, 30, 2, alignment=AHCENTER)
- sci_count = create_label2(self.theme2, '0', 'label_subhead')
+ sci_count = create_label2(self.theme, '0', 'label_subhead')
bonus_bar_layout.addWidget(sci_count, 31, 2, alignment=AHCENTER)
self.build2.skills.count_labels['sci'] = sci_count
self.create_bonus_bar_space('tac', bonus_bar_layout, 3)
- tac_label = create_label2(self.theme2, '', style='unlock_label')
- tac_label.setPixmap(self.theme2.icons['tac'])
+ tac_label = create_label2(self.theme, '', style='unlock_label')
+ tac_label.setPixmap(self.theme.icons['tac'])
bonus_bar_layout.addWidget(tac_label, 30, 3, alignment=AHCENTER)
- tac_count = create_label2(self.theme2, '0', 'label_subhead')
+ tac_count = create_label2(self.theme, '0', 'label_subhead')
bonus_bar_layout.addWidget(tac_count, 31, 3, alignment=AHCENTER)
self.build2.skills.count_labels['tac'] = tac_count
bonus_bar_container.setLayout(bonus_bar_layout)
@@ -1221,20 +1221,20 @@ def setup_space_skill_frame(self):
# sidebar
sidebar_frame = self.tabbers.sidebar_frames[2]
sidebar_layout = GridLayout(margins=(csp, isp * 2, csp, csp), spacing=csp)
- desc_label = create_label2(self.theme2, 'Space Skill Notes:')
+ desc_label = create_label2(self.theme, 'Space Skill Notes:')
sidebar_layout.addWidget(desc_label, 0, 0, 1, 2)
desc_edit = QPlainTextEdit()
- desc_edit.setStyleSheet(self.theme2.get_style_class('QPlainTextEdit', 'textedit'))
- desc_edit.setFont(self.theme2.get_font('textedit'))
+ desc_edit.setStyleSheet(self.theme.get_style_class('QPlainTextEdit', 'textedit'))
+ desc_edit.setFont(self.theme.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
desc_edit.textChanged.connect(lambda: self.build2.set(
'space', 'skill_desc', value=desc_edit.toPlainText(), autosave=False))
self.build2.skills.space_desc = desc_edit
sidebar_layout.addWidget(desc_edit, 1, 0, 1, 2)
- load_skills_button = create_button2(self.theme2, 'Load Skills')
+ load_skills_button = create_button2(self.theme, 'Load Skills')
load_skills_button.clicked.connect(self.build_loader.load_skills_callback)
sidebar_layout.addWidget(load_skills_button, 2, 0, alignment=AHCENTER)
- save_skills_button = create_button2(self.theme2, 'Save Skills')
+ save_skills_button = create_button2(self.theme, 'Save Skills')
save_skills_button.clicked.connect(self.build_loader.save_skills_callback)
sidebar_layout.addWidget(save_skills_button, 2, 1, alignment=AHCENTER)
sidebar_frame.setLayout(sidebar_layout)
@@ -1244,13 +1244,13 @@ def setup_ground_skill_frame(self):
Creates Ground skill GUI
"""
frame = self.tabbers.build_frames[3]
- isp = self.theme2['defaults']['isp'] * self.theme2.scale
- csp = self.theme2['defaults']['csp'] * self.theme2.scale
+ isp = self.theme['defaults']['isp'] * self.theme.scale
+ csp = self.theme['defaults']['csp'] * self.theme.scale
col_layout = GridLayout(margins=isp, spacing=csp)
col_layout.setRowStretch(0, 1)
col_layout.setColumnStretch(0, 3)
col_layout.setColumnStretch(2, 1)
- tree_frame = create_frame2(self.theme2, size_policy=SMINMIN)
+ tree_frame = create_frame2(self.theme, size_policy=SMINMIN)
col_layout.addWidget(tree_frame, 0, 0)
# skill tree
@@ -1297,11 +1297,11 @@ def setup_ground_skill_frame(self):
self.create_skill_button_ground(skills[9], 3, 1), 2, 0, alignment=ARIGHT)
tree_layout.addLayout(group_layout, 2, 2)
tree_frame.setLayout(tree_layout)
- seperator = create_frame2(self.theme2, size_policy=SMAXMIN, style_override={
+ seperator = create_frame2(self.theme, size_policy=SMAXMIN, style_override={
'background-color': '@sets'})
- seperator.setFixedWidth(self.theme2['defaults']['sep'] * self.theme2.scale)
+ seperator.setFixedWidth(self.theme['defaults']['sep'] * self.theme.scale)
col_layout.addWidget(seperator, 0, 1)
- bonus_bar_container = create_frame2(self.theme2, size_policy=SMINMIN)
+ bonus_bar_container = create_frame2(self.theme, size_policy=SMINMIN)
# bonus bars
bonus_bar_layout = GridLayout(margins=isp)
@@ -1313,15 +1313,15 @@ def setup_ground_skill_frame(self):
bonus_bar_layout.addWidget(seg1, row, 1, alignment=AHCENTER)
seg2 = self.create_bonus_bar_segment('ground', i * 2 + 1)
bonus_bar_layout.addWidget(seg2, row - 1, 1, alignment=AHCENTER)
- button = create_item_button2(self.theme2)
+ button = create_item_button2(self.theme)
button.clicked.connect(lambda i=i: self.build2.skill_unlock_callback('ground', i))
bonus_bar_layout.addWidget(button, row - 2, 1, alignment=AHCENTER)
self.build2.skills.unlocks['ground'][i] = button
row -= 3
- icon_label = create_label2(self.theme2, '', style='unlock_label')
- icon_label.setPixmap(self.theme2.icons['ground'])
+ icon_label = create_label2(self.theme, '', style='unlock_label')
+ icon_label.setPixmap(self.theme.icons['ground'])
bonus_bar_layout.addWidget(icon_label, 16, 1, alignment=AHCENTER)
- count_label = create_label2(self.theme2, '0', 'label_subhead')
+ count_label = create_label2(self.theme, '0', 'label_subhead')
bonus_bar_layout.addWidget(count_label, 17, 1, alignment=AHCENTER)
self.build2.skills.count_labels['ground'] = count_label
bonus_bar_container.setLayout(bonus_bar_layout)
@@ -1331,20 +1331,20 @@ def setup_ground_skill_frame(self):
# sidebar
sidebar_frame = self.tabbers.sidebar_frames[3]
sidebar_layout = GridLayout(margins=(csp, isp * 2, csp, csp), spacing=csp)
- desc_label = create_label2(self.theme2, 'Ground Skill Notes:')
+ desc_label = create_label2(self.theme, 'Ground Skill Notes:')
sidebar_layout.addWidget(desc_label, 0, 0, 1, 2)
desc_edit = QPlainTextEdit()
- desc_edit.setStyleSheet(self.theme2.get_style_class('QPlainTextEdit', 'textedit'))
- desc_edit.setFont(self.theme2.get_font('textedit'))
+ desc_edit.setStyleSheet(self.theme.get_style_class('QPlainTextEdit', 'textedit'))
+ desc_edit.setFont(self.theme.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
desc_edit.textChanged.connect(lambda: self.build2.set(
'ground', 'skill_desc', value=desc_edit.toPlainText(), autosave=False))
self.build2.skills.ground_desc = desc_edit
sidebar_layout.addWidget(desc_edit, 1, 0, 1, 2)
- load_skills_button = create_button2(self.theme2, 'Load Skills')
+ load_skills_button = create_button2(self.theme, 'Load Skills')
load_skills_button.clicked.connect(self.build_loader.load_skills_callback)
sidebar_layout.addWidget(load_skills_button, 2, 0, alignment=AHCENTER)
- save_skills_button = create_button2(self.theme2, 'Save Skills')
+ save_skills_button = create_button2(self.theme, 'Save Skills')
save_skills_button.clicked.connect(self.build_loader.save_skills_callback)
sidebar_layout.addWidget(save_skills_button, 2, 1, alignment=AHCENTER)
sidebar_frame.setLayout(sidebar_layout)
@@ -1361,10 +1361,10 @@ def setup_splash(self, frame: QFrame):
layout.setColumnStretch(2, 3)
loading_image = ImageLabel(self.app_dir2 / 'local' / 'sets_loading.png', (1, 1))
layout.addWidget(loading_image, 1, 1)
- loading_label = create_label2(self.theme2, 'Loading: ...', 'label_subhead')
+ loading_label = create_label2(self.theme, 'Loading: ...', 'label_subhead')
self.splash.loading_label = loading_label
layout.addWidget(loading_label, 2, 0, 1, 3, alignment=AHCENTER)
- progress_label = create_label2(self.theme2, '', 'label_subhead')
+ progress_label = create_label2(self.theme, '', 'label_subhead')
self.splash.progress_label = progress_label
layout.addWidget(progress_label, 3, 0, 1, 3, alignment=AHCENTER)
frame.setLayout(layout)
@@ -1383,11 +1383,11 @@ def setup_settings_frame(self):
Populates the settings frame.
"""
settings_frame = self.tabbers.build_frames[5]
- isp = self.theme2['defaults']['isp'] * self.theme2.scale
+ isp = self.theme['defaults']['isp'] * self.theme.scale
settings_layout = HBoxLayout(margins=(2 * isp, isp, isp, isp), spacing=isp)
scroll_layout = VBoxLayout(margins=(0, isp, 0, 0), spacing=isp)
scroll_layout.setSpacing(isp)
- scroll_frame = create_frame2(self.theme2)
+ scroll_frame = create_frame2(self.theme)
scroll_area = QScrollArea()
scroll_area.setSizePolicy(SMINMIN)
scroll_area.setHorizontalScrollBarPolicy(SCROLLOFF)
@@ -1396,56 +1396,56 @@ def setup_settings_frame(self):
settings_frame.setLayout(settings_layout)
# first section
- settings_header = create_label2(self.theme2, 'Settings:', 'label_heading')
+ settings_header = create_label2(self.theme, 'Settings:', 'label_heading')
scroll_layout.addWidget(settings_header, alignment=ALEFT)
sec_1 = GridLayout(spacing=isp)
sec_1.setColumnMinimumWidth(1, 3 * isp)
sec_1.setColumnMinimumWidth(2, 12 * isp)
sec_1.setColumnMinimumWidth(3, 3 * isp)
sec_1.setColumnStretch(5, 1)
- ui_scale_label = create_label2(self.theme2, 'UI Scale')
+ ui_scale_label = create_label2(self.theme, 'UI Scale')
sec_1.addWidget(ui_scale_label, 0, 0, alignment=ALEFT)
ui_scale_slider = create_annotated_slider2(
- self.theme2, default_value=round(self.settings.ui_scale * 50, 0), min=25, max=75,
+ self.theme, default_value=round(self.settings.ui_scale * 50, 0), min=25, max=75,
callback=self.settings.set_ui_scale)
sec_1.addLayout(ui_scale_slider, 0, 2, alignment=ALEFT)
- ui_scale_desc = create_label2(self.theme2, 'Requires restart.', 'hint_label')
+ ui_scale_desc = create_label2(self.theme, 'Requires restart.', 'hint_label')
sec_1.addWidget(ui_scale_desc, 0, 4, alignment=ALEFT)
- mark_label = create_label2(self.theme2, 'Default Mark')
+ mark_label = create_label2(self.theme, 'Default Mark')
sec_1.addWidget(mark_label, 1, 0, alignment=ALEFT)
- mark_combo = create_combo_box2(self.theme2, style_override={'font': '@small_text'})
+ mark_combo = create_combo_box2(self.theme, style_override={'font': '@small_text'})
mark_combo.addItems(('',) + MARKS)
mark_combo.setCurrentText(self.settings.default_mark)
mark_combo.currentTextChanged.connect(
lambda new_mark: self.settings.set('default_mark', new_mark))
sec_1.addWidget(mark_combo, 1, 2, alignment=ALEFT)
- rarity_label = create_label2(self.theme2, 'Default Rarity')
+ rarity_label = create_label2(self.theme, 'Default Rarity')
sec_1.addWidget(rarity_label, 2, 0, alignment=ALEFT)
- rarity_combo = create_combo_box2(self.theme2, style_override={'font': '@small_text'})
+ rarity_combo = create_combo_box2(self.theme, style_override={'font': '@small_text'})
rarity_combo.addItems(RARITIES.keys())
rarity_combo.setCurrentText(self.settings.default_rarity)
rarity_combo.currentTextChanged.connect(
lambda new_rarity: self.settings.set('default_rarity', new_rarity))
sec_1.addWidget(rarity_combo, 2, 2, alignment=ALEFT | AVCENTER)
- picker_rel_label = create_label2(self.theme2, 'Picker Position')
+ picker_rel_label = create_label2(self.theme, 'Picker Position')
sec_1.addWidget(picker_rel_label, 3, 0, alignment=ALEFT)
- picker_rel_combo = create_combo_box2(self.theme2, style_override={'font': '@small_text'})
+ picker_rel_combo = create_combo_box2(self.theme, style_override={'font': '@small_text'})
picker_rel_combo.addItems(('Absolute', 'Relative'))
picker_rel_combo.setCurrentIndex(self.settings.picker_relative)
picker_rel_combo.currentIndexChanged.connect(
lambda new_i: self.settings.set('picker_relative', new_i))
sec_1.addWidget(picker_rel_combo, 3, 2, alignment=ALEFT | AVCENTER)
- picker_rel_label = create_label2(self.theme2, 'Default Save Format')
+ picker_rel_label = create_label2(self.theme, 'Default Save Format')
sec_1.addWidget(picker_rel_label, 4, 0, alignment=ALEFT)
- picker_rel_combo = create_combo_box2(self.theme2, style_override={'font': '@small_text'})
+ picker_rel_combo = create_combo_box2(self.theme, style_override={'font': '@small_text'})
picker_rel_combo.addItems(('JSON', 'PNG'))
picker_rel_combo.setCurrentText(self.settings.default_save_format)
picker_rel_combo.currentTextChanged.connect(
lambda new_t: self.settings.set('default_save_format', new_t))
sec_1.addWidget(picker_rel_combo, 4, 2, alignment=ALEFT | AVCENTER)
- backup_label = create_label2(self.theme2, 'Preferred Backup')
+ backup_label = create_label2(self.theme, 'Preferred Backup')
sec_1.addWidget(backup_label, 5, 0, alignment=ALEFT)
- backup_combo = create_combo_box2(self.theme2, style_override={'font': '@small_text'})
+ backup_combo = create_combo_box2(self.theme, style_override={'font': '@small_text'})
backup_combo.addItems(('Auto', 'Manual'))
backup_combo.setCurrentIndex(self.settings.pref_backup)
backup_combo.currentIndexChanged.connect(
@@ -1454,50 +1454,50 @@ def setup_settings_frame(self):
scroll_layout.addLayout(sec_1)
# second section
- sep = create_frame2(self.theme2)
+ sep = create_frame2(self.theme)
sep.setFixedHeight(isp)
scroll_layout.addWidget(sep)
- maintenance_header = create_label2(self.theme2, 'Maintenance:', 'label_heading')
+ maintenance_header = create_label2(self.theme, 'Maintenance:', 'label_heading')
scroll_layout.addWidget(maintenance_header, alignment=ALEFT)
sec_2 = GridLayout(spacing=isp)
sec_2.setColumnMinimumWidth(1, 3 * isp)
sec_2.setColumnStretch(3, 1)
- cargo_clear_button = create_button2(self.theme2, 'Clear Cargo Data')
+ cargo_clear_button = create_button2(self.theme, 'Clear Cargo Data')
cargo_clear_button.clicked.connect(
lambda: delete_folder_contents(self.config.config_subfolders['cargo']))
sec_2.addWidget(cargo_clear_button, 0, 0, alignment=ALEFT)
cargo_clear_label = create_label2(
- self.theme2, 'Clears cargo data. Restart to refresh data.', 'hint_label')
+ self.theme, 'Clears cargo data. Restart to refresh data.', 'hint_label')
sec_2.addWidget(cargo_clear_label, 0, 2, alignment=ALEFT)
- cache_clear_button = create_button2(self.theme2, 'Clear Cache')
+ cache_clear_button = create_button2(self.theme, 'Clear Cache')
cache_clear_button.clicked.connect(
lambda: delete_folder_contents(self.config.config_subfolders['cache']))
sec_2.addWidget(cache_clear_button, 1, 0, alignment=ALEFT)
cache_clear_label = create_label2(
- self.theme2, 'Clears cache. Restart to rebuild cache.', 'hint_label')
+ self.theme, 'Clears cache. Restart to rebuild cache.', 'hint_label')
sec_2.addWidget(cache_clear_label, 1, 2, alignment=ALEFT)
- backup_cargo_button = create_button2(self.theme2, 'Backup Cargo Data')
+ backup_cargo_button = create_button2(self.theme, 'Backup Cargo Data')
backup_cargo_button.clicked.connect(self.cargo.backup_cargo_data)
sec_2.addWidget(backup_cargo_button, 2, 0, alignment=ALEFT)
backup_cargo_label = create_label2(
- self.theme2, 'Creates cargo backup to protect against download failures.', 'hint_label')
+ self.theme, 'Creates cargo backup to protect against download failures.', 'hint_label')
sec_2.addWidget(backup_cargo_label, 2, 2, alignment=ALEFT)
scroll_layout.addLayout(sec_2)
# third section
- sep = create_frame2(self.theme2)
+ sep = create_frame2(self.theme)
sep.setFixedHeight(isp)
scroll_layout.addWidget(sep)
- compatibility_header = create_label2(self.theme2, 'Compatibility:', 'label_heading')
+ compatibility_header = create_label2(self.theme, 'Compatibility:', 'label_heading')
scroll_layout.addWidget(compatibility_header, alignment=ALEFT)
sec_3 = GridLayout(spacing=isp)
sec_3.setColumnMinimumWidth(1, 3 * isp)
sec_3.setColumnStretch(3, 1)
- build_image_button = create_button2(self.theme2, 'Convert Legacy Build Image')
+ build_image_button = create_button2(self.theme, 'Convert Legacy Build Image')
build_image_button.clicked.connect(self.build_loader.load_legacy_build_image)
sec_3.addWidget(build_image_button, 0, 0, alignment=ALEFT)
build_image_label = create_label2(
- self.theme2, 'Loads build from legacy build image. Use the "Load" button to load '
+ self.theme, 'Loads build from legacy build image. Use the "Load" button to load '
'legacy JSON build files.', 'hint_label')
sec_3.addWidget(build_image_label, 0, 2, alignment=ALEFT)
scroll_layout.addLayout(sec_3)
@@ -1507,13 +1507,13 @@ def setup_settings_frame(self):
# sidebar
sidebar_frame = self.tabbers.sidebar_frames[5]
- csp = self.theme2['defaults']['csp'] * self.theme2.scale
+ csp = self.theme['defaults']['csp'] * self.theme.scale
sidebar_layout = VBoxLayout(margins=csp, spacing=isp)
sidebar_layout.setAlignment(ATOP)
sidebar_layout.addWidget(
- create_label2(self.theme2, 'About SETS:', 'label_heading'), alignment=ALEFT)
+ create_label2(self.theme, 'About SETS:', 'label_heading'), alignment=ALEFT)
about_label = create_label2(
- self.theme2, 'Thank you for using the STO Equipment and Trait Selector (SETS)! Make '
+ self.theme, 'Thank you for using the STO Equipment and Trait Selector (SETS)! Make '
'sure to check out other projects of the STO Community Developers on our Github page '
'and contact us on Discord for support.')
about_label.setWordWrap(True)
@@ -1531,12 +1531,12 @@ def setup_settings_frame(self):
'callback': lambda: open_url(self.config.link_downloads), 'align': AHCENTER}
}
button_layout, buttons = create_button_series2(
- self.theme2, link_button_style, 'button', shape='column', ret=True)
+ self.theme, link_button_style, 'button', shape='column', ret=True)
buttons[0].setToolTip(self.config.link_website)
buttons[1].setToolTip(self.config.link_github)
buttons[2].setToolTip(self.config.link_discord)
buttons[3].setToolTip(self.config.link_downloads)
- link_button_frame = create_frame2(self.theme2)
+ link_button_frame = create_frame2(self.theme)
link_button_frame.setLayout(button_layout)
sidebar_layout.addWidget(link_button_frame, alignment=AHCENTER)
sidebar_frame.setLayout(sidebar_layout)
@@ -1544,9 +1544,9 @@ def setup_settings_frame(self):
footer_frame = self.tabbers.character_frames[2]
footer_layout = GridLayout(margins=csp, spacing=isp)
version_label = create_label2(
- self.theme2, f"Version: {self.version}", 'hint_label')
+ self.theme, f"Version: {self.version}", 'hint_label')
footer_layout.addWidget(version_label, 0, 0, alignment=ALEFT | ABOTTOM)
- stocd_label = create_label2(self.theme2, '')
- stocd_label.setPixmap(self.theme2.icons['STOCD'])
+ stocd_label = create_label2(self.theme, '')
+ stocd_label.setPixmap(self.theme.icons['STOCD'])
footer_layout.addWidget(stocd_label, 0, 1, alignment=ARIGHT | ABOTTOM)
footer_frame.setLayout(footer_layout)
From 6336101ecf943c65a9c09ab62a698dbc59ae8be1 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 10:25:20 +0200
Subject: [PATCH 26/44] added alt images to update build version
---
src/buildloader.py | 9 ++++++++-
src/widgets.py | 14 +++++++++++++-
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/src/buildloader.py b/src/buildloader.py
index 00436ea..cbc0370 100644
--- a/src/buildloader.py
+++ b/src/buildloader.py
@@ -14,7 +14,7 @@
from .config import SETSConfig, SETSSettings
from .constants import BUILD_CONVERSION, BUILD_VERSION, SETS_FILE_FILTER
from .iofunc import browse_path, load_json__new, store_json__new
-from .widgets import pixel_range
+from .widgets import bundle, pixel_range
class BuildLoader():
@@ -218,6 +218,13 @@ def _fix_boff_seat(environment):
spec = build['ground']['boff_specs'][station_id]
_fix_boff_seat('ground')
+ alt_images_inverted = {image: key for key, image in self._cargo.alt_images.items()}
+ alt_image_items = bundle(
+ build['space']['traits'], build['ground']['traits'], build['ground']['rep_traits'])
+ for trait in alt_image_items:
+ if isinstance(trait, dict) and trait['item'] in alt_images_inverted:
+ trait['item'] = alt_images_inverted[trait['item']].split('__', 1)[0]
+
build['_version'] = BUILD_VERSION
def remove_invalid_build_items(self, build: dict[str, int | dict[str]]):
diff --git a/src/widgets.py b/src/widgets.py
index dc8fa3a..d9b497c 100644
--- a/src/widgets.py
+++ b/src/widgets.py
@@ -1,6 +1,6 @@
from collections import namedtuple
from pathlib import Path
-from typing import Callable
+from typing import Callable, Generator, Iterable
from PySide6.QtCore import QEvent, QPoint, QRect, QSize, Qt, QThread, Signal, Slot
from PySide6.QtGui import (
@@ -387,6 +387,18 @@ def __iter__(self):
return self.__gen
+def bundle[_T](*iterables: Iterable[_T]) -> Generator[_T, None, None]:
+ """
+ Generator yielding the items of the given iterables in the order they were provided.
+
+ Parameters:
+ - :param iterables: iterables to be bundled
+ """
+ for inner_iterable in iterables:
+ for element in inner_iterable:
+ yield element
+
+
class TooltipLabel(QLabel):
"""Label with tooltip"""
def __init__(self, text: str, tooltip: QLabel):
From 3a7892389d59fa01bf363d4b3a326e119d96c263 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 11:13:39 +0200
Subject: [PATCH 27/44] renaming build2 to build
---
src/app.py | 162 ++++++++++++++++++++++++++---------------------------
1 file changed, 81 insertions(+), 81 deletions(-)
diff --git a/src/app.py b/src/app.py
index f0ebff9..7ee1774 100644
--- a/src/app.py
+++ b/src/app.py
@@ -69,7 +69,7 @@ def __init__(self, args, app_dir_path: str, version: str):
Path(self.config.config_subfolders['images']),
Path(self.config.config_subfolders['ship_images']),
self.app_dir2, self.cargo, self.downloader)
- self.build2: BuildManager = BuildManager(
+ self.build: BuildManager = BuildManager(
self.cargo, self.images, self.config.autosave_path, self.theme.tooltips)
self.splash: SplashScreen = SplashScreen()
self.tabbers: Tabbers = Tabbers()
@@ -77,15 +77,15 @@ def __init__(self, args, app_dir_path: str, version: str):
self.cache_icons()
self.cargo.load_static_data()
self.build_loader: BuildLoader = BuildLoader(
- self.build2, self.cargo, self.config, self.settings, self.window)
- self.export_window = ExportWindow(self.theme, self.window, self.build2, self.cargo)
+ self.build, self.cargo, self.config, self.settings, self.window)
+ self.export_window = ExportWindow(self.theme, self.window, self.build, self.cargo)
self.picker_window: Picker = Picker(self.theme, self.window, self.settings, self.images)
- self.picker_window.dialog_result.connect(self.build2.handle_picker_result)
+ self.picker_window.dialog_result.connect(self.build.handle_picker_result)
self.edit_window: ItemEditor = ItemEditor(self.theme, self.window)
- self.edit_window.dialog_result.connect(self.build2.finish_item_edit)
+ self.edit_window.dialog_result.connect(self.build.finish_item_edit)
self.ship_selector_window: ShipSelector = ShipSelector(self.theme, self.window)
- self.ship_selector_window.dialog_result.connect(self.build2.finish_ship_pick)
- self.context_menu: ContextMenu = ContextMenu(self.theme, self.build2, self.cargo)
+ self.ship_selector_window.dialog_result.connect(self.build.finish_ship_pick)
+ self.context_menu: ContextMenu = ContextMenu(self.theme, self.build, self.cargo)
self.context_menu.edit_slot.connect(self.edit_window.edit_item)
self.setup_main_layout()
self.window.show()
@@ -223,7 +223,7 @@ def main_window_close_callback(self, event: QCloseEvent):
"""
window_geometry = self.window.saveGeometry()
self.settings.state__geometry = window_geometry
- self.build2.autosave()
+ self.build.autosave()
self.settings.store_settings()
event.accept()
@@ -259,15 +259,15 @@ def init_ui(self):
"""
self.ship_selector_window.set_ships(self.cargo.ships.keys())
space_doff_specs = [''] + sorted(self.cargo.space_doffs.keys())
- for combobox in self.build2.space.doffs_spec:
+ for combobox in self.build.space.doffs_spec:
combobox.addItems(space_doff_specs)
ground_doff_specs = [''] + sorted(self.cargo.ground_doffs.keys())
- for combobox in self.build2.ground.doffs_spec:
+ for combobox in self.build.ground.doffs_spec:
combobox.addItems(ground_doff_specs)
- for career_block in self.build2.skills.space.values():
+ for career_block in self.build.skills.space.values():
for skill_button in career_block:
skill_button.set_item(self.images.get(skill_button.skill_image_name))
- for skill_group in self.build2.skills.ground:
+ for skill_group in self.build.skills.ground:
for skill_button in skill_group:
skill_button.set_item(self.images.get(skill_button.skill_image_name))
@@ -293,12 +293,12 @@ def picker(
modifiers = self.cargo.modifiers[build_key]
elif build_key == 'boffs':
if environment == 'space':
- profession, specialization = self.build2['space']['boff_specs'][boff_id]
+ profession, specialization = self.build['space']['boff_specs'][boff_id]
if specialization == 'Temporal Operative':
specialization = 'Temporal'
else:
- profession = self.build2['ground']['boff_profs'][boff_id]
- specialization = self.build2['ground']['boff_specs'][boff_id]
+ profession = self.build['ground']['boff_profs'][boff_id]
+ specialization = self.build['ground']['boff_specs'][boff_id]
items = self.cargo.boff_abilities[environment][profession][build_subkey]
if specialization != '':
items = items + self.cargo.boff_abilities[environment][specialization][build_subkey]
@@ -324,7 +324,7 @@ def setup_main_layout(self):
"""
Creates the main layout and places it into the main window.
"""
- self.build2._building = True
+ self.build._building = True
# master layout: banner, borders and splash screen
layout = VBoxLayout()
background_frame = create_frame2(
@@ -362,9 +362,9 @@ def setup_main_layout(self):
left_button_group = {
'Save': {'callback': self.build_loader.save_build_callback},
'Open': {'callback': self.build_loader.load_build_callback},
- 'Clear Current Tab': {'callback': lambda: self.build2.clear_build_callback(
+ 'Clear Current Tab': {'callback': lambda: self.build.clear_build_callback(
self.tabbers.build_tabber.currentIndex())},
- 'Clear All Tabs': {'callback': self.build2.clear_all}
+ 'Clear All Tabs': {'callback': self.build.clear_all}
}
menu_layout.addLayout(
create_button_series2(self.theme, left_button_group), 0, 0, alignment=ALEFT | ATOP)
@@ -452,7 +452,7 @@ def setup_main_layout(self):
self.setup_settings_frame()
content_frame.setLayout(content_layout)
- self.build2._building = False
+ self.build._building = False
def setup_ship_frame(self):
"""
@@ -466,7 +466,7 @@ def setup_ship_frame(self):
image_layout = GridLayout()
ship_image = ShipImage()
ship_image.setSizePolicy(SMINMIN)
- self.build2.ship.image = ship_image
+ self.build.ship.image = ship_image
image_layout.addWidget(ship_image, 0, 0)
image_frame.setLayout(image_layout)
layout.addWidget(image_frame, stretch=1)
@@ -481,14 +481,14 @@ def setup_ship_frame(self):
self.theme.get_style_class('ShipButton', 'button', override={'margin': 0}))
ship_selector.setFont(self.theme.get_font(font_spec='@subhead'))
ship_selector.clicked.connect(self.ship_selector_window.pick_ship)
- self.build2.ship.button = ship_selector
+ self.build.ship.button = ship_selector
ship_layout.addWidget(ship_selector, 0, 0, 1, 4, alignment=ATOP)
tier_label = create_label2(self.theme, 'Ship Tier:')
ship_layout.addWidget(tier_label, 1, 0)
tier_combo = create_combo_box2(self.theme)
- tier_combo.currentTextChanged.connect(self.build2.tier_callback)
+ tier_combo.currentTextChanged.connect(self.build.tier_callback)
tier_combo.setSizePolicy(SMAXMAX)
- self.build2.ship.tier = tier_combo
+ self.build.ship.tier = tier_combo
ship_layout.addWidget(tier_combo, 1, 1, alignment=ALEFT)
dc_tooltip = create_label2(self.theme, 'Can equip Dual Cannons', 'label_tooltip')
dc_label = TooltipLabel('', dc_tooltip)
@@ -496,17 +496,17 @@ def setup_ship_frame(self):
dc_label_size_policy = dc_label.sizePolicy()
dc_label_size_policy.setRetainSizeWhenHidden(True)
dc_label.setSizePolicy(dc_label_size_policy)
- self.build2.ship.dc = dc_label
+ self.build.ship.dc = dc_label
ship_layout.addWidget(dc_label, 1, 2, alignment=ARIGHT)
info_button = create_button2(self.theme, 'Ship Info', style_override={'margin': 0})
- info_button.clicked.connect(self.build2.ship_info_callback)
+ info_button.clicked.connect(self.build.ship_info_callback)
ship_layout.addWidget(info_button, 1, 3, alignment=ARIGHT)
name_label = create_label2(self.theme, 'Ship Name:')
ship_layout.addWidget(name_label, 2, 0)
name_entry = create_entry2(self.theme)
name_entry.editingFinished.connect(
- lambda: self.build2.set('space', 'ship_name', value=name_entry.text()))
- self.build2.ship.name = name_entry
+ lambda: self.build.set('space', 'ship_name', value=name_entry.text()))
+ self.build.ship.name = name_entry
name_entry.setSizePolicy(SMINMAX)
ship_layout.addWidget(name_entry, 2, 1, 1, 3)
desc_label = create_label2(self.theme, 'Build Description:')
@@ -516,9 +516,9 @@ def setup_ship_frame(self):
desc_edit.setStyleSheet(self.theme.get_style_class('QPlainTextEdit', 'textedit'))
desc_edit.setFont(self.theme.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
- desc_edit.textChanged.connect(lambda: self.build2.set(
+ desc_edit.textChanged.connect(lambda: self.build.set(
'space', 'ship_desc', value=desc_edit.toPlainText(), autosave=False))
- self.build2.ship.desc = desc_edit
+ self.build.ship.desc = desc_edit
ship_layout.addWidget(desc_edit, 4, 0, 1, 4)
ship_frame.setLayout(ship_layout)
layout.addWidget(ship_frame, stretch=2)
@@ -544,7 +544,7 @@ def create_build_section(
label_size_policy.setRetainSizeWhenHidden(True)
label.setSizePolicy(label_size_policy)
layout.addWidget(label, 0, 0, 1, button_count, alignment=ALEFT)
- widget_storage = self.build2.space if environment == 'space' else self.build2.ground
+ widget_storage = self.build.space if environment == 'space' else self.build.ground
if label_store != '':
setattr(widget_storage, label_store, label)
for i in range(button_count):
@@ -579,7 +579,7 @@ def create_boff_station_space(
)
else:
label_options = (profession + specialization,)
- widget_storage = self.build2.space
+ widget_storage = self.build.space
label_layout = HBoxLayout(spacing=self.config.ui_scale * 3)
icon_label = TooltipLabel('', create_label2(self.theme, '', 'label_tooltip'))
widget_storage.boff_label_icons[boff_id] = icon_label
@@ -588,7 +588,7 @@ def create_boff_station_space(
label = create_combo_box2(
self.theme, size_policy=SMAXMAX, style_override=self.theme['boff_combo'])
label.currentTextChanged.connect(
- lambda new: self.build2.boff_profession_callback_space(boff_id, new))
+ lambda new: self.build.boff_profession_callback_space(boff_id, new))
label.addItems(label_options)
label_size_policy = label.sizePolicy()
label_size_policy.setRetainSizeWhenHidden(True)
@@ -614,20 +614,20 @@ def create_boff_station_ground(self, boff_id: int) -> VBoxLayout:
Parameters:
- :param boff_id: identifies the boff station
"""
- widget_storage = self.build2.ground
+ widget_storage = self.build.ground
m = self.theme['defaults']['margin'] * self.theme.scale
layout = VBoxLayout(spacing=m)
label_layout = HBoxLayout(spacing=m)
label_layout.setAlignment(ALEFT)
prof_label = create_combo_box2(self.theme, style_override=self.theme['boff_combo'])
prof_label.currentTextChanged.connect(
- lambda new: self.build2.boff_label_callback_ground(boff_id, 'boff_profs', new))
+ lambda new: self.build.boff_label_callback_ground(boff_id, 'boff_profs', new))
prof_label.addItems(CAREERS)
widget_storage.boff_profs[boff_id] = prof_label
label_layout.addWidget(prof_label)
spec_label = create_combo_box2(self.theme, style_override=self.theme['boff_combo'])
spec_label.currentTextChanged.connect(
- lambda new: self.build2.boff_label_callback_ground(boff_id, 'boff_specs', new))
+ lambda new: self.build.boff_label_callback_ground(boff_id, 'boff_specs', new))
spec_label.addItems(GROUND_BOFF_SPECS)
widget_storage.boff_specs[boff_id] = spec_label
label_layout.addWidget(spec_label)
@@ -656,7 +656,7 @@ def create_personal_trait_section(self, environment: str) -> GridLayout:
label = create_label2(
self.theme, 'Personal Traits', style_override={'margin': (0, 0, 6, 0)})
layout.addWidget(label, 0, 0, 1, 4, alignment=ALEFT)
- widget_storage = self.build2.space if environment == 'space' else self.build2.ground
+ widget_storage = self.build.space if environment == 'space' else self.build.ground
for row in range(3):
for col in range(4):
i = row * 4 + col
@@ -681,7 +681,7 @@ def create_starship_trait_section(self) -> GridLayout:
self.theme, 'Starship Traits', style_override={'margin': (0, 0, 6, 0)})
label.sizePolicy().setRetainSizeWhenHidden(True)
layout.addWidget(label, 0, 0, 1, 4, alignment=ALEFT)
- widget_storage = self.build2.space
+ widget_storage = self.build.space
for col in range(5):
button = create_item_button2(self.theme)
button.sizePolicy().setRetainSizeWhenHidden(True)
@@ -711,17 +711,17 @@ def create_doff_section(self, environment: str) -> GridLayout:
"""
doff_layout = GridLayout(spacing=self.theme['defaults']['bw'] * self.theme.scale)
doff_layout.setColumnStretch(1, 1)
- widget_storage = self.build2.space if environment == 'space' else self.build2.ground
+ widget_storage = self.build.space if environment == 'space' else self.build.ground
for i in range(6):
spec_combo = create_combo_box2(self.theme, style_override=self.theme['doff_combo'])
spec_combo.currentTextChanged.connect(
- lambda spec, id=i: self.build2.doff_spec_callback(spec, environment, id))
+ lambda spec, id=i: self.build.doff_spec_callback(spec, environment, id))
doff_layout.addWidget(spec_combo, i, 0)
widget_storage.doffs_spec[i] = spec_combo
variant_combo = create_combo_box2(
self.theme, style_override=self.theme['doff_combo'], class_=DoffCombobox)
variant_combo.currentTextChanged.connect(
- lambda variant, id=i: self.build2.doff_variant_callback(variant, environment, id))
+ lambda variant, id=i: self.build.doff_variant_callback(variant, environment, id))
doff_layout.addWidget(variant_combo, i, 1)
widget_storage.doffs_variant[i] = variant_combo
return doff_layout
@@ -740,40 +740,40 @@ def create_skill_group_space(self, group_data: dict, id_offset: int) -> GridLayo
for index, node in enumerate(group_data['nodes']):
button = create_item_button2(self.theme)
skill_id = id_offset + index
- button.clicked.connect(lambda id=skill_id: self.build2.skill_callback_space(
+ button.clicked.connect(lambda id=skill_id: self.build.skill_callback_space(
group_data['career'], id, 'column'))
button.skill_image_name = node['image']
button.tooltip = format_skill_tooltip(
group_data['skill'], group_data, index, 'space', self.theme.tooltips)
- self.build2.skills.space[group_data['career']][id_offset + index] = button
+ self.build.skills.space[group_data['career']][id_offset + index] = button
layout.addWidget(button, index, 0)
# == 'pair+1': one skill with 2 ranks and one sub-skill with 1 rank
# == 'separate': 3 separate skills
else:
button = create_item_button2(self.theme)
- button.clicked.connect(lambda id=id_offset: self.build2.skill_callback_space(
+ button.clicked.connect(lambda id=id_offset: self.build.skill_callback_space(
group_data['career'], id, group_data['grouping']))
button.skill_image_name = group_data['nodes'][0]['image']
button.tooltip = format_skill_tooltip(
group_data['skill'][0], group_data, 0, 'space', self.theme.tooltips)
layout.addWidget(button, 0, 0, 1, 2, alignment=AHCENTER | ABOTTOM)
- self.build2.skills.space[group_data['career']][id_offset] = button
+ self.build.skills.space[group_data['career']][id_offset] = button
button = create_item_button2(self.theme)
- button.clicked.connect(lambda id=id_offset + 1: self.build2.skill_callback_space(
+ button.clicked.connect(lambda id=id_offset + 1: self.build.skill_callback_space(
group_data['career'], id, group_data['grouping']))
button.skill_image_name = group_data['nodes'][1]['image']
button.tooltip = format_skill_tooltip(
group_data['skill'][1], group_data, 1, 'space', self.theme.tooltips)
layout.addWidget(button, 1, 0, alignment=ATOP)
- self.build2.skills.space[group_data['career']][id_offset + 1] = button
+ self.build.skills.space[group_data['career']][id_offset + 1] = button
button = create_item_button2(self.theme)
- button.clicked.connect(lambda id=id_offset + 2: self.build2.skill_callback_space(
+ button.clicked.connect(lambda id=id_offset + 2: self.build.skill_callback_space(
group_data['career'], id, group_data['grouping']))
button.skill_image_name = group_data['nodes'][2]['image']
button.tooltip = format_skill_tooltip(
group_data['skill'][2], group_data, 2, 'space', self.theme.tooltips)
layout.addWidget(button, 1, 1, alignment=ATOP)
- self.build2.skills.space[group_data['career']][id_offset + 2] = button
+ self.build.skills.space[group_data['career']][id_offset + 2] = button
return layout
def create_bonus_bar_segment(
@@ -793,7 +793,7 @@ def create_bonus_bar_segment(
seg.setCheckable(True)
seg.setStyleSheet(self.theme.get_style_class('QPushButton', style, style_override))
seg.setFixedSize(7 * self.theme.scale, 17 * self.theme.scale)
- self.build2.skills.bonus_bars[bar][index] = seg
+ self.build.skills.bonus_bars[bar][index] = seg
return seg
def create_bonus_bar_space(self, career: str, layout: GridLayout, column: int):
@@ -811,9 +811,9 @@ def create_bonus_bar_space(self, career: str, layout: GridLayout, column: int):
if row % 6 == 0:
button = create_item_button2(self.theme)
button.clicked.connect(
- lambda i=button_index: self.build2.skill_unlock_callback(career, i))
+ lambda i=button_index: self.build.skill_unlock_callback(career, i))
layout.addWidget(button, row, column, alignment=AHCENTER)
- self.build2.skills.unlocks[career][button_index] = button
+ self.build.skills.unlocks[career][button_index] = button
button_index += 1
else:
segment = self.create_bonus_bar_segment(career, segment_index)
@@ -824,9 +824,9 @@ def create_bonus_bar_space(self, career: str, layout: GridLayout, column: int):
layout.addWidget(segment, row, column, alignment=AHCENTER)
segment_index += 1
button = create_item_button2(self.theme)
- button.clicked.connect(lambda: self.build2.skill_unlock_callback(career, 4))
+ button.clicked.connect(lambda: self.build.skill_unlock_callback(career, 4))
layout.addWidget(button, 1, column, alignment=AHCENTER)
- self.build2.skills.unlocks[career][4] = button
+ self.build.skills.unlocks[career][4] = button
def create_skill_button_ground(self, group_data: dict, id: int, node_id: int) -> ItemButton:
"""
@@ -838,12 +838,12 @@ def create_skill_button_ground(self, group_data: dict, id: int, node_id: int) ->
- :param node_id: 0 or 1 for first or second node
"""
button = create_item_button2(self.theme)
- button.clicked.connect(lambda: self.build2.skill_callback_ground(group_data['tree'], id))
+ button.clicked.connect(lambda: self.build.skill_callback_ground(group_data['tree'], id))
button.skill_image_name = group_data['nodes'][node_id]['image']
button.tooltip = format_skill_tooltip(
group_data['nodes'][node_id]['name'], group_data, node_id, 'ground',
self.theme.tooltips)
- self.build2.skills.ground[group_data['tree']][id] = button
+ self.build.skills.ground[group_data['tree']][id] = button
return button
def setup_space_build_frame(self):
@@ -1054,9 +1054,9 @@ def setup_ground_build_frame(self):
desc_edit.setStyleSheet(self.theme.get_style_class('QPlainTextEdit', 'textedit'))
desc_edit.setFont(self.theme.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
- desc_edit.textChanged.connect(lambda: self.build2.set(
+ desc_edit.textChanged.connect(lambda: self.build.set(
'ground', 'ground_desc', value=desc_edit.toPlainText(), autosave=False))
- self.build2.ground.desc = desc_edit
+ self.build.ground.desc = desc_edit
sidebar_layout.addWidget(desc_edit, 1, 0)
sidebar_frame.setLayout(sidebar_layout)
@@ -1075,54 +1075,54 @@ def setup_character_frame(self, frame: QFrame):
char_name.setAlignment(AHCENTER)
char_name.setSizePolicy(SMINMAX)
char_name.editingFinished.connect(
- lambda: self.build2.set('captain', 'name', value=char_name.text()))
+ lambda: self.build.set('captain', 'name', value=char_name.text()))
layout.addWidget(char_name, 1, 0, 1, 2)
- self.build2.character.name = char_name
+ self.build.character.name = char_name
elite_label = create_label2(self.theme, 'Elite Captain')
layout.addWidget(elite_label, 2, 0, alignment=ARIGHT)
elite_checkbox = create_checkbox2(self.theme)
- elite_checkbox.checkStateChanged.connect(self.build2.elite_callback)
+ elite_checkbox.checkStateChanged.connect(self.build.elite_callback)
layout.addWidget(elite_checkbox, 2, 1, alignment=ALEFT)
- self.build2.character.elite = elite_checkbox
+ self.build.character.elite = elite_checkbox
career_label = create_label2(self.theme, 'Captain Career')
layout.addWidget(career_label, 3, 0, alignment=ARIGHT)
career_combo = create_combo_box2(self.theme)
career_combo.addItems({''} | CAREERS)
career_combo.currentTextChanged.connect(
- lambda new_career: self.build2.set('captain', 'career', value=new_career))
+ lambda new_career: self.build.set('captain', 'career', value=new_career))
layout.addWidget(career_combo, 3, 1)
- self.build2.character.career = career_combo
+ self.build.character.career = career_combo
faction_label = create_label2(self.theme, 'Faction')
layout.addWidget(faction_label, 4, 0, alignment=ARIGHT)
faction_combo = create_combo_box2(self.theme)
faction_combo.addItems({''} | FACTIONS)
- faction_combo.currentTextChanged.connect(self.build2.faction_combo_callback)
+ faction_combo.currentTextChanged.connect(self.build.faction_combo_callback)
layout.addWidget(faction_combo, 4, 1)
- self.build2.character.faction = faction_combo
+ self.build.character.faction = faction_combo
species_label = create_label2(self.theme, 'Species')
layout.addWidget(species_label, 5, 0, alignment=ARIGHT)
species_combo = create_combo_box2(self.theme)
species_combo.addItems({''})
- species_combo.currentTextChanged.connect(self.build2.species_combo_callback)
+ species_combo.currentTextChanged.connect(self.build.species_combo_callback)
layout.addWidget(species_combo, 5, 1)
- self.build2.character.species = species_combo
+ self.build.character.species = species_combo
primary_label = create_label2(self.theme, 'Primary Spec')
layout.addWidget(primary_label, 6, 0, alignment=ARIGHT)
primary_combo = create_combo_box2(self.theme)
primary_combo.addItems({''} | PRIMARY_SPECS)
primary_combo.currentTextChanged.connect(
- lambda new_spec: self.build2.spec_combo_callback(True, new_spec))
+ lambda new_spec: self.build.spec_combo_callback(True, new_spec))
layout.addWidget(primary_combo, 6, 1)
- self.build2.character.primary = primary_combo
+ self.build.character.primary = primary_combo
secondary_label = create_label2(
self.theme, 'Secondary Spec', style_override={'margin-bottom': 0})
layout.addWidget(secondary_label, 7, 0, alignment=ARIGHT)
secondary_combo = create_combo_box2(self.theme)
secondary_combo.addItems({''} | PRIMARY_SPECS | SECONDARY_SPECS)
secondary_combo.currentTextChanged.connect(
- lambda new_spec: self.build2.spec_combo_callback(False, new_spec))
+ lambda new_spec: self.build.spec_combo_callback(False, new_spec))
layout.addWidget(secondary_combo, 7, 1)
- self.build2.character.secondary = secondary_combo
+ self.build.character.secondary = secondary_combo
frame.setLayout(layout)
def setup_space_skill_frame(self):
@@ -1199,21 +1199,21 @@ def setup_space_skill_frame(self):
bonus_bar_layout.addWidget(eng_label, 30, 1, alignment=AHCENTER)
eng_count = create_label2(self.theme, '0', 'label_subhead')
bonus_bar_layout.addWidget(eng_count, 31, 1, alignment=AHCENTER)
- self.build2.skills.count_labels['eng'] = eng_count
+ self.build.skills.count_labels['eng'] = eng_count
self.create_bonus_bar_space('sci', bonus_bar_layout, 2)
sci_label = create_label2(self.theme, '', style='unlock_label')
sci_label.setPixmap(self.theme.icons['sci'])
bonus_bar_layout.addWidget(sci_label, 30, 2, alignment=AHCENTER)
sci_count = create_label2(self.theme, '0', 'label_subhead')
bonus_bar_layout.addWidget(sci_count, 31, 2, alignment=AHCENTER)
- self.build2.skills.count_labels['sci'] = sci_count
+ self.build.skills.count_labels['sci'] = sci_count
self.create_bonus_bar_space('tac', bonus_bar_layout, 3)
tac_label = create_label2(self.theme, '', style='unlock_label')
tac_label.setPixmap(self.theme.icons['tac'])
bonus_bar_layout.addWidget(tac_label, 30, 3, alignment=AHCENTER)
tac_count = create_label2(self.theme, '0', 'label_subhead')
bonus_bar_layout.addWidget(tac_count, 31, 3, alignment=AHCENTER)
- self.build2.skills.count_labels['tac'] = tac_count
+ self.build.skills.count_labels['tac'] = tac_count
bonus_bar_container.setLayout(bonus_bar_layout)
col_layout.addWidget(bonus_bar_container, 0, 2)
frame.setLayout(col_layout)
@@ -1227,9 +1227,9 @@ def setup_space_skill_frame(self):
desc_edit.setStyleSheet(self.theme.get_style_class('QPlainTextEdit', 'textedit'))
desc_edit.setFont(self.theme.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
- desc_edit.textChanged.connect(lambda: self.build2.set(
+ desc_edit.textChanged.connect(lambda: self.build.set(
'space', 'skill_desc', value=desc_edit.toPlainText(), autosave=False))
- self.build2.skills.space_desc = desc_edit
+ self.build.skills.space_desc = desc_edit
sidebar_layout.addWidget(desc_edit, 1, 0, 1, 2)
load_skills_button = create_button2(self.theme, 'Load Skills')
load_skills_button.clicked.connect(self.build_loader.load_skills_callback)
@@ -1314,16 +1314,16 @@ def setup_ground_skill_frame(self):
seg2 = self.create_bonus_bar_segment('ground', i * 2 + 1)
bonus_bar_layout.addWidget(seg2, row - 1, 1, alignment=AHCENTER)
button = create_item_button2(self.theme)
- button.clicked.connect(lambda i=i: self.build2.skill_unlock_callback('ground', i))
+ button.clicked.connect(lambda i=i: self.build.skill_unlock_callback('ground', i))
bonus_bar_layout.addWidget(button, row - 2, 1, alignment=AHCENTER)
- self.build2.skills.unlocks['ground'][i] = button
+ self.build.skills.unlocks['ground'][i] = button
row -= 3
icon_label = create_label2(self.theme, '', style='unlock_label')
icon_label.setPixmap(self.theme.icons['ground'])
bonus_bar_layout.addWidget(icon_label, 16, 1, alignment=AHCENTER)
count_label = create_label2(self.theme, '0', 'label_subhead')
bonus_bar_layout.addWidget(count_label, 17, 1, alignment=AHCENTER)
- self.build2.skills.count_labels['ground'] = count_label
+ self.build.skills.count_labels['ground'] = count_label
bonus_bar_container.setLayout(bonus_bar_layout)
col_layout.addWidget(bonus_bar_container, 0, 2)
frame.setLayout(col_layout)
@@ -1337,9 +1337,9 @@ def setup_ground_skill_frame(self):
desc_edit.setStyleSheet(self.theme.get_style_class('QPlainTextEdit', 'textedit'))
desc_edit.setFont(self.theme.get_font('textedit'))
desc_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
- desc_edit.textChanged.connect(lambda: self.build2.set(
+ desc_edit.textChanged.connect(lambda: self.build.set(
'ground', 'skill_desc', value=desc_edit.toPlainText(), autosave=False))
- self.build2.skills.ground_desc = desc_edit
+ self.build.skills.ground_desc = desc_edit
sidebar_layout.addWidget(desc_edit, 1, 0, 1, 2)
load_skills_button = create_button2(self.theme, 'Load Skills')
load_skills_button.clicked.connect(self.build_loader.load_skills_callback)
From 9639c02df653e0a27b532a41aa306ebf682f4f80 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 11:45:33 +0200
Subject: [PATCH 28/44] improving splash and adding image download progress to
it
---
src/app.py | 12 +++++++-----
src/downloader.py | 11 ++++++++++-
src/imagemanager.py | 12 ++++++++++--
src/splash.py | 44 +++++++++++++++++++++++++++++++++-----------
4 files changed, 60 insertions(+), 19 deletions(-)
diff --git a/src/app.py b/src/app.py
index 7ee1774..bde5276 100644
--- a/src/app.py
+++ b/src/app.py
@@ -72,6 +72,9 @@ def __init__(self, args, app_dir_path: str, version: str):
self.build: BuildManager = BuildManager(
self.cargo, self.images, self.config.autosave_path, self.theme.tooltips)
self.splash: SplashScreen = SplashScreen()
+ self.images.splash_text.connect(self.splash.loading_text)
+ self.downloader.progress_init.connect(self.splash.progress_init)
+ self.downloader.progress_step.connect(self.splash.progress_step)
self.tabbers: Tabbers = Tabbers()
self.app, self.window = self.create_main_window()
self.cache_icons()
@@ -167,21 +170,19 @@ def init_backend(self):
"""
Sets up downloader and provides cargo data and images.
"""
+ self.splash.show_progress(False)
self.splash.show_splash(True)
self.splash.set_loading_text('Loading Cargo Data...')
- self.splash.init_progress('Steps completed:', 3)
self.downloader.default_session_from_env()
self.cargo.provision_cargo_data()
self.images.image_set = self.cargo.image_set
self.images.failed_images = self.cargo.failed_images
- self.splash.increment_progress()
self.splash.set_loading_text('Downloading Images...')
self.images.download_images(self.cargo.skills)
self.cargo.store_failed_images()
- self.splash.increment_progress()
+ self.splash.show_progress(False)
self.splash.set_loading_text('Loading Base Images...')
self.images.load_base_images()
- self.splash.increment_progress()
def complete_app_init(self):
"""
@@ -1364,7 +1365,8 @@ def setup_splash(self, frame: QFrame):
loading_label = create_label2(self.theme, 'Loading: ...', 'label_subhead')
self.splash.loading_label = loading_label
layout.addWidget(loading_label, 2, 0, 1, 3, alignment=AHCENTER)
- progress_label = create_label2(self.theme, '', 'label_subhead')
+ progress_label = create_label2(
+ self.theme, '', 'label_subhead', style_override={'font': ('Roboto Mono', 12, 'normal')})
self.splash.progress_label = progress_label
layout.addWidget(progress_label, 3, 0, 1, 3, alignment=AHCENTER)
frame.setLayout(layout)
diff --git a/src/downloader.py b/src/downloader.py
index 6b479ef..284cf77 100644
--- a/src/downloader.py
+++ b/src/downloader.py
@@ -8,6 +8,8 @@
from typing import Callable
from urllib.parse import quote_plus
+from PySide6.QtCore import QObject, Signal
+
from .constants import GITHUB_CACHE_URL, WIKI_IMAGE_URL
from .textedit import compensate_json
@@ -28,15 +30,19 @@ def join(self):
return self._return
-class Downloader():
+class Downloader(QObject):
"""Downloads images and cargo tables"""
+ progress_init: Signal = Signal(int)
+ progress_step: Signal = Signal()
+
def __init__(self, images_dir: Path, ship_images_dir: Path):
"""
Parameters:
- :param images_dir: path to directory storing icons
- :param ship_images_dir: path to directory storing ship images
"""
+ super().__init__()
self._images_dir: str = str(images_dir)
self._ship_images_dir: str = str(ship_images_dir)
self._session: Session = Session()
@@ -133,6 +139,7 @@ def download_image(
image_file.write(image_response.content)
else:
failed_images[name] = int(time())
+ self.progress_step.emit()
def download_ship_image(
self, name: str, failed_images: dict[str, int], session: Session | None = None):
@@ -162,6 +169,7 @@ def download_ship_image(
image_file.write(image_response.content)
else:
failed_images[name] = int(time())
+ self.progress_step.emit()
def download_image_chunk(
self, image_list: list[str], image_suffix: str = '_icon.png',
@@ -206,6 +214,7 @@ def download_image_list(
while image_chunk_size < 4 and total_threads > 1:
total_threads -= 1
image_chunk_size = len(image_list) // total_threads
+ self.progress_init.emit(len(image_list))
threads: list[ReturnValueThread] = list()
for thread_num in range(total_threads):
image_chunk_start = image_chunk_size * thread_num
diff --git a/src/imagemanager.py b/src/imagemanager.py
index 2fe0009..4bc86d7 100644
--- a/src/imagemanager.py
+++ b/src/imagemanager.py
@@ -1,9 +1,11 @@
from os import listdir as os__listdir
from pathlib import Path
-from PySide6.QtGui import QIcon, QImage, QPixmap
from time import time
from urllib.parse import quote_plus, unquote_plus
+from PySide6.QtCore import QObject, Signal
+from PySide6.QtGui import QIcon, QImage, QPixmap
+
from .cargomanager import CargoManager
from .constants import SEVEN_DAYS_IN_SECONDS
from .downloader import Downloader
@@ -25,9 +27,11 @@ def __init__(self):
self.check: QImage
-class ImageManager():
+class ImageManager(QObject):
"""Manages icons and ship images"""
+ splash_text: Signal = Signal(str)
+
def __init__(
self, images_dir: Path, ship_images_dir: Path, app_dir: Path, cargo_cache: CargoManager,
downloader: Downloader):
@@ -39,6 +43,7 @@ def __init__(
- :param cargo_cache: used to access cache
- :param downloader: used to download icons and ship images
"""
+ super().__init__()
self._images_dir: Path = images_dir
self._ship_images_dir: Path = ship_images_dir
self._app_dir: Path = app_dir
@@ -105,15 +110,18 @@ def download_images(self, skill_cache: dict[str, dict]):
ultimate_skill_icons = {'Focused Frenzy', 'Probability Manipulation', 'EPS Corruption'}
image_set = self.image_set | ultimate_skill_icons
images = image_set - available_images - self._cargo_cache.boff_abilities['all'].keys()
+ self.splash_text.emit('Downloading Equipment and Trait Images...')
failed = self._downloader.download_image_list(list(images))
self.failed_images.update(failed)
boff_images = self._cargo_cache.boff_abilities['all'].keys() - available_images
+ self.splash_text.emit('Downloading Bridge Officer Images...')
failed = self._downloader.download_image_list(
list(boff_images), image_suffix='_icon_(Federation).png')
self.failed_images.update(failed)
skill_images = self.get_skill_icons(skill_cache) - available_images
+ self.splash_text.emit('Downloading Skill Images...')
failed = self._downloader.download_image_list(list(skill_images), image_suffix='.png')
self.failed_images.update(failed)
diff --git a/src/splash.py b/src/splash.py
index e27dee4..4898882 100644
--- a/src/splash.py
+++ b/src/splash.py
@@ -1,4 +1,4 @@
-from PySide6.QtCore import QObject, Signal
+from PySide6.QtCore import QObject, Signal, Slot
from PySide6.QtWidgets import QLabel, QTabWidget
@@ -7,7 +7,8 @@ class SplashScreen(QObject):
show: Signal = Signal(bool)
loading_text: Signal = Signal(str)
- progress_init: Signal = Signal(str, int)
+ progress_visible: Signal = Signal(bool)
+ progress_init: Signal = Signal(int)
progress_step: Signal = Signal()
def __init__(self):
@@ -15,11 +16,11 @@ def __init__(self):
self.loading_label: QLabel
self.progress_label: QLabel
self.tabber: QTabWidget
- self._progress_text: str = 'Progress:'
self._progress_total: int = 0
self._progress_current: int = 0
self.show.connect(self._show_splash)
self.loading_text.connect(self._set_loading_text)
+ self.progress_visible.connect(self._show_progress)
self.progress_init.connect(self._init_progress)
self.progress_step.connect(self._increment_progress)
@@ -32,15 +33,14 @@ def show_splash(self, visible: bool):
"""
self.show.emit(visible)
- def init_progress(self, message: str, total_progress: int):
+ def init_progress(self, total_progress: int):
"""
Makes progress label ready.
Parameters:
- - :param message: progress message
- :param total_progress: total number of steps
"""
- self.progress_init.emit(message, total_progress)
+ self.progress_init.emit(total_progress)
def increment_progress(self):
"""
@@ -48,6 +48,15 @@ def increment_progress(self):
"""
self.progress_step.emit()
+ def show_progress(self, visible: bool):
+ """
+ Shows/hides progress label.
+
+ Parameters:
+ - :param visible: `True` to show progress label, `False` to hide it
+ """
+ self.progress_visible.emit(visible)
+
def set_loading_text(self, message: str):
"""
Sets loading labels' text.
@@ -57,6 +66,7 @@ def set_loading_text(self, message: str):
"""
self.loading_text.emit(message)
+ @Slot(bool)
def _show_splash(self, visible: bool):
"""
Shows/hides splash.
@@ -69,28 +79,40 @@ def _show_splash(self, visible: bool):
else:
self.tabber.setCurrentIndex(0)
- def _init_progress(self, message: str, total_progress: int):
+ @Slot(int)
+ def _init_progress(self, total_progress: int):
"""
Makes progress label ready.
Parameters:
- - :param message: progress message
- :param total_progress: total number of steps
"""
- self._progress_text = message
self._progress_total = total_progress
self._progress_current = 0
self.progress_label.setText(
- f'{self._progress_text} ({self._progress_current:>4}/{self._progress_total:>4})')
+ f'({self._progress_current:>4}/{self._progress_total:>4})')
+ self.progress_label.show()
+ @Slot()
def _increment_progress(self):
"""
Increments progress count by 1.
"""
self._progress_current += 1
self.progress_label.setText(
- f'{self._progress_text} ({self._progress_current:>4}/{self._progress_total:>4})')
+ f'({self._progress_current:>4}/{self._progress_total:>4})')
+
+ @Slot(bool)
+ def _show_progress(self, visible: bool):
+ """
+ Shows/hides progress label.
+
+ Parameters:
+ - :param visible: `True` to show progress label, `False` to hide it
+ """
+ self.progress_label.setVisible(visible)
+ @Slot(str)
def _set_loading_text(self, message: str):
"""
Sets loading labels' text.
From 463e798f0b0dba9fc63ed56c9256f5a978f2da3f Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 11:56:34 +0200
Subject: [PATCH 29/44] splitting save into save and save as
---
src/app.py | 5 +++--
src/buildloader.py | 14 +++++++++++++-
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/src/app.py b/src/app.py
index bde5276..6290757 100644
--- a/src/app.py
+++ b/src/app.py
@@ -362,10 +362,10 @@ def setup_main_layout(self):
menu_layout.setColumnStretch(2, 2)
left_button_group = {
'Save': {'callback': self.build_loader.save_build_callback},
+ 'Save As': {'callback': self.build_loader.save_build_as_callback},
'Open': {'callback': self.build_loader.load_build_callback},
'Clear Current Tab': {'callback': lambda: self.build.clear_build_callback(
- self.tabbers.build_tabber.currentIndex())},
- 'Clear All Tabs': {'callback': self.build.clear_all}
+ self.tabbers.build_tabber.currentIndex())}
}
menu_layout.addLayout(
create_button_series2(self.theme, left_button_group), 0, 0, alignment=ALEFT | ATOP)
@@ -387,6 +387,7 @@ def setup_main_layout(self):
center_buttons = create_button_series2(self.theme, center_button_group, 'heavy_button')
menu_layout.addLayout(center_buttons, 0, 1)
right_button_group = {
+ 'Clear All Tabs': {'callback': self.build.clear_all},
'Export': {'callback': self.export_window.invoke},
'Settings': {'callback': lambda: self.tabbers.switch(5)},
}
diff --git a/src/buildloader.py b/src/buildloader.py
index cbc0370..70d0828 100644
--- a/src/buildloader.py
+++ b/src/buildloader.py
@@ -28,6 +28,7 @@ def __init__(
self._config: SETSConfig = config
self._settings: SETSSettings = settings
self._window: QWidget = window
+ self._current_build_path: Path | None = None
def load_build_callback(self):
"""
@@ -37,8 +38,18 @@ def load_build_callback(self):
self._config.config_subfolders['library'], SETS_FILE_FILTER, parent_window=self._window)
if load_path is not None:
self.load_build_file(load_path)
+ self._current_build_path = load_path
def save_build_callback(self):
+ """
+ Saves build to file it was opened from, overwriting that file.
+ """
+ if self._current_build_path is None:
+ self.save_build_as_callback()
+ else:
+ self.save_build_file(self._current_build_path)
+
+ def save_build_as_callback(self):
"""
Saves build to file
"""
@@ -56,6 +67,7 @@ def save_build_callback(self):
save_path = browse_path(preset_path, file_types, save=True, parent_window=self._window)
if save_path is not None:
self.save_build_file(save_path)
+ self._current_build_path = save_path
def load_skills_callback(self):
"""
@@ -122,7 +134,7 @@ def save_build_file(self, filepath: Path):
"""
extension = filepath.suffix.lower()
if extension == '.json':
- store_json__new(self._build, filepath)
+ store_json__new(self._build.data, filepath)
elif extension == '.png':
image = self._window.grab().toImage()
self.encode_in_image(image, json__dumps(self._build.data))
From c2de4cfe3167326f94b3b37116078354f9719507 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 13:58:21 +0200
Subject: [PATCH 30/44] Making library folder custom selectable
---
src/app.py | 47 ++++++++++++++++++++++++++++++++++++++++++----
src/buildloader.py | 20 +++++++++++++++-----
src/config.py | 3 ++-
src/constants.py | 1 +
src/iofunc.py | 12 +++++++++---
src/textedit.py | 9 +++++++++
6 files changed, 79 insertions(+), 13 deletions(-)
diff --git a/src/app.py b/src/app.py
index 6290757..499975f 100644
--- a/src/app.py
+++ b/src/app.py
@@ -4,7 +4,7 @@
from PySide6.QtCore import QDir, QPoint, Qt, QThread
from PySide6.QtGui import QCloseEvent, QFontDatabase, QTextOption
from PySide6.QtWidgets import (
- QApplication, QFrame, QPlainTextEdit, QPushButton, QScrollArea, QTabWidget, QWidget)
+ QApplication, QFrame, QLineEdit, QPlainTextEdit, QPushButton, QScrollArea, QTabWidget, QWidget)
from .buildhelpers import empty_build
from .buildloader import BuildLoader
@@ -14,15 +14,15 @@
from .constants import (
ABOTTOM, AHCENTER, ALEFT, ARIGHT, ATOP, AVCENTER, CAREERS, FACTIONS, GROUND_BOFF_SPECS, MARKS,
PRIMARY_SPECS, RARITIES, SCROLLOFF, SCROLLON, SECONDARY_SPECS, SMAXMAX, SMAXMIN, SMINMAX,
- SMINMIN)
+ SMINMIN, SMIXMAX)
from .contextmenu import ContextMenu
from .downloader import Downloader
from .exportwindow import ExportWindow
from .imagemanager import ImageManager
-from .iofunc import delete_folder_contents, load_icon, open_url, store_json
+from .iofunc import browse_path, delete_folder_contents, load_icon, open_url, store_json
from .picker import ItemEditor, Picker, ShipSelector
from .splash import SplashScreen
-from .textedit import format_skill_tooltip
+from .textedit import format_path, format_skill_tooltip
from .theme import AppTheme
from .widgetbuilder import (
create_annotated_slider2, create_button2, create_button_series2, create_checkbox2,
@@ -158,6 +158,10 @@ def init_config(self):
"""
self.config.autosave_path = self.config.config_dir / self.config.autosave_filename
self.config.ui_scale = self.settings.ui_scale
+ if os.name == 'nt':
+ self.config.home_dir = Path(os.getenv('USERPROFILE'))
+ else:
+ self.config.home_dir = Path(os.getenv('HOME'))
def init_environment(self):
"""
@@ -1381,6 +1385,30 @@ def hide_tooltips(self):
if window.type() == Qt.WindowType.ToolTip:
window.hide()
+ def set_library_path(self, entry_widget: QLineEdit):
+ """
+ Formats and stores new library path to `library_path`.
+
+ Parameters:
+ - :param entry_widget: the entry that holds the path
+ """
+ formatted_path = format_path(entry_widget.text())
+ self.settings.library_path = formatted_path
+ entry_widget.setText(formatted_path)
+
+ def browse_library_path(self, entry_widget: QLineEdit):
+ """
+ Browses for new library path, formats and stores new library path to `library_path`.
+
+ Parameters:
+ - :param entry_widget: the entry that holds the path
+ """
+ new_path = browse_path(self.config.home_dir, folder=True, parent_window=self.window)
+ if new_path is not None:
+ formatted_path = format_path(str(new_path))
+ self.settings.library_path = formatted_path
+ entry_widget.setText(formatted_path)
+
def setup_settings_frame(self):
"""
Populates the settings frame.
@@ -1454,6 +1482,17 @@ def setup_settings_frame(self):
backup_combo.currentIndexChanged.connect(
lambda new_i: self.settings.set('pref_backup', new_i))
sec_1.addWidget(backup_combo, 5, 2, alignment=ALEFT | AVCENTER)
+ library_path_label = create_label2(self.theme, 'Library Folder')
+ sec_1.addWidget(library_path_label, 6, 0, alignment=ALEFT)
+ library_path_entry = create_entry2(
+ self.theme, self.settings.library_path, style_override={'font': '@small_text'})
+ library_path_entry.setSizePolicy(SMIXMAX)
+ library_path_entry.editingFinished.connect(
+ lambda: self.set_library_path(library_path_entry))
+ sec_1.addWidget(library_path_entry, 6, 2)
+ library_path_button = create_button2(self.theme, 'Browse')
+ library_path_button.clicked.connect(lambda: self.browse_library_path(library_path_entry))
+ sec_1.addWidget(library_path_button, 6, 4)
scroll_layout.addLayout(sec_1)
# second section
diff --git a/src/buildloader.py b/src/buildloader.py
index 70d0828..4e843db 100644
--- a/src/buildloader.py
+++ b/src/buildloader.py
@@ -35,7 +35,7 @@ def load_build_callback(self):
Loads build from file
"""
load_path = browse_path(
- self._config.config_subfolders['library'], SETS_FILE_FILTER, parent_window=self._window)
+ self.get_library_path(), SETS_FILE_FILTER, parent_window=self._window)
if load_path is not None:
self.load_build_file(load_path)
self._current_build_path = load_path
@@ -59,7 +59,7 @@ def save_build_as_callback(self):
proposed_filename = f"({self._build['space']['ship']})"
if self._build['space']['ship_name'] != '':
proposed_filename = f"{self._build['space']['ship_name']} {proposed_filename}"
- preset_path = self._config.config_subfolders['library'] / proposed_filename
+ preset_path = self.get_library_path() / proposed_filename
if self._settings.default_save_format == 'PNG':
file_types = 'PNG image (*.png);;JSON file (*.json);;Any File (*.*)'
else:
@@ -74,7 +74,7 @@ def load_skills_callback(self):
Loads skills from file
"""
load_path = browse_path(
- self._config.config_subfolders['library'], SETS_FILE_FILTER, parent_window=self._window)
+ self.get_library_path(), SETS_FILE_FILTER, parent_window=self._window)
if load_path is not None:
self.load_skill_tree_file(load_path)
@@ -82,7 +82,7 @@ def save_skills_callback(self):
"""
Save skills to file
"""
- preset_path = self._config.config_subfolders['library'] / 'Skill Tree'
+ preset_path = self.get_library_path() / 'Skill Tree'
if self._settings.default_save_format == 'PNG':
file_types = 'PNG image (*.png);;JSON file (*.json);;Any File (*.*)'
else:
@@ -186,6 +186,16 @@ def save_skill_tree_file(self, filepath: Path):
self.encode_in_image(image, json__dumps(skill_tree))
image.save(filepath)
+ def get_library_path(self) -> Path:
+ """
+ Returns current library path.
+ """
+ if self._settings.library_path != '':
+ path = Path(self._settings.library_path)
+ if path.is_dir():
+ return path
+ return self._config.config_subfolders['library']
+
def merge_build(self, original_build: dict[str, dict[str]], new_build: dict[str, dict[str]]):
"""
updates `original_build` with contents of `new_build`
@@ -379,7 +389,7 @@ def load_legacy_build_image(self):
Loads legacy build from image file
"""
load_path = browse_path(
- self._config.config_subfolders['library'],
+ self.get_library_path(),
'PNG image (*.png);;Any File (*.*)', parent_window=self._window)
if load_path is not None:
if load_path.suffix.lower() != '.png':
diff --git a/src/config.py b/src/config.py
index 8ca6fc1..485433d 100644
--- a/src/config.py
+++ b/src/config.py
@@ -40,12 +40,13 @@ def __repr__(self):
class SETSSettings():
__slots__ = ('_settings', 'default_mark', 'default_save_format', 'default_rarity',
- 'picker_relative', 'pref_backup', 'ui_scale', 'state__geometry')
+ 'library_path', 'picker_relative', 'pref_backup', 'ui_scale', 'state__geometry')
def __init__(self, settings_file_path: Path):
self.default_mark: str = ''
self.default_save_format: str = 'JSON'
self.default_rarity: str = 'Common'
+ self.library_path: str = ''
self.picker_relative: int = 0
self.pref_backup: int = 0 # 0: auto backup preferred, 1: manual backup preferred
self.ui_scale: float = 1
diff --git a/src/constants.py b/src/constants.py
index 5e17380..5770676 100644
--- a/src/constants.py
+++ b/src/constants.py
@@ -9,6 +9,7 @@
SMAXMAX = QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum)
SMAXMIN = QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Minimum)
SMINMAX = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Maximum)
+SMIXMAX = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Maximum)
ATOP = Qt.AlignmentFlag.AlignTop
ABOTTOM = Qt.AlignmentFlag.AlignBottom
diff --git a/src/iofunc.py b/src/iofunc.py
index 88ae2d1..2819a5f 100644
--- a/src/iofunc.py
+++ b/src/iofunc.py
@@ -14,7 +14,7 @@
def browse_path(
- preset_path: Path, types: str = 'Any File (*.*)', save: bool = False,
+ preset_path: Path, types: str = 'Any File (*.*)', save: bool = False, folder: bool = False,
parent_window: QWidget | None = None) -> Path | None:
"""
Opens file dialog prompting the user to select a file.
@@ -25,17 +25,23 @@ def browse_path(
allowed. Format: ` (*.);; (*.);; \
[...]` Example: `Logfile (*.log);;Any File (*.*)`
- :param save: False => open file with dialog; True => save file with dialog
+ - :param folder: True => tries to open folder instead of file
- :param parent_window: window to use as parent; uses window icon and name of parent window
:return: returns selected path; None if user aborts or tries to open not-existing file
"""
+ if folder:
+ f = QFileDialog.getExistingDirectory(parent_window, 'Open Folder', str(preset_path))
+ if f == '':
+ return None
+ return Path(f)
if save:
- f = QFileDialog.getSaveFileName(parent_window, 'Save Log', str(preset_path), types)[0]
+ f = QFileDialog.getSaveFileName(parent_window, 'Save File', str(preset_path), types)[0]
if f == '':
return None
return Path(f)
else:
- f = QFileDialog.getOpenFileName(parent_window, 'Open Log', str(preset_path), types)[0]
+ f = QFileDialog.getOpenFileName(parent_window, 'Open File', str(preset_path), types)[0]
if f == '':
return None
selected_path = Path(f)
diff --git a/src/textedit.py b/src/textedit.py
index f4eedb7..ba96aa1 100644
--- a/src/textedit.py
+++ b/src/textedit.py
@@ -314,3 +314,12 @@ def sanitize_equipment_name(name: str) -> str:
def wiki_url(page_name: str, prefix: str = ''):
return (WIKI_URL + prefix + page_name).replace(' ', '_')
+
+
+def format_path(path: str):
+ if len(path) < 2:
+ return path
+ path = path.replace(chr(92), '/')
+ if path[1] == ':' and path[0] >= 'a' and path[0] <= 'z':
+ path = path[0].capitalize() + path[1:]
+ return path
From 41313ac340f2317a98083487a94cb617088ace0f Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 14:09:22 +0200
Subject: [PATCH 31/44] removing 'new' suffix from new functions
---
src/app.py | 2 +-
src/buildloader.py | 10 +++++-----
src/buildmanager.py | 12 ++++++------
src/cargomanager.py | 48 ++++++++++++++++++++++-----------------------
src/iofunc.py | 47 ++++++--------------------------------------
src/textedit.py | 8 ++++----
6 files changed, 46 insertions(+), 81 deletions(-)
diff --git a/src/app.py b/src/app.py
index 499975f..ed0aa66 100644
--- a/src/app.py
+++ b/src/app.py
@@ -168,7 +168,7 @@ def init_environment(self):
Creates external files before starting the app.
"""
if not self.config.autosave_path.exists():
- store_json(empty_build(), str(self.config.autosave_path))
+ store_json(empty_build(), self.config.autosave_path)
def init_backend(self):
"""
diff --git a/src/buildloader.py b/src/buildloader.py
index 4e843db..5509769 100644
--- a/src/buildloader.py
+++ b/src/buildloader.py
@@ -13,7 +13,7 @@
from .cargomanager import CargoManager
from .config import SETSConfig, SETSSettings
from .constants import BUILD_CONVERSION, BUILD_VERSION, SETS_FILE_FILTER
-from .iofunc import browse_path, load_json__new, store_json__new
+from .iofunc import browse_path, load_json, store_json
from .widgets import bundle, pixel_range
@@ -100,7 +100,7 @@ def load_build_file(self, filepath: Path, update_ui: bool = True):
"""
extension = filepath.suffix.lower()
if extension == '.json':
- build_data = load_json__new(filepath)
+ build_data = load_json(filepath)
elif extension == '.png':
decoded_str = self.decode_from_image(self, QImage(filepath))
if decoded_str == '':
@@ -134,7 +134,7 @@ def save_build_file(self, filepath: Path):
"""
extension = filepath.suffix.lower()
if extension == '.json':
- store_json__new(self._build.data, filepath)
+ store_json(self._build.data, filepath)
elif extension == '.png':
image = self._window.grab().toImage()
self.encode_in_image(image, json__dumps(self._build.data))
@@ -149,7 +149,7 @@ def load_skill_tree_file(self, filepath: Path):
"""
extension = filepath.suffix.lower()
if extension == '.json':
- build_data = load_json__new(filepath)
+ build_data = load_json(filepath)
elif extension == '.png':
decoded_str = self.decode_from_image(self, QImage(filepath))
if decoded_str == '':
@@ -180,7 +180,7 @@ def save_skill_tree_file(self, filepath: Path):
'skill_desc': self._build['skill_desc'],
}
if extension == '.json':
- store_json__new(skill_tree, filepath)
+ store_json(skill_tree, filepath)
elif extension == '.png':
image = self._window.grab().toImage()
self.encode_in_image(image, json__dumps(skill_tree))
diff --git a/src/buildmanager.py b/src/buildmanager.py
index c0744ba..b29e1b2 100644
--- a/src/buildmanager.py
+++ b/src/buildmanager.py
@@ -9,8 +9,8 @@
EQUIPMENT_TYPES, PRIMARY_SPECS, SECONDARY_SPECS, SHIP_TEMPLATE, SKILL_POINTS_FOR_RANK, SPECIES,
SPECIES_TRAITS)
from .imagemanager import ImageManager
-from .iofunc import open_wiki_page, store_json__new
-from .textedit import add_equipment_tooltip_header__new, get_ultimate_skill_unlock_tooltip__new
+from .iofunc import open_wiki_page, store_json
+from .textedit import add_equipment_tooltip_header, get_ultimate_skill_unlock_tooltip
from .theme import TooltipCSS
from .widgets import ItemButton, ItemSlot, ShipButton, ShipImage, Thread, TooltipLabel
@@ -174,7 +174,7 @@ def autosave(self):
Saves build to autosave file.
"""
if not self._building:
- store_json__new(self._build_data, self._autosave_path)
+ store_json(self._build_data, self._autosave_path)
def __getitem__(self, key: str):
return self._build_data[key]
@@ -722,7 +722,7 @@ def slot_equipment_item(
"""
self._build_data[environment][build_key][build_subkey] = item
overlay = getattr(self._images.overlays, item['rarity'].lower().replace(' ', ''))
- tooltip = add_equipment_tooltip_header__new(
+ tooltip = add_equipment_tooltip_header(
item, self._cache.equipment[build_key][item['item']], self._tooltip_styles)
item_button: ItemButton = getattr(getattr(self, environment), build_key)[build_subkey]
item_button.set_item_full(self._images.get(item['item']), overlay, tooltip)
@@ -1003,11 +1003,11 @@ def set_skill_unlock_space(
unlock_data = self._cache.skills['space_unlocks'][career][4]
unlock_button.set_item(self._images.get(unlock_data['name']))
if points_spent > 26:
- unlock_button.tooltip = get_ultimate_skill_unlock_tooltip__new(
+ unlock_button.tooltip = get_ultimate_skill_unlock_tooltip(
unlock_data, state, 3, self._tooltip_styles)
self._build_data['skill_unlocks'][career][id] = 3
else:
- unlock_button.tooltip = get_ultimate_skill_unlock_tooltip__new(
+ unlock_button.tooltip = get_ultimate_skill_unlock_tooltip(
unlock_data, state, points_spent - 24, self._tooltip_styles)
self._build_data['skill_unlocks'][career][id] = state
if not self._building:
diff --git a/src/cargomanager.py b/src/cargomanager.py
index f8b8c3a..1087b5a 100644
--- a/src/cargomanager.py
+++ b/src/cargomanager.py
@@ -7,9 +7,9 @@
PRIMARY_SPECS, SEVEN_DAYS_IN_SECONDS, SHIP_QUERY_URL, STARSHIP_TRAIT_QUERY_URL, TRAIT_QUERY_URL,
TRAYSKILL_QUERY)
from .downloader import Downloader
-from .iofunc import load_json__new, store_json__new
+from .iofunc import load_json, store_json
from .textedit import (
- create_equipment_tooltip__new, create_trait_tooltip__new, dewikify, parse_wikitext,
+ create_equipment_tooltip, create_trait_tooltip, dewikify, parse_wikitext,
sanitize_equipment_name)
from .theme import AppTheme
@@ -68,11 +68,11 @@ def load_static_data(self):
Loads skill data and item aliases.
"""
local_folder = self._app_dir / 'local'
- self.item_aliases = load_json__new(local_folder / 'aliases.json')
- space_skill_data = load_json__new(local_folder / 'space_skills.json')
+ self.item_aliases = load_json(local_folder / 'aliases.json')
+ space_skill_data = load_json(local_folder / 'space_skills.json')
self.skills['space'] = space_skill_data['space']
self.skills['space_unlocks'] = space_skill_data['space_unlocks']
- ground_skill_data = load_json__new(local_folder / 'ground_skills.json')
+ ground_skill_data = load_json(local_folder / 'ground_skills.json')
self.skills['ground'] = ground_skill_data['ground']
self.skills['ground_unlocks'] = ground_skill_data['ground_unlocks']
@@ -135,9 +135,9 @@ def provision_cargo_data(self):
self.ground_doffs = ground_doff_data
if images_updated:
alt_images.update(self.alt_images)
- store_json__new(alt_images, self._folders['cache'] / 'alt_images.json')
+ store_json(alt_images, self._folders['cache'] / 'alt_images.json')
image_set |= self.image_set
- store_json__new(list(image_set), self._folders['cache'] / 'images_list.json')
+ store_json(list(image_set), self._folders['cache'] / 'images_list.json')
self.alt_images = alt_images
self.image_set = image_set
self.failed_images = self.get_cached_data('images_failed.json')
@@ -155,14 +155,14 @@ def get_cached_data(self, file_name: str) -> dict | list | None:
if file_path.is_file():
last_modified = file_path.stat().st_mtime
if time() - last_modified < SEVEN_DAYS_IN_SECONDS:
- return load_json__new(file_path)
+ return load_json(file_path)
return None
def store_failed_images(self):
"""
Stores failed images to cache folder
"""
- store_json__new(self.failed_images, self._folders['cache'] / 'images_failed.json')
+ store_json(self.failed_images, self._folders['cache'] / 'images_failed.json')
def cache_ship_data(self):
"""
@@ -170,7 +170,7 @@ def cache_ship_data(self):
"""
ship_cargo_data: list[dict[str]] = self.get_cargo_data('ship_list.json', SHIP_QUERY_URL)
self.ships = {ship['Page']: ship for ship in ship_cargo_data}
- store_json__new(self.ships, self._folders['cache'] / 'ships.json')
+ store_json(self.ships, self._folders['cache'] / 'ships.json')
def cache_equipment_data(self):
"""
@@ -196,7 +196,7 @@ def cache_equipment_data(self):
'name': name,
'rarity': item['rarity'],
'type': item['type'],
- 'tooltip': create_equipment_tooltip__new(item, tooltip_styles)
+ 'tooltip': create_equipment_tooltip(item, tooltip_styles)
}
self.image_set.add(name)
self.equipment['fore_weapons'].update(self.equipment['ship_weapon'])
@@ -208,7 +208,7 @@ def cache_equipment_data(self):
self.equipment['uni_consoles'].update(self.equipment['tac_consoles'])
self.equipment['uni_consoles'].update(self.equipment['sci_consoles'])
self.equipment['uni_consoles'].update(self.equipment['eng_consoles'])
- store_json__new(self.equipment, self._folders['cache'] / 'equipment.json')
+ store_json(self.equipment, self._folders['cache'] / 'equipment.json')
def cache_trait_data(self):
"""
@@ -229,7 +229,7 @@ def cache_trait_data(self):
trait_data = {
'Page': trait['Page'],
'name': name,
- 'tooltip': create_trait_tooltip__new(
+ 'tooltip': create_trait_tooltip(
name, trait['description'], trait_type, trait['environment'],
tooltip_styles)
}
@@ -246,8 +246,8 @@ def cache_trait_data(self):
# catch wrong values in trait['environment'] (cargo issue)
except (KeyError, AttributeError):
pass
- store_json__new(self.space_traits, self._folders['cache'] / 'space_traits.json')
- store_json__new(self.ground_traits, self._folders['cache'] / 'ground_traits.json')
+ store_json(self.space_traits, self._folders['cache'] / 'space_traits.json')
+ store_json(self.ground_traits, self._folders['cache'] / 'ground_traits.json')
def cache_starship_trait_data(self):
"""
@@ -271,7 +271,7 @@ def cache_starship_trait_data(self):
f"Starship Trait
"
f"{ship_trait['short']}
{parse_wikitext(ship_trait['detailed'], styles)}")
}
- store_json__new(self.starship_traits, self._folders['cache'] / 'starship_traits.json')
+ store_json(self.starship_traits, self._folders['cache'] / 'starship_traits.json')
def cache_boff_data(self):
"""
@@ -308,7 +308,7 @@ def cache_boff_data(self):
f"{parse_wikitext(dewikify(boff_ability[f'rank{decimal}info']), styles)}")
self.boff_abilities['all'][boff_name] = ability_item
self.image_set |= self.boff_abilities['all'].keys()
- store_json__new(self.boff_abilities, self._folders['cache'] / 'boff_abilities.json')
+ store_json(self.boff_abilities, self._folders['cache'] / 'boff_abilities.json')
def cache_modifier_data(self):
"""
@@ -340,7 +340,7 @@ def cache_modifier_data(self):
self.modifiers['uni_consoles'].update(self.modifiers['sci_consoles'])
self.modifiers['uni_consoles'].update(self.modifiers['eng_consoles'])
self.modifiers['uni_consoles'].update(self.modifiers['tac_consoles'])
- store_json__new(self.modifiers, self._folders['cache'] / 'modifiers.json')
+ store_json(self.modifiers, self._folders['cache'] / 'modifiers.json')
def cache_duty_officer_data(self):
"""
@@ -359,8 +359,8 @@ def cache_duty_officer_data(self):
elif doff['shipdutytype'] is not None:
self.cache_doff_single(self.space_doffs, doff)
self.cache_doff_single(self.ground_doffs, doff)
- store_json__new(self.space_doffs, self._folders['cache'] / 'space_doffs.json')
- store_json__new(self.ground_doffs, self._folders['cache'] / 'ground_doffs.json')
+ store_json(self.space_doffs, self._folders['cache'] / 'space_doffs.json')
+ store_json(self.ground_doffs, self._folders['cache'] / 'ground_doffs.json')
def cache_doff_single(self, cache: dict, doff: dict):
"""
@@ -393,7 +393,7 @@ def get_cargo_data(
if cargo_file.is_file():
last_modified = cargo_file.stat().st_mtime
if time() - last_modified < SEVEN_DAYS_IN_SECONDS or ignore_cache_age:
- cargo_data = load_json__new(cargo_file)
+ cargo_data = load_json(cargo_file)
if cargo_data is not None:
return cargo_data
@@ -409,9 +409,9 @@ def get_cargo_data(
backup_paths = (backup_path, auto_backup_path)
for path in backup_paths:
if path.is_file():
- cargo_data = load_json__new(path)
+ cargo_data = load_json(path)
if cargo_data is not None:
- store_json__new(cargo_data, cargo_file)
+ store_json(cargo_data, cargo_file)
return cargo_data
# TODO what happens when both backups fail?
else:
@@ -419,7 +419,7 @@ def get_cargo_data(
else:
if cargo_file.is_file():
cargo_file.copy_into(self._folders['auto_backups'])
- store_json__new(cargo_data, cargo_file)
+ store_json(cargo_data, cargo_file)
return cargo_data
def backup_cargo_data(self):
diff --git a/src/iofunc.py b/src/iofunc.py
index 2819a5f..1426de0 100644
--- a/src/iofunc.py
+++ b/src/iofunc.py
@@ -1,9 +1,6 @@
-import json
from json import dump as json__dump, load as json__load, JSONDecodeError
-import os
from pathlib import Path
from shutil import rmtree as shutil__rmtree
-import sys
from urllib.parse import quote_plus
from webbrowser import open as webbrowser_open
@@ -51,16 +48,16 @@ def browse_path(
return None
-def delete_folder_contents(path_to_folder):
+def delete_folder_contents(path_to_folder: Path):
"""
Delets all files and folders within a folder.
Parameters:
- - :param path_to_folder: absolute path to folder
+ - :param path_to_folder: path to folder
"""
- if os.path.exists(path_to_folder) and os.path.isdir(path_to_folder):
+ if path_to_folder.is_dir():
shutil__rmtree(path_to_folder)
- os.mkdir(path_to_folder)
+ path_to_folder.mkdir(exist_ok=True)
def load_icon(filename: str, app_directory: Path, size: tuple = tuple()) -> QIcon | QPixmap:
@@ -77,7 +74,7 @@ def load_icon(filename: str, app_directory: Path, size: tuple = tuple()) -> QIco
return icon
-def load_json__new(file_path: Path) -> dict | list | None:
+def load_json(file_path: Path) -> dict | list | None:
"""
Loads json from path and returns dictionary or list. Returns `None` if no data could be found.
@@ -91,21 +88,7 @@ def load_json__new(file_path: Path) -> dict | list | None:
return None
-def load_json(path: str) -> dict | list:
- """
- Loads json from path and returns dictionary or list.
-
- Parameters:
- - :param path: absolute path to json file
- """
- if not (os.path.exists(path) and os.path.isfile(path) and os.path.isabs(path)):
- raise FileNotFoundError(f'Invalid / not absolute path: {path}')
- with open(path, 'r', encoding='utf-8') as file:
- data = json.load(file)
- return data
-
-
-def store_json__new(data: dict | list, path: Path) -> bool:
+def store_json(data: dict | list, path: Path) -> bool:
"""
Stores data to json file at path. Overwrites file at target location. Raises ValueError if path
is not absolute. Returns `False` if file could not be saved, `True` otherwise.
@@ -122,24 +105,6 @@ def store_json__new(data: dict | list, path: Path) -> bool:
return False
-def store_json(data: dict | list, path: str):
- """
- Stores data to json file at path. Overwrites file at target location. Raises ValueError if path
- is not absolute.
-
- Paramters:
- - :param data: dictionary or list that should be stored
- - :param path: target location; must be absolute path
- """
- if not os.path.isabs(path):
- raise ValueError(f'Path to file must be absolute: {path}')
- try:
- with open(path, 'w') as file:
- json.dump(data, file)
- except OSError as e:
- sys.stdout.write(f'[Error] Data could not be saved: {e}')
-
-
def get_image_file_name(name: str) -> str:
"""
Converts image name to valid file name
diff --git a/src/textedit.py b/src/textedit.py
index ba96aa1..402e8cc 100644
--- a/src/textedit.py
+++ b/src/textedit.py
@@ -4,7 +4,7 @@
from .theme import TooltipCSS
-def add_equipment_tooltip_header__new(
+def add_equipment_tooltip_header(
item: dict[str, str], item_data: dict[str], tooltip_styles: TooltipCSS) -> str:
"""
Adds equipment header including name, mark, modifiers, rarity and item type to the tooltip body
@@ -69,7 +69,7 @@ def format_skill_tooltip(
f"{skill_data['gdesc']}
{skill_data['nodes'][node_index]['desc']}
")
-def get_ultimate_skill_unlock_tooltip__new(
+def get_ultimate_skill_unlock_tooltip(
unlock: dict[str], unlock_choice: int, enhancements: int, tooltip_styles: TooltipCSS):
"""
Formats tooltip for ultimate skill unlock.
@@ -105,7 +105,7 @@ def get_ultimate_skill_unlock_tooltip__new(
return tooltip
-def create_equipment_tooltip__new(item: dict, tooltip_style: TooltipCSS) -> str:
+def create_equipment_tooltip(item: dict, tooltip_style: TooltipCSS) -> str:
"""
Creates tooltip for equipment from raw item data.
@@ -132,7 +132,7 @@ def create_equipment_tooltip__new(item: dict, tooltip_style: TooltipCSS) -> str:
return tooltip
-def create_trait_tooltip__new(
+def create_trait_tooltip(
name: str, description: str, type_: str, environment: str,
styles: TooltipCSS) -> str:
"""
From 2e47a7e77e9f2a8b298b7a6e34e49e25f62f8d1b Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 14:26:51 +0200
Subject: [PATCH 32/44] fixing typo in export
---
src/exportwindow.py | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/exportwindow.py b/src/exportwindow.py
index 404098d..a535aca 100644
--- a/src/exportwindow.py
+++ b/src/exportwindow.py
@@ -280,7 +280,7 @@ def get_build_markdown(self, environment: str, type_: str) -> str:
equip_table += self.md_equipment_table('space', 'aft_weapons', 'Aft Weapons')
equip_table += self.md_equipment_table(
'space', 'deflector', 'Deflector', single_line=True)
- if self.build['space']['sec_def'][0]:
+ if self._build['space']['sec_def'][0]:
equip_table += self.md_equipment_table(
'space', 'sec_def', 'Secondary Deflector', single_line=True)
equip_table += self.md_equipment_table(
@@ -288,7 +288,7 @@ def get_build_markdown(self, environment: str, type_: str) -> str:
equip_table += self.md_equipment_table('space', 'core', 'Warp', single_line=True)
equip_table += self.md_equipment_table('space', 'shield', 'Shield', single_line=True)
equip_table += self.md_equipment_table('space', 'devices', 'Devices')
- if self.build['space']['experimental'][0]:
+ if self._build['space']['experimental'][0]:
equip_table += self.md_equipment_table(
'space', 'experimental', 'Experimental Weapon', single_line=True)
if self._build['space']['hangars'][0] or self._build['space']['hangars'][1]:
@@ -317,17 +317,17 @@ def get_build_markdown(self, environment: str, type_: str) -> str:
md += self.create_md_table(trait_table)
md += '\n\n\n\n'
trait_table = [['**Personal Space Traits**', '**Notes**']]
- for trait in notempty(self.build['space']['traits']):
+ for trait in notempty(self._build['space']['traits']):
trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
md += self.create_md_table(trait_table)
md += '\n\n\n\n'
trait_table = [['**Space Reputation Traits**', '**Notes**']]
- for trait in notempty(self.build['space']['rep_traits']):
+ for trait in notempty(self._build['space']['rep_traits']):
trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
md += self.create_md_table(trait_table)
md += '\n\n\n\n'
trait_table = [['**Active Space Reputation Traits**', '**Notes**']]
- for trait in notempty(self.build['space']['active_rep_traits']):
+ for trait in notempty(self._build['space']['active_rep_traits']):
trait_table.append([f"[{trait['item']}]({wiki_url(trait['item'], 'Trait: ')})", ''])
md += self.create_md_table(trait_table)
@@ -426,7 +426,7 @@ def get_build_markdown(self, environment: str, type_: str) -> str:
else:
unlock_slot = self._cargo.skills['space_unlocks'][career][i]
if unlock_slot['points_required'] == 24:
- skill_count = self._cargo.skills[f"space_points_{career}"]
+ skill_count = self._build._skill_state[f"space_points_{career}"]
link = wiki_url(unlock_slot['name'], 'Ability: ')
if unlock_state is None:
row += ['', '', '', ' ']
@@ -467,11 +467,11 @@ def get_build_markdown(self, environment: str, type_: str) -> str:
id_offset = 0
for skill in self._cargo.skills['ground']:
row = [f"[{skill['nodes'][0]['name']}]({skill['link']})"]
- if self.build['ground_skills'][skill['tree']][id_offset]:
+ if self._build['ground_skills'][skill['tree']][id_offset]:
row.append('[X]')
else:
row.append('[ ]')
- if self.build['ground_skills'][skill['tree']][id_offset + 1]:
+ if self._build['ground_skills'][skill['tree']][id_offset + 1]:
row.append('[X]')
else:
row.append('[ ]')
From 4b0dc91a9e42c2fde431fb45afdfcbc433226464 Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 15:27:25 +0200
Subject: [PATCH 33/44] adding packaging methods
---
distribution/.gitignore | 1 +
distribution/README.md | 71 ++++++++
distribution/SETS.iss | 197 +++++++++++++++++++++
distribution/arch/PKGBUILD | 65 +++++++
distribution/arch/README.md | 21 +++
distribution/build_pyinstaller.sh | 29 +++
distribution/debian/DEBIAN/control | 8 +
distribution/debian/README.md | 31 ++++
distribution/debian/build_deb.Dockerfile | 13 ++
distribution/debian/build_deb.compose.yaml | 22 +++
distribution/debian/changelog | 5 +
distribution/debian/copyright | 6 +
distribution/debian/package_deb.sh | 62 +++++++
distribution/sets.desktop | 9 +
distribution/windows/README.md | 9 +
local/{icon.ico => sets_icon_small.ico} | Bin
main.py | 2 +-
pyproject.toml | 17 +-
18 files changed, 556 insertions(+), 12 deletions(-)
create mode 100644 distribution/.gitignore
create mode 100644 distribution/README.md
create mode 100644 distribution/SETS.iss
create mode 100644 distribution/arch/PKGBUILD
create mode 100644 distribution/arch/README.md
create mode 100644 distribution/build_pyinstaller.sh
create mode 100644 distribution/debian/DEBIAN/control
create mode 100644 distribution/debian/README.md
create mode 100644 distribution/debian/build_deb.Dockerfile
create mode 100644 distribution/debian/build_deb.compose.yaml
create mode 100644 distribution/debian/changelog
create mode 100644 distribution/debian/copyright
create mode 100644 distribution/debian/package_deb.sh
create mode 100644 distribution/sets.desktop
create mode 100644 distribution/windows/README.md
rename local/{icon.ico => sets_icon_small.ico} (100%)
diff --git a/distribution/.gitignore b/distribution/.gitignore
new file mode 100644
index 0000000..612654b
--- /dev/null
+++ b/distribution/.gitignore
@@ -0,0 +1 @@
+*.spec
diff --git a/distribution/README.md b/distribution/README.md
new file mode 100644
index 0000000..a5642c0
--- /dev/null
+++ b/distribution/README.md
@@ -0,0 +1,71 @@
+# Development Notes
+
+These are currently reflecting the state of the app, but are subject ot change.
+
+## Windows
+- [Currently Testing With Inno Setup](https://jrsoftware.org/isinfo.php)
+ - [SETS.iss](./SETS.iss)
+
+## Linux
+
+#### System Wide Paths
+- /opt/sets/
+ - This is the ideal loation for Linux, all assets are located here.
+- /usr/bin/sets -> /opt/sets/SETS
+ - shell script to execute the binary from a PATH location
+- /usr/share/applications/sets.desktop
+ - The `.desktop` entry that registers the application.
+- /usr/share/icons/hicolor/256x256/apps/sets.png
+ - Location for the application icon, referred to in the `.desktop` entry.
+
+#### Config Paths
+1. If `$XDG_CONFIG_HOME` is set, takes priority. Is supposed to be a directory. Don't use if it is a file. Don't create it if it doesn't exist.
+2. `$HOME/.config` if the `.config` folder exists, but do not create it if it does not.
+3. [`$HOME/.` if you can keep the settings in one file (and the dot here is important)] -> not used, because SETS requires a folder
+4. ` $HOME/.sets/`
+
+##### .desktop entry template
+```
+[Desktop Entry]
+Type=Application
+Name=SETS
+Comment=STO Equipment and Trait Selector
+Icon=/usr/share/icons/hicolor/256x256/apps/sets.png
+Exec=/opt/sets/SETS
+Terminal=False
+Categories=Utility;GameTool;
+StartupWMClass=STO Equipment and Trait Selector
+```
+
+#### Debian `.deb` Package Approach
+
+```bash
+apt install -f ./sets---x86_64.deb
+```
+- Unpacks the contents to `/opt/sets`, `/usr/share/applications/sets.desktop`, etc
+- Automatically registers the package manager metadata with `dpkg` so the system becomes aware which files belong to which package
+ - This is the biggest advantage.
+- Allows uninstallation by name (apt remove sets), because it recorded which files it put there.
+ - Does not remove settings.
+- uses `-f` flag to install dependencies
+
+To check whether it was installed correctly: `apt list --installed sets`
+
+Use `dpkg -L sets` to list registered files for SETS.
+
+
+#### Arch `PKGBUILD` File Structure
+###### Note: to public to AUR we need a PKGBUILD that builds from source
+
+On Arch there is similar tools, but there is no standard format like `.deb`, rather it's usually just a compressed archive `sets.pkg.tar.zst` by a `PKGBUILD` script, which can then be fed into `pacman`/`paru`/`yay`.
+
+Run `makepkg` from the directory that contains the `PKGBUILD` file and that spits out the `sets---x86_64.pkg.tar.zst` file, which can then be installed via pacman:
+
+```
+sudo pacman -U ./sets---x86_64.pkg.tar.zst
+```
+
+And `pacman -Qs sets` to confirm that it is registered, just as a sanity check.
+
+For debugging `pacman -Ql sets` lists where everything registers under that package is located (the assets and `.desktop` files, to ensure the `PKGBUILD` file was defined properly)
+
diff --git a/distribution/SETS.iss b/distribution/SETS.iss
new file mode 100644
index 0000000..9b52fca
--- /dev/null
+++ b/distribution/SETS.iss
@@ -0,0 +1,197 @@
+; Script generated by the Inno Setup Script Wizard.
+; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
+
+#define MyAppName "SETS"
+#define MyAppVersion "3.0.0"
+#define MyAppPublisher "STOCD"
+#define MyAppURL "https://github.com/STOCD/SETS"
+#define MyAppExeName "SETS.exe"
+
+[Setup]
+; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
+; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
+AppId={{A0137D88-47DD-4D9D-8904-8CA92CD0D3B3}
+AppName={#MyAppName}
+AppVersion={#MyAppVersion}
+;AppVerName={#MyAppName} {#MyAppVersion}
+AppPublisher={#MyAppPublisher}
+AppPublisherURL={#MyAppURL}
+AppSupportURL={#MyAppURL}
+AppUpdatesURL={#MyAppURL}
+DefaultDirName={autopf}\{#MyAppName}
+UninstallDisplayIcon={app}\{#MyAppExeName}
+; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run
+; on anything but x64 and Windows 11 on Arm.
+ArchitecturesAllowed=x64compatible
+; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the
+; install be done in "64-bit mode" on x64 or Windows 11 on Arm,
+; meaning it should use the native 64-bit Program Files directory and
+; the 64-bit view of the registry.
+ArchitecturesInstallIn64BitMode=x64compatible
+DisableProgramGroupPage=yes
+OutputDir=..\dist\{#MyAppName}
+OutputBaseFilename=SETS-Installer_{#MyAppVersion}
+SetupIconFile=..\local\sets_icon_small.ico
+SolidCompression=yes
+WizardStyle=modern
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+[Tasks]
+Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
+
+[Files]
+Source: "..\dist\{#MyAppName}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
+Source: "..\dist\{#MyAppName}\_internal\*"; DestDir: "{app}\_internal"; Flags: ignoreversion recursesubdirs createallsubdirs
+; NOTE: Don't use "Flags: ignoreversion" on any shared system files
+
+[Icons]
+Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
+Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
+
+[Run]
+Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
+
+[Code]
+var
+ checkbox: TNewCheckBox;
+ UninstallFirstPage: TNewNotebookPage;
+ configPath: string;
+
+function GetConfigPath(): string;
+var
+ envAppdata: string;
+ envProf: string;
+begin
+ envAppdata := GetEnv('APPDATA');
+ envProf := GetEnv('USERPROFILE');
+ if DirExists(envAppdata + '\SETS') and FileExists(envAppdata + '\SETS\SETS_settings.ini') then
+ begin
+ Result := envAppdata + '\SETS';
+ end
+ else if DirExists(envProf + '\SETS') and FileExists(envProf + '\SETS\SETS_settings.ini') then
+ begin
+ Result := envProf + '\SETS';
+ end
+ else
+ begin
+ Result := '';
+ end;
+end;
+
+procedure UpdateUninstallWizard;
+begin
+ if UninstallProgressForm.InnerNotebook.ActivePage = UninstallFirstPage then
+ begin
+ UninstallProgressForm.PageNameLabel.Caption := 'Uninstall SETS';
+ UninstallProgressForm.PageDescriptionLabel.Caption := 'Preparing to remove SETS from your device.';
+ end;
+end;
+
+
+procedure InitializeUninstallProgressForm();
+var
+ UninstallButton: TNewButton;
+ infotext: TNewStaticText;
+ CancelButtonEnabled: Boolean;
+ CancelButtonModalResult: Integer;
+ PageNameLabel: string;
+ PageDescriptionLabel: string;
+begin
+ if not UninstallSilent then
+ begin
+ PageNameLabel := UninstallProgressForm.PageNameLabel.Caption;
+ PageDescriptionLabel := UninstallProgressForm.PageDescriptionLabel.Caption;
+
+ UninstallFirstPage := TNewNotebookPage.Create(UninstallProgressForm);
+ UninstallFirstPage.Notebook := UninstallProgressForm.InnerNotebook;
+ UninstallFirstPage.Parent := UninstallProgressForm.InnerNotebook;
+ UninstallFirstPage.Align := alClient;
+
+ infotext := TNewStaticText.Create(UninstallProgressForm);
+ infotext.Parent := UninstallFirstPage;
+ infotext.Top := UninstallProgressForm.StatusLabel.Top;
+ infotext.Left := UninstallProgressForm.PageNameLabel.Left;
+ infotext.AutoSize := True;
+ infotext.WordWrap := True;
+ infotext.Width := UninstallProgressForm.PageNameLabel.Width;
+
+ checkbox := TNewCheckBox.Create(UninstallProgressForm);
+ checkbox.Caption := 'Delete App Configuration (including default library!)';
+ checkbox.Checked := True;
+ checkbox.Parent := UninstallFirstPage;
+ checkbox.Left := UninstallProgressForm.StatusLabel.Left;
+ checkbox.Width := UninstallProgressForm.StatusLabel.Width;
+
+ configPath := GetConfigPath();
+ if configPath = '' then
+ begin
+ checkbox.Checked := False;
+ checkbox.Enabled := False;
+ infotext.Caption := 'No app configuration found. Press Uninstall to proceed with uninstallation.';
+ end
+ else
+ begin
+ infotext.Caption := 'App configuration found in following location:'#13 + configPath;
+ end;
+
+ infotext.AdjustHeight;
+ checkbox.Top := infotext.Top + infotext.Height + ScaleY(8);
+
+ UninstallButton := TNewButton.Create(UninstallProgressForm);
+ UninstallButton.Parent := UninstallProgressForm;
+ UninstallButton.Left :=
+ UninstallProgressForm.CancelButton.Left -
+ UninstallProgressForm.CancelButton.Width -
+ ScaleX(10);
+ UninstallButton.Top := UninstallProgressForm.CancelButton.Top;
+ UninstallButton.Width := UninstallProgressForm.CancelButton.Width;
+ UninstallButton.Height := UninstallProgressForm.CancelButton.Height;
+ UninstallButton.ModalResult := mrOK;
+ UninstallButton.Caption := 'Uninstall';
+ UninstallButton.TabOrder := UninstallProgressForm.CancelButton.TabOrder;
+ UninstallButton.Default := True;
+ UninstallProgressForm.CancelButton.TabOrder := UninstallButton.TabOrder + 1;
+
+ UninstallProgressForm.InnerNotebook.ActivePage := UninstallFirstPage;
+
+ UpdateUninstallWizard;
+ CancelButtonEnabled := UninstallProgressForm.CancelButton.Enabled
+ UninstallProgressForm.CancelButton.Enabled := True;
+ CancelButtonModalResult := UninstallProgressForm.CancelButton.ModalResult;
+ UninstallProgressForm.CancelButton.ModalResult := mrCancel;
+ if UninstallProgressForm.ShowModal = mrCancel then Abort;
+
+ UninstallButton.Visible := False;
+ UninstallProgressForm.CancelButton.Enabled := CancelButtonEnabled;
+ UninstallProgressForm.CancelButton.ModalResult := CancelButtonModalResult;
+
+ UninstallProgressForm.PageNameLabel.Caption := PageNameLabel;
+ UninstallProgressForm.PageDescriptionLabel.Caption := PageDescriptionLabel;
+
+ UninstallProgressForm.InnerNotebook.ActivePage := UninstallProgressForm.InstallingPage;
+ end;
+end;
+
+procedure removeConfig();
+var
+ findRecord: TFindRec;
+ FileFound: Boolean;
+begin
+ if DirExists(configPath) then
+ begin
+ DelTree(configPath, True, True, True);
+ end;
+end;
+
+procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
+begin
+ if CurUninstallStep = usPostUninstall then
+ begin
+ if checkbox.Checked then
+ begin
+ removeConfig();
+ end;
+ end;
+end;
diff --git a/distribution/arch/PKGBUILD b/distribution/arch/PKGBUILD
new file mode 100644
index 0000000..45e5d43
--- /dev/null
+++ b/distribution/arch/PKGBUILD
@@ -0,0 +1,65 @@
+# Maintainer: Shinga
+pkgname=sets
+pkgver=3.0.0
+pkgrel=1
+pkgdesc="SETS - STO Equipment and Trait Selector"
+arch=('x86_64')
+url="https://github.com/STOCD/SETS"
+license=('GPL-3.0')
+depends=('python')
+makedepends=('python' 'python-virtualenv' 'python-wheel' 'python-setuptools' 'git' 'unzip')
+source=("https://github.com/STOCD/SETS/archive/refs/tags/v${pkgver}.tar.gz")
+sha256sums=('')
+
+build() {
+ cd "$srcdir"
+
+ srcdir_name="${srcdir}/SETS-${pkgver#v}"
+ cd "$srcdir_name" || return 1
+
+ mkdir -p "$srcdir_name/build-venv"
+ python -m venv build-venv
+ source build-venv/bin/activate
+
+ # Upgrade pip then install exact packages used in CI
+ pip install --upgrade pip
+ pip install -e ".[pyinst]"
+
+ # Run pyinstaller from the venv
+ # ensure we run pyinstaller in project root and output to dist/
+ pyinstaller --name SETS --onedir main.py \
+ --add-data "local:local" \
+ --icon "local/SETS_icon_small.png" \
+ --windowed
+
+ deactivate
+}
+
+package() {
+ cd "$srcdir"
+
+ srcdir_name="SETS-${pkgver#v}"
+
+ install -d "${pkgdir}/opt/sets"
+ install -d "${pkgdir}/usr/bin"
+ install -d "${pkgdir}/usr/share/applications"
+ install -d "${pkgdir}/usr/share/icons/hicolor/256x256/apps"
+
+ cp -r "${srcdir}/${srcdir_name}/dist/SETS/_internal" "${pkgdir}/opt/sets/"
+ cp "${srcdir}/${srcdir_name}/dist/SETS/SETS" "${pkgdir}/opt/sets/"
+
+ cat > "${pkgdir}/usr/bin/sets" <<'EOF'
+#!/bin/sh
+exec /opt/sets/SETS "$@"
+EOF
+
+ chmod 755 "${pkgdir}/usr/bin/sets"
+
+ # Install desktop file
+ cp "${srcdir}/${srcdir_name}/distribution/sets.desktop" "${pkgdir}/usr/share/applications/sets.desktop"
+
+ # Install an icon into the icon theme
+ if [ -f "${pkgdir}/opt/sets/_internal/local/SETS_icon_small.png" ]; then
+ install -Dm644 "${pkgdir}/opt/sets/_internal/assets/SETS_icon_small.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/sets.png"
+ fi
+}
diff --git a/distribution/arch/README.md b/distribution/arch/README.md
new file mode 100644
index 0000000..8154f1b
--- /dev/null
+++ b/distribution/arch/README.md
@@ -0,0 +1,21 @@
+# PKGBUILD / makepkg / AUR / Arch Build Reference
+
+I plan to include more information regarding the AUR here as well at a later date.
+For now this should serve as a quick reference/mini-instruction manual.
+
+- `makepkg` in isolation, will create
+ - sets---x86_64.pkg.tar.zst
+ - `v3.0.0.tar.gz`
+ - It pulls the source code from this tag (which I had to create ahead of time so as not to compile the older version), though whenever new code is introduced, that code will not be part of the tag unless updated, or a new tag is made. If it is updated, then the SHA256 hash of the tarball will change as well. So you must compute it again with `sha256sum v3.0.0` and replace the old hash with the new one inside the PKGBUILD.
+ - Whenever there is a new tag, the PyPi package should be updated as well, by running the one and only workflow that is still useful.
+ - `./src`
+ - This is the directory it will extract the tarball into. This is effectively a working directory for makepkg, in order to be able to compile/build what it needs to build first, before being able to package it.
+ - `./pkg`
+ - This too is a working directory; it is what the `.pkg.tar.zst` file contains. Once `src` is built, the relevant files are copied into the `pkg` folder, within which, there a directory structure that mirrors the Linux Filesystem Hierarchy. It will grab what it needs to grab from `src` and possibly other sources, place it in the `pkg` folder in the subdirectory that corresponds to the actual directory where those files will get placed when the package is actually installed through `pacman`
+
+
+- `makepkg -si` will do the above, but `-i/--install` actually installs the package after it's done constructing it, and `--s/--syncdeps` will also install missing dependencies if there are any (system-wide dependencies that are not managed by us, e.g. `libssl` and the like. This is convention when installing with `-i` to mimic the behavior of `pacman` when installing.
+
+
+- `pacman -U package.pkg.tar.zst` will install the package and register it with `pacman`, fetching any required dependencies that it can resolve along the way.
+ - When in doubt/sanity check, `pacman -Sy` to ensure `pacman`'s package index is up to date.
diff --git a/distribution/build_pyinstaller.sh b/distribution/build_pyinstaller.sh
new file mode 100644
index 0000000..11d9a3c
--- /dev/null
+++ b/distribution/build_pyinstaller.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+set -e
+if [ ! -d distribution ] || [ ! -f local/SETS_icon_small.png ]
+then
+ echo "[Error] Start this script from the base folder of the application"
+ exit
+fi
+
+echo "[Info] Checking for existing venv \".venv\""
+if [ ! -d ".venv" ]
+then
+ echo "[Info] No venv found. Creating venv \".venv\"..."
+ python3 -m venv .venv
+fi
+
+echo "[Info] Activating venv."
+. ".venv/bin/activate"
+
+echo "[Info] Installing (build) dependencies."
+python3 -m pip install --upgrade pip setuptools wheel
+python3 -m pip install -e ".[pyinst]"
+
+echo "[Info] Creating binary app."
+pyinstaller --noconfirm --clean --onedir --name SETS main.py \
+ --add-data local:local --windowed \
+ --icon local/SETS_icon_small.png
+
+echo "[Info] Leaving venv."
+deactivate
diff --git a/distribution/debian/DEBIAN/control b/distribution/debian/DEBIAN/control
new file mode 100644
index 0000000..588d07c
--- /dev/null
+++ b/distribution/debian/DEBIAN/control
@@ -0,0 +1,8 @@
+Package: sets
+Maintainer: Shinga
+Architecture: all
+Version: 3.0.0
+Section: misc
+Priority: optional
+Standards-Version: 4.7.2
+Description: STO Equipment and Trait Selector
diff --git a/distribution/debian/README.md b/distribution/debian/README.md
new file mode 100644
index 0000000..d0bded0
--- /dev/null
+++ b/distribution/debian/README.md
@@ -0,0 +1,31 @@
+# debian-package / dpkg Build Reference
+
+## Build Preperation
+Before building, make sure to complete the following steps:
+- Update the app version in `DEBIAN/control`.
+- Add an entry to the changelog file `changelog`.
+
+## Building
+To build and package the app for debian-based distros using docker, run
+`docker compose -f distribution/debian/build_deb.compose.yaml up` from the base directory of the
+project. This will create the .deb package and put it into the `dist/` folder.
+
+To build and package the app for debian-based distros while running a debian-based machine:
+- From the base directory of the project, run `distribution/build_pyinstaller.sh` to build the
+binary.
+- From the base directory of the project, run `distribution/debian/package_deb.sh` to package the
+app. The result will be located in the `dist/` folder.
+
+To build and package the app for debian-based distros using docker:
+- From the base directory of the project, run
+`docker compose -f distribution/debian/build_deb.compose.yaml up -d`. The result will be in the
+`dist/` folder.
+
+## File reference
+*All files relative to the `distribution/debian` folder.*
+- `DEBIAN/control`: Defines the debian package and tells dpkg what to do. Will be included as is in
+the package.
+- `copyright`: Contains copyright information for the project. Required by dpkg and will be included
+as is in the package.
+- `changelog`: Contains changelog information for the project. Required by dpkg and will be included
+as is in the package.
diff --git a/distribution/debian/build_deb.Dockerfile b/distribution/debian/build_deb.Dockerfile
new file mode 100644
index 0000000..058654a
--- /dev/null
+++ b/distribution/debian/build_deb.Dockerfile
@@ -0,0 +1,13 @@
+FROM python:3.14-trixie AS deb_build_base
+
+RUN apt-get update
+RUN apt-get install -y binutils
+RUN apt-get install -y libopencv-dev
+RUN apt-get install -y python3-opencv
+RUN apt-get install -y libxcb-cursor0
+
+FROM deb_build_base
+
+RUN mkdir /build
+COPY ./ /build/
+WORKDIR /build
diff --git a/distribution/debian/build_deb.compose.yaml b/distribution/debian/build_deb.compose.yaml
new file mode 100644
index 0000000..8814a77
--- /dev/null
+++ b/distribution/debian/build_deb.compose.yaml
@@ -0,0 +1,22 @@
+name: sets_deb_build
+
+services:
+ sets_deb_build:
+ image: setsdeb
+ build:
+ context: ../..
+ dockerfile: distribution/debian/build_deb.Dockerfile
+ container_name: running_sets_deb_build
+ volumes:
+ - type: bind
+ source: ../../dist/
+ target: /mnt/
+ read_only: false
+ command:
+ - /bin/sh
+ - -c
+ - |
+ cd /build
+ distribution/build_pyinstaller.sh
+ distribution/debian/package_deb.sh
+ cp dist/sets.deb /mnt/sets.deb
diff --git a/distribution/debian/changelog b/distribution/debian/changelog
new file mode 100644
index 0000000..66a0ce6
--- /dev/null
+++ b/distribution/debian/changelog
@@ -0,0 +1,5 @@
+sets (3.0.0-1) stable; urgency=low
+
+ * Initial release.
+
+ -- Shinga Thu, 27 Nov 2025 13:12:55 +0100
diff --git a/distribution/debian/copyright b/distribution/debian/copyright
new file mode 100644
index 0000000..dca4a1d
--- /dev/null
+++ b/distribution/debian/copyright
@@ -0,0 +1,6 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files: *
+Copyright: 2025 STOCD
+License: GPL-3 /usr/share/common-licenses/GPL-3
+
diff --git a/distribution/debian/package_deb.sh b/distribution/debian/package_deb.sh
new file mode 100644
index 0000000..566d8a3
--- /dev/null
+++ b/distribution/debian/package_deb.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+set -e
+if [ ! -d distribution ]
+then
+ echo "[Error] Start this script from the base folder of the application."
+ exit
+fi
+if [ ! -d dist ] || [ ! -d "dist/SETS" ]
+then
+ echo "[Error] Build the app before attempting to package it."
+ exit
+fi
+
+echo "[Info] Creating .deb Package..."
+PKGDIR="dist/deb-pkg"
+PKGNAME="sets"
+
+echo "[Info] Creating temporary folder for packaging \"${PKGDIR}/\"."
+mkdir -p "${PKGDIR}"
+
+echo "[Info] Cleaning \"${PKGDIR}/\" folder."
+rm -rf "${PKGDIR}"/*
+
+echo "[Info] Copying base structure."
+cp -r "distribution/debian/DEBIAN" "${PKGDIR}"
+echo "[Info] Copying copyright information."
+mkdir -p "${PKGDIR}/usr/share/doc/${PKGNAME}"
+cp "distribution/debian/copyright" "${PKGDIR}/usr/share/doc/${PKGNAME}/copyright"
+echo "[Info] Copying changelog."
+cp "distribution/debian/changelog" "${PKGDIR}/usr/share/doc/${PKGNAME}/changelog"
+gzip -9 "${PKGDIR}/usr/share/doc/${PKGNAME}/changelog"
+
+echo "[Info] Copying app."
+mkdir -p "${PKGDIR}/opt/${PKGNAME}"
+cp -r "dist/SETS"/* "${PKGDIR}/opt/${PKGNAME}/"
+
+echo "[Info] Linking app binary."
+mkdir -p "${PKGDIR}/usr/bin"
+LAUNCHCOMMAND="\"/opt/${PKGNAME}/SETS\" \"\$@\""
+cat > "${PKGDIR}/usr/bin/${PKGNAME}" < str:
diff --git a/pyproject.toml b/pyproject.toml
index bb34f20..a48ed0f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,6 +22,11 @@ dependencies = [
]
dynamic = ["version"]
+[project.optional-dependencies]
+pyinst = [
+ "pyinstaller==6.19.0"
+]
+
[project.urls]
homepage = "https://stobuilds.com/apps/sets"
repository = "https://github.com/STOCD/SETS"
@@ -34,14 +39,4 @@ sets = "main:Launcher.launch"
[tool.hatch.version]
path = "main.py"
-pattern = "\\s*version = '(?P.*)'"
-
-[tool.cxfreeze]
-executables = [
- { script = "main.py", base = "gui", icon = "local/icon", target_name = "SETS" }
-]
-
-[tool.cxfreeze.build_exe]
-include_files = ["local", "LICENSE", "README.md"]
-packages = ["PySide6", "requests", "numpy", "requests_html", "lxml_html_clean"]
-optimize = 2
+pattern = "\\s*__version__ = '(?P.*)'"
From 08e65a37458382266082493a8854b8374b7e806a Mon Sep 17 00:00:00 2001
From: Shinga13 <93780215+Shinga13@users.noreply.github.com>
Date: Wed, 13 May 2026 15:52:29 +0200
Subject: [PATCH 34/44] updating packaging
---
.gitignore | 2 +-
distribution/.gitignore | 1 -
distribution/SETS.iss | 3 ---
local/sets_icon_small.ico | Bin 1100670 -> 11540 bytes
4 files changed, 1 insertion(+), 5 deletions(-)
delete mode 100644 distribution/.gitignore
diff --git a/.gitignore b/.gitignore
index 0449b53..d181bb8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,4 @@ images/
..debug/
__pycache__/
*.py[cod]
-.SETS_settings.ini
+*.spec
diff --git a/distribution/.gitignore b/distribution/.gitignore
deleted file mode 100644
index 612654b..0000000
--- a/distribution/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.spec
diff --git a/distribution/SETS.iss b/distribution/SETS.iss
index 9b52fca..4928191 100644
--- a/distribution/SETS.iss
+++ b/distribution/SETS.iss
@@ -175,9 +175,6 @@ begin
end;
procedure removeConfig();
-var
- findRecord: TFindRec;
- FileFound: Boolean;
begin
if DirExists(configPath) then
begin
diff --git a/local/sets_icon_small.ico b/local/sets_icon_small.ico
index 7f91a8a46d40006b462b52070ade763d951348fe..35c02afa0051b6676d64b757933f23254c375f93 100644
GIT binary patch
literal 11540
zcmcI~hd-5X{P%sx%!sUPl9lYe_ewU|8EGLSMAoqhp+Zt-LRPZ(rl^QyWn}NY=kIgA
zzvp@WgQs4-s^h-zbKTeV`MlS4AqWb7V}79!8sv-zL8##0k-FMy#Duhj@K0h5b!C0b
zPceV+ap5lux7>RO!i;DrD;RpcTOIQ@F}UA=x3R9FNUp@Ta+BgN85s^&5Z2mP52cTf
z-v6m7+pCbR+}o7(+)Pd$A4wiDUKO0~ld3N-yqW(e^pi(kZEi&FK+RyxpFj2mtob$?
z%xn_-q|w+H_*uFoz8(6d=J-*(d}772zFxPyZjXFs4#9sJv5a*|u}TSrzJ$s`58y66r(pQe?82!p2b*VGWwKNC+24k2l%we|iENlj!mj^8F04
z&qMom*NpXMIYb})!LrW7nM~ZlrNZkmo?4bMJdAXDTZt@`PJ6;<$2wxhXST$rSnweZ
zIbfmEQ+7v
z!ojD8=8*av?vi^^V;6}*~1W(Ox;nrI8A&%t~W+5PhBFl
zlOR?nLyN&RD@Pgh4*LmNWnrl&ZJQ4m5S@X=AAVzKmccAXk-R06nmd^CdD=_j&wN5Z
zE7O!96<2e_@*+G<+UiB|{%a)zwMLvN$sgtj4-V>f|A3h6p&0##E**>58if8+f6C|V
zbRs$uW8JhgmzFOV<Q)<;{^+HIOP7Jx)J#_jhtU_^R$VCC?kBuK_D*5@`JW>%Tu-P&+)h?
z__4ai*3`X!Qa;bBUupQ3FHbCUND!-wxClDECm4-QVA;Q9D8
zuZzQ-H>~58>y1o)qF~I0a$N1~!h90ri7gpe@iSySR_w(dOoi_~0bKb)C`i;l_W=M*dT>JZ6Y6N#IE%Q
z&DVHB&g8d4_VVS+Dqiw`arUxEAG=dM!33qS=&{4@+J37o4eoSDOzXtio|yakI0G>;
z@y6a>$I0Qg>*|O|QIwjv^)D=!#@KR)g(bS%2Tsy6sR>NTmoHy7=2Dzl+1YWC$=TV^
zl@-Ujy%C3nzU+&g*;g$tVZXV57Nx_1dsW}>U;c-0fgdro%gEb)RHs>KIn!T!N3QPt
z>{wk>^U>ek!#EQj)mTOuX7WhavlEF0cS(|)8WukUxpIB{8{Vt#yWCtq3Ql;O(s5l>
zl}!>Q#Mp7bjEGoO1+nHb-1~{ZdY4Wo6$;
zJYiRjra_gM{9RkV_v3A7iCt&SXZH{Xs8YlU&sY7nb4!O(wn=@DjM{Nv$K2_Y9+~im
z{U1$t?~)P_H2*$!;h5rfbs!Icc$ZS(-Xq%V(R`jg##j
zCH7YmdF~;6JlJS-u)NcIGCI5$MS5
z8itSS{8igV5Y^9?Xegcv#v834Pw;ZZTrA(du%p54|9GpF>HMJ66YrE!EnELzjM%vR
zqeq{xZ>k-9Jmf*knwFg{(7`-OIE-U!M8+*zK(htKYo&WJrV{z@!WbA;&KBQ1!j7
z8zypmeathpG4K0#t&_ok)1=nhTO~YEMV7U;e+o?T2?$&l$_e%_R5MhWIXG&~xu*9y
z?Qe@wUbzx^+ba3t_Up8Any}^wva1_A{W?l)_IEYrqO-FzxJZ(fmDR#cE?zw(@-m0*$-$=Z
z^+^_o#KfK;&
zO%MKiJFla~9Eq_)YAUKQYAY`k{CbxVIMl;>HIBnD;gi`_EhV-
zO1={;gnjZ*Ef@=6nqU}6Z*KE=`0gq%FA3)9`gt=ooK&n=uL^i?-F0939l4x&iLTnu
zc#^x2?9Mt@Mgtm!k*TU#x!bYvGrIrasKg3nHm
zYkh;s7sb0jq)VUG8x7EwtTp*LZuNSfCE^m%pk1dsst4$66*HP(#xBv(b$jSVQ2zZm
zTAU|fJ=T2uRfaXXYkhS!M(%7g7N3}SwuVzQDmJzgzCdHK^)I^po4l_h^57BikW21`
zlvpqS(h0}Ora&4Aw-1)#J}xTojx8Ys5wWrJHJmd7UQC%H<*JV2CP9CFV@7kB``BXBZQDnHV(jBKQLpo#I;?d%%v>+F`lPD~7ozp7SaG|_ks
zKu_Zpd&cczD+}t-vQ|N4gU#CE5gqB$UyDia+M^LW*%%HUp6KPwp&OEtZ%JroCAtfm
zEM8~g+$uyZGszx$5L+a5lnV;>$=pM3L5o0hs%vQctKs}WoM;*-v3Ael5$*BG#N5T@
zZ=}Mqr=2l$>v|j;%BrfYQc|N=vEFh_4LE1RTGj;b%kYM$m19=s+0KK?wc|>*1Wf^0
zq@Bh7@a4=n;|+G?P13&Ti@hh7w1Yx9LvBe%0TmI?pJ!e{rG6PA`L04LzwHt_@l-8T
zsAobS*pAJ<_eCtKSvx#Z&wq5HN#f9`sqa+14Xo!Wmi->;C!E
zbC^KRA|fIp{X(X6+u|?lx6q<%4dFM`@^MP)DsSlN>3Pj1-V`q1>
zr05H=S7oMFL7>9&J*n-EHy_c2mNXULx~ldzz1Re0;oiEnZ0sO_TKY9m(!1(OSEq#eLrWx-mLuK0}^$
zw5ox({Sg|Wdqh^gn<`1yPWY3xs=T+^scx8s#L6DwH=b-=+<7L$Yf*(hIq{_-!c|sN
zQ_9d4pVe5MpAVn#KPJB4lj?C$9=ZA!2g#0Z&h`I!5ivYWd8N*F(4TfT^X2u3Ss~SE
zg;T4~dVRRlXoW&fzsRw_iV6-1lWa8R{)~(a{EHW#96VNzR1QL;k(JR(QV|gml6uh+
z%WW!o4&1CGDGsQ5+LY5#ZQWThI#EX&tAJCFoSc497^~m0C>5_o1gYG~B{Ao5$Cb76
zAbV2^O3Hstf!MkE`APtLzw!(?!Ygon{rp^)|J-;d7cg!!$i`roO!k!k+l_Jnw_wCT
zE1{79kOyet;OIzlm6515y`q9DMbxPUBSt8%8>bf){rt2)-Wb4ik(#${yGJhe^mtz>
ze?XE4pOb=6>kWUiQ>B#8%X>GFH0>8m*qp_J*6l?;Lz~z}XCFcMVU(I;w!Xyd5&J8G
z!Qwle;Y|J!Dp6Gb_Etw3YJRScR=OT+*t||iU;zaHnu&zqLSSHEmG7ZokyZVp{c`nA
zk&6g-DE?J?aa`Sx0@jD{Y>!X4?14JFBGl;U!vW*Q35iW`kXI6K6V-@`&-SBX9
zetJ3>bZ5)x=qSse5N%AA$1+vo4f8PPhQp4jJW%Q|Epf@ok*{A&;#fU1y)-I9hrMCMPZPg%(Fm^-=@Q2EEuUxfOwH_?NQ{mq
zsykZBCrLiaFDmL-8>`*?DYb45O+)2(3jMD2UXn84Y&&gprL-s0d7=RyA0KVV9csek
zFZ`yHpGcXNtj$(H(qk#)^Jjj{;Dv-Pv#_vWI)j^=n`h1WvBW#6rzEhDWMpKbc%q?-
zfnUA|QczJ%V}q34kkPG&Dg4tL0&(k{p|MC)owON;JvX%wmwE3(TKaGTV7p+
zwZOdmRC`p3*QPCe&iA8%k32FiHTA{Qac?}n`WE~n>o7}5
zT){xd+TZH_C5QEi##p<~7a3o^khg{rm*!mqzcIPI9935*ZD?$4b}BnXL!yDNuRn~Y
ztX;F2*Z6d@y7(9>W2GuFt`G6wo&df@h*_W87c7h79f_a>;L+TRF2B$qv$xg58hRxRtdHQh
z*EI3T(Q@&@>Hfs1ucg}8SvTp!nO8#8wr>CFGvxOv@#rL$
zjDjMB?5e8!gTXZ8{B}^*{62e*JmwVz16`zusi~>g@oMG!m--fLiMF0M6m@;u+FgY$MDLh%|{?yF3
zS5C%XpoTD7Ut2~;p8eIo>iJZMCxrc}MPZedVqo1Sr>Fb>Ib0g`$Q3f%N=J1+9dBJ@
z%G)vrD`fLKSC7GJ&)mQucxh=#w2?1f9aGdqjM77S-$YFkRZL81QoZMrjXMQ0v$CY>
zzWn;DqN%N2QTe6~R}>E|cniDXBsKM<0wXWLU`YxeK!s*zX7(1ESBlk8jua~7-ZkGCZtY+9Ktf@)0M8PO*TxMzM
zy4Lmm-Cd_A{+1wgKt}M&sMa|Q78Tw**xlQ!$j4!Yp@K3{R#6!?khaCnXVRE8!sQwD
zr}kO+=KLxy?h+A`EWKcILF*uY@sPtUz^r~z%5s$q-Exc!FgeHei6=Bee1RMzatU-_rgVABDkHYskw}x
z7JQ+w@bB3l_F*=?wNITd;NqJ3j*&?|UBfV0xYHX}r&5{=!j*tF#3mKNZ%_9YMl1Pj
z?d(2F0Uv*tJ^vnTRkBeYL<{Tlwe?{Bq`
z>I-^QsN%o4<#Lg)ok`7t9+TTxe&e
zQf+;_{#RqbTP1E~pPSCr_z2)=SsBId?rvGdLUBG&6SW;HfsST>)>-X>OCTS)-RINQdJ+}P65()3~)3_XN+1FkU!HPM)OiN3<
z((rTo%|4#^Cpr}#J-lv$XZve0ZEecoGGtf69(oW>g5|4nn|*GypI1=OHtO6Mqa$}p
zQC?o|nV^=_|L=RU(2pRrkQp+U_&Q%(o1B#pA?bh-_xJyspPzs2AZBE08c8Si*kpis
zV|TY5jl$ko%t^By{$k(zK$wA7&D8V?;J}|12liLrxJtxy0{oxe{4-XGcPR`FuWlVX7&%xz_%}i~|REgS1YCORiEV3+>;E?P7BK~xZ
z-v4NkBV~M050DUWB_i6j2q5V>xsN7sm
z4A$#%Czw<|W;i=;Jf}0Sd|aLL<4#N{FCZ>rD3&NRAPDC6+qmJGT
zr5mZg_XJ~6Wuhc5Ag>(9YRZRSTlnCT&_{rAP+%kf2ax2o{X+nX=MewlO3;j^6&YEhOlTvA_kSJ}qoS%BX5cxO-0xX=SD5y{
zYh}I7Uu#}tzx8(KL7|_ktAKH#S*U60y?=$3<1IkUdZwlX!Ock4uY(1-7}a3Dh6Ajd
zNxY%I9+XuTDYLqcWHLZTP*x_aNi}tWLZ5;2+O_PlHin=2fcibM$3y|=#}kE`c|ZVs
z(#I>MnAs%4#nxiK{QUXz4G?`8T2Q}rs~tp4x_(~M7ari=7mPB`gYif!p2?!G!yMhc
zdv~&vDPU&iEcH1SIv8>>A{zcCE;wg0Gh6I-;<5+l-hSwefaRf8e~?iANfrP
zO=e~$A&CGIF)uxq_S!(ay}iG7cl+#kgxN4kwXCmPtA!QKQjKZph@l(RdamiHW{Kb}
zj@L^a?Jm!kJqJZMLd*+|Wm4^S4NU+3P*>L%p5Ko|?)`ihIkYy$6q$A7RQZ^WD#AOEeg8Q2AY-k~qE>mN8NQKtK*RLg$~gOie|&yEHgWkvBJ__KxU#C`U7
zmX?>vXlU?3=S)sohlGbi|C9P1?}dW@)zl=MagzWi!1C(b+Z%eGM%qCO3lDZyN0VT!
zAe%;i_`rrK^zl|{N_MI!8-5yigI^EY-ikONh;!qqXi!klNI^QNLc5M=1tTL$%y%&{
zk(QegXX{4-iA5-9swLEm6ltqw>T>eC&8YhVuk#Ma%`9>vLtxUhF>vxDw^(s#2I}{U&^_b<5&oa(qbH#@G&{?Kqg|dcQkpM$|g=yGN9U=wNTud2?Rw{K|Y!n)A~SR&3-lOD!AaTPdL-A?VTW64vOA%np%`}-aw|pDwEpg!o)7w5Al@IxzkWTWO=QhD80vDHJn{l`
z59`tX`ufY9cbN}2EhZKoC<#m>ei&SOe&8`|+_s!Y`KVDS@6ib~ZRh?KIj
zvNhxqybjN~6EthJd+GzuWyk&YE$cQr=)?ogeDfWS#zNE5m;?m{IUnY}U&y&14y)_F
zIx=9t58U*Os%74i6(*2TnVVKqKaP)&S9xu+
z77~2M#zB)1l9F~h6xYx739Wp;fjk+s0}VtY<;eqm6%G{yM6I@c_y?>M3|{&LlN?F<
zrkC?m$rwodGVAK-kN4LhaCsbsC67mZMeqV-ztfKVF*Y<`?+P9q9FGPZ`3c0y~z
z=p#XxDP`*eJE6ep$2u{9<;zGDIT3fHl^-K=
z!$=sqqVAP?YZU3{Dc`-zhDnruWvN}ge*I!FE^&}qVHgy+pPx*bnN$8CKMfHlH+Ku9
zI2h{$Jk6P)nZzhHPWE2Xqb2)hl0{-xZ@(_GfCk6?JynJdasmtL>|1~^&CSgV!=)Ty
zL|1~~>2|&83K5Z!3f9)l?vj@I)rf~Aga65<8U+PKI>>n7ETFbom2`&~W!6B|tY(P7
zEWcbE{_oKsP3w00Zj$tz&J)dz`D$j9kJWf6nVHc-xDf~frLD75Zsgx>0|V@OtMlAi
zT{E-ikY!K8zDMq-Pm1t{6ofeI#uEZW*wD&BL0Fm;eJBbxQJQ5?JAMEA2Q&R
z)wQ)TL?mF_h96BYK`!%b2Mt0O3X}W#^^Cz~*ZH3eV0Wf~uP;TlLz*3E!8>mh)%2IR
z%H(pC`81fC%G&CV_v&oqkobb+q=&{kh&?|b#XR=(Xg3(D*4Nip7u50f
z+eOTLe5Amx;RMUR_PSNqk&K=`
z8XP7eBASUGAau3`VM(odaCsp%4AD!&cEu!PsNg?xx
zrc*pWJFVsoGtWS*gJ)*$)>cfD&%e~nrV+f0Kac%#Ed&dl07b8FV6fQQZa3!}I94dW
z5+V$v%-+~=*;yMi-#86uxJW>dk=5oTk80Aaf3~Zb#B1r5#&56yFb<(2m#%pvJOu)4
z9Ff!cpJBw^?hik*KZU^(vc^Yj^;E}00~67U$1U4d;T$QOXFyu@U$jiqrU?cg`ev$K
z7N;Q#u?~f5%{AmMifX>Ox?5ho6fQ&*470X`YZ5r8x5DrdL5Qj`X93C>VS|H3K!8j?
z@AY!#239LSBxbqA#cKBU*Mw<_ySlppcQ!fG&=!zZrZE8rUb-|%%EUwo7^}=0-;Jv`
z@JQrnZ`EN}Z&6VNH7Nj69_F8ILC-$X{#S1=8YQTP_(e1ryvaNf5HVx)Q68BZ6Wt56H8|(V>l3ixs6Xr
ziGoQeiki|VYHfNTJcU_h_eF9NM(GHM^dKrz$R8LvVY^R*3&E4Rj*gnArzl{}^N0u+
zFK>xrdyk`?hnNWB>H4_Gh|FJxa%2Q_IcCZ$PX_M_{sXNsRO!q_LNAVnip{918Y*)5
z!gCSi7m)71-i%YwQ$5Abl%o)PV~u-2oiJ-!A)0JSLlZ)e1=PiH~~tlz-F7;
zkMk6mfJT3Y?!+6d5I%a3@%-%WR(kl5>uf8j(HfY~6K4uA0s?q_TRK(x|q00X;bKbByp2
zs!S0GT~>B{_4xe%29P|aK@3u!=^sCaEwg%j9jkqc@e@cBY{)r2Cg3N+08B_&*i_|&
z{}V-mF=`yNF;E`(%GMh_XF~nUyWpNM>6M;|$%3)Y=^r3oo3oR{IL&OYrMw(RtLvAb
zqd*H1A~$rY(G}KTU%~LQPVCENw>9xUd(_hO-_F3as3v&*0pOmK%1WH
z4jXL0u^t54(6qI;E94o%PC?38gC8CAP&-GO?^Jxk#LCh@{z0eQSs;+b@b4N-76uC8
zor&N+)FuueA0OOVZ6wJB`v;4C*8ukMFq-(+gP9t2?WZe40z4+4kxJ)@iX$1=TYNoI
zj{`3U7>Bv*7M5;sGrFZk5feNP)jlom+}STIdAe@pd$QSS%(oG*nGH}74w1mCn{%>%
zrw!H=;TaLp(e6>tKMXFHfP2AwUykF71;VOO``UJGgWKNp@Gso^9ojF3}myfG%QTGfl
z*~7+3apid;EbXsMl>^vr&iM79ikHOMqRNHrW+IRG%1XF0t193D^XZjGYt
zO$#h*E5?fCK_i0^wX_gC%Rn~b05NOa7h9rG@&IU{z^tJ!#N8Jzf!(-C8-oi-;ZS-V
zGQ-wo)ScUKgRYAUKi}IB++Y+Ajv7C7A-$~c&<$g@Eh8Hnn-ohF1QhUvp5i~=pzWKPG>8COZiO~syd_5@^e|h(r<4767MJk@Aa)&{SXU-XiaN?xI
z_i*dpF$YZ;rU)_mjps@wCxYOUkSLm%yt%3NcX@dkL&c=B2MCC@{O+~ucYXF&6s)YQ
zN}XE4T4y87?kH3sJ`s`o@F}n!q^G!;W`v!JM7Y>}c6QdJ(ciBWr2uL;B;*3g&Gs8`
zbg`wS#X=M@xqUmlxZ#KpHYHd<1}?tzc^S~%)6)r20TVI)b6{B5B;Dq+F49GiUPw2R
zE+7Eg#f@jQ04?q7jpu7S$7DC*;L%Xo!^;p#;U?Mx$3s#`#>n{7^u&j^BPsNP2moD?e29%OU6aXwBpAG)!+*rIJ>f(eR9yVf5}al^sXMc=*Q
z`?QSwNTbbWedh3;O#MGccIhG6VFd{QOPW{ILd#F+B8s0Gc~i
zko)3puWmCQIz62^=K>m!l+k*zFZ%!v{pgvSFK;YgKE|klM*sKAwl115UNDDqSs$4?
zedXsN34x<;R+rros7!o9!Y@Z5rVhYh({QW@4??Gbdz##e2a3I@rta>Gpb}h%D6D~m
zWzP=me0K+K$74u<1n<<4kerar>4v#@k*w0>Th4*_TQmnxrq
zUMe0FG%=lMlQqA1u6u}h8vGcJV?f%lQq$3Rw#&z3QFRIQVA+z3rs9EeFG@boaKuCP
zFKo$eHsdi_Lyy9NM00XqWZ^3XGd$9Bm3FQtEf(Na|EK>{#RUe$kUQZA3LwCu&WjAQ
zhnXtlbD`SzwHJv39&+c-ofItOf8)csA1UFW3*jW>DuF1%PlOGpWb$*67}cxV?HfiFFezk_GcA2&Q?xM{v0&dD6LOs$_5|pk$xi*cK!<=UGjl>28X4IPf9cscoWGM^P!Jvb
zl59#chy=MIgfcQTwBDR*Hz|Ka356oTeTAjy>JS1N2~LzwP1$0m7q~NPwN9BIo(9oy
zZ*MOGyb6g2=9t`2owrcoGDlDga%tw7F({S)b0%V_F+h%lK~njqPvj)NDDp;tNQp8H
zKXDQpVNqlssz*XD5h5Y@v^a?3g=j2<1xxk+9p=?Tn3bD2kxQ)B6bL>UKjt!;|4*0g
aofGbnX@8MuSch}Xh=z)`a*?8S(Ek7@MG$@Ud3>mL^N@jHpk)q<#;CGb>xpLE_C`zS5
zrAhNZbNuhUh9B<#!@2kQ9jEi0XP;gJ{qMKW
zfd^DN;F^VH4yd5(DTlwq$u+qP{TJ9a$ipo8*#h94jR0SG_<0yYwm!X-)GZll4jT$vlALefeKmY;|fPh5=`uFdj=)=q)d-~~L
z-+%w{+`0AZ*S9EXmvu1yIo}Q|00I#B4}o*fJ=eMBC0cvh
zpR9A|&d45R2m%m*00b-}piQNJ{PD-UmYkCdX{2^b+xO_BkCrZ7I<^2wI0PU70SH(_pj^3f
zOO`CjyevJl?CIAbLxvo7*kRVB8ABle0SG`Ko&>ao^t5Ty94^sID}K^TH4{gT8pUqZ
z@hn258Uhf2fHeekTF@(F$GVutlB|EyJbm7J>#g$T%UhFj421v$AOL~55zxV~&ph*t
z_E<^QFc;UF1)~Pv)TvXeRH+g-hNKq)5P$##>>yCMaAD28)CMaqbZ-Ag)JOWk2Ord~
zUE7YtV6|%p8Z>ASH;kkg0uX=z1S}wM(M1>i
z```a|r*LVXQmgTkPd;hVq=^NI$3zG~00Iz*6@jKrn|j#JOQ@h^PfH+Lv}h44k|Y)a
z5P$##Oeb*CNhj^yyH^u0(>B`NeW|@&{`Y_Xcjd~JZQHgrJ^k1R0SG_<0`VbGw{Be>
zmEd|=dInL{*xvf}>)W?)A0L>c6ao-{00fLCP_t&u`Sa&zb)rxPolE&i!EV~LseAYC
zMkByD2tWV=5Qqr@t*DzaWlHW#^itF=?@ujY*t&JAW``bh&_OXlN-`k;0SG|APy&Y^
ze)!n2W4&6UmnU_}7l|YHuD$l!Lk>B_Pzsm@0SG_<0&yTvqC|;jpMBQzshi1~r)$aH
zjvYI0yX`h)FAjx@G(rFZ5HOTLp+bcoeDFbUm*}MkUgA%6a_-u->*0qVrjygqqJUWt
zfB*#IK;W8dt_eW%qH<#aAiOHbRL%lxGl
z0&4Z1dQ-K%wTUdS2?7v+00g2<;M{Z1-M)RhA4~Mo>R)30`>p>g1H{O`JFp*^6)iBWVzT00hh;FnI9bV3sTFub>Ipn>=}P
z)v8s^f`T;=fB*y_5J>{}-FKhgTe0k~cFEspY){*MR;yMml6XlJ1Rwwb2pB}*y6djf
zvN>;0(My&zC)b)pGi}2k*S|PVmdpGt1r=Uwm=qnP*0cGiiYU1RwwbBM6KdHOicJUP24i
z+WTVZ(zBa4Hv$kwKmY;|fIwsj=rmW2r3G`MP=@AR%1@HvWy_YGbIv)D;Y>mx009U<
zAbbK>U3HaC6bg5VUW(u){?wEf$=>-FTyWrl2ZoOc3m^ai2tXhL1aykt#*G_8U!u3a
z>?LM$u3o*mef#zi;7k%A009UJ
zwBF>_uV4SlE3dHGtkJoJagikO!3Q6hta;KxnYU@vrk*`}Mv^~?iWPz9o_kJ13F=wV
zN81k#9Xhmd;li;ZKw=>fGJ!E;#+Wv5bN@D<9E4=ASFc`LQy4NYqKAN60vbM73r~eP
zQNlGpdCr_U#~ypE+wgb}0uacaz=IDy`0cmfnlF25?P*8sK7IP|UYva<^F;UrN|!GE
z%Gj~VTR>=X^30htk3Rb7@QuR)2*ihg8c(}-?>1la5}lmewr#uq`s-=!#ix8mCZ%1w
zcHOgQ&%gfp&%AC7o!I=+OD`RK@WGKuBME_k1q7NkYqn#@4l6WI&O-e{T2Oe)Ew@-e
z784=xUjkLCRMCN1$&EbKI2GpGZ@>M&xA0#GKp+qTs*P$q*`j%pJxu}FxpQZ~e*FT$
zLnIK$m4Lb>bUt&+0VWc>b?erhfByNohR*X4fIwCRw9&~|UwviZRxEj4K>J@v_S7GX
z>}8eF+#D7Gt;SOoOWFBB6Ied}_+$MPmQ}>YB*A@R*Q
zQf@o}HS(T%>Z!cBnt3m9n!qxC{CMNd$3O@~k-+fb!>!Z2gzW9xx37Qy{zZ!xjbgTt
zrYI25G03}i?aGt7#D#_uRCMpX_ueR^kTgKRIs$+C)1NFF&r5b6>MzpHoH`wX)}D2x
zE)E83PUfUZllFh1RJqmeLRzjD2MDAQ0>%+&-MY1EA(v1~KQ`xPldXgRZ>t
zO10CV{F|tA76Z=lk!Flu}*o`ctlF
z^5n^EX&7d~idozZ8#esrn{U*T%de(L@Rl!M&NQo-l?#$<9sy1IP^&01LwX61BiAF@
zn>uxBty;CrLykpJBcMj!-~ayi%=4cUbFA1lC~A36daNU$CuvR$mX=HQ
z8Z>AS&731u1`s&<=%be{Tb8+zCpnZT)~Qp+0DCb30S~EQB4*S>#o(U
zv~c0VX3d&qg@v0TP#}R09Xf2=wynS^V88zQt2QKKSJ(o1asvc{AW*w@?TvS%~U`^7|^we`&w+;d*am0gwwrHO+WlDwxd3l9Uoef&99uF#{5(35((A3SbW5-6d
zdo$1PNYq!XSaHDx7Z{%*7-#_j?O}B1U3dNX(@&XuHC6o73S6^h&B-U9Y(c6p5dtwG
zFkrxds7_z`L_`?sYR;@zzVp6JoO{TVr-n40xlMw+;q|$OA_6G7bI|v*C*%7#=
zZ(nuM6!^xPtRI$MYc_#iy4!c?p!f9b7;!fQ{!8G{Ll4z4cn(*!>TUAjhadj$qx=^F
z5HNtiMHgMPb?a6qFRhkJU9#Ddy^WhTb?ep**)yQ%S!u$x*Iw&z{9D_vXq7_u?%l0S
zCq_dcssuFHuz2xeht)V)KP}}by<;p9sjB19GF8>JBuU~(|h7A%kCxu^iN;T`G
zlTPx_9zuoy1hOWel|$N~GLpNo1k
z4SVB_H%gQ!5pQgy9s=eO7&U5?RuQGt*l7Q$zTEBGx8Hi}t>)zh78y$5mRoP#_wBb1
zN4|BN)KM$*3{4DXK_Jcq9(w2@HGLdb=E!KFWN+85U6MV<_TpUl9j0Bo`DnR|s)@t$
zuhOM1^e$bxI1HcLAOL~j2wc{?xBB{GGL0qKXC-DC+ZC
znu!9Y!Mr6)mT1oh3zLh<5QsVfZOx^%YtG+clV!v9YssD(xDP(~VDaL`qn^*C&n1DI
zZ@yWlD?6OJlBap^zWc7r_;?Hg5b%}25l0-cV8Md;Ox;Y;X=$GHe(>PIrAwFgl{10c
zNZ`~{Pt_Y?R<%PVN~;tec;EpW(~Hdzhy{TuQ>SLtb(10+w*M(7C8suWIPGI
zs|Jpr2u&oQGtf1~$wi;PhQ@WSUeTgOO-vOwK>z|Gpu=M|iBZiR*P0Q52bFv^cK`Ob
zztyNwgHi#3Q%*TWbGxz{#!J)!6{MG6ei?uefPlFK&N}NX9i-=?dPhL&l9M2BrQJuT
zO`FES*5(%0KrAa;w(Q)wbKTVlombqeSFb?KBoYWfz!d?FTWJ5~7%o>x7QDP`wf1Ju
znZvG8u8L9=9y;itgN6+o=DOQs=gytgs#S{u2+{z72osn!YnHpW+B~U<(uHJi{`~nI
zupZ$8pCM^#rS0Cm+g)|gQ*-9b(O6)H@c9V>5D1@uChJE@v~o|2WN+!xrS03d4?l0P
zAV30Y-+%GN7g;S%NG@#ZEMb*GfZ0V<5O7OC@3vZan_PRjmg8H$em!F|Zp)N?&y_1z
zZtU2x%6Hctn|k()GtRItni
zhZZNe>(x}r(Sxg3udYy`LgZPIKnPe*ph%G-n>KHD{d$`s6;b+Ay;a&)eB_Zw
z*q_Xd((1hxwQAK`v}low73j&OMQ3tde);9z8%Kx`fPm)&)ORPJp36&aa^;WYwcg^N
zd+s@HPwF{*0t}acT4t|~AMgA;*tEr4?~588V2Mk(SxM{=$bvwJ4jpo=_Gx`Vl)lt~
z@;X+AGh(tRRW{su@4fftUh;qX@yBV?r&nR`Iva8v8zJx;0d?=tx0zX?)yb(f3{$2|
z;dMSTb>@;wF4?|)d+sCI|Ni&CA005@;DZm23>gvvfrt~(ZY)}vME7QavZqeYPd@qN
zjHXQ^ULazSJZ-A^`RAYKIE<$PBgs@CojZ4q0W*>afv6BDQKG~vue>6Mp4sX1s~nuV
z!gQ?K`RAV>mCTQ0Ix1JLta)2%xaVG^RUV|BwEp8*j7YP+1hiq2X5?$gDt8wUhY-{t
zR-ah!-n|PKE^Ke6$JX*<#fl9XGDL!xd#|QSyJl=^F+0O3_lbVap+koa=N|
zjBxBP4^jO_Z3PYIygp$<-MV!tM&=V}(4avsyGZX}jCJPXhaY~Z(LwXmj)f3_z#j-`
zkV6M7XmDcxip@6}2qa`rhsbCi;2(hUhhYS?rN9RteBkXwErs^ti!U0MP|Se<1e_7L
z^2#f<(kO#}=clv;5}ll1e)(mqR;`?o=B}U#lqyx~%{Sl7effEE0nqD?rZQc1)m1?!
zhWH==0Z$0DZrxgSm?6Dnzvlc}+%zECwQE-z~55zsUiEx7i!^QS<^5v3dCQZ<>gfB*i69d=l7*+xtm31~Z6t;5lN
zn4V13OfIg{ycsiQWQ?32Apik`36v>Q=G9kUWyM0WU(5DdvL{#g^2;wDee}@=XLVdm
zli=y#U$wuLDcNRa=KGb=I%%1M(c@B*NM}?D3>YwgX)KwYU4g&K8>+3W>3pZ3etJ~P
zi7n~U%7k_6*7?zOp5H2G-!^U9R1=GQT7ILR(&fvSbC|II*+>i!@RLBZX3gX#yq(?_F+Fph95pY#{`zZm
zf3f4fpJg_E*jh2^?NYpyeAjEuh!G>w$IZ78fPgIoDpjg9Yt}5aQBrC+f4U=}$($PX
z88c=Kvs!|z0yS&aT(Dq)=VN%uWkBt`_U+pTnJD6d00ev?pbc}`Y&zMy<#??If&Aml
znKLyl)|U(;&_5EWUcI{3p8G0zDiT`NQ?6XOKgP^o5P*P%1n#@t%^L;J^s#U8tY0@M=W@)Ar0CoOfb@kP`n98#dfPg6k^rpOQ
z*)qT1lvA8YO#alHg!jKmPE8P9P~?zPyL_6BGm>
z5GH{qpL|k#Xa(Rt_Lqd1e53w1t!NhKk|j%qnbVPo`>3Oi(i9=Jp@S*0+O$E3#Io!%
z5@m-(K;U-*9WTCEBTvD2keGjv2PPysHlP5EuGnZ;tt5z)?^9);#8CD!H2Ld@0c>3w5gXt{FPYGi6y#es6lv;S2X%%E1sbllA&ptcllvA8r%bgH_fE@%hq_=nP-XI+cafyJ2rZlldTb{OL
zmPERWqODH`4H~p-*RD|3(x@%|+H0>BD^@I>X?zC(2-r+O6?@*ic?{_Vuf%Fz%b{yK
z$44G{M5p-LT!=ij{m3JaeB;eGl_Q}P%S6X!|Ni|wP8GpH00Jfu(2&W~g9isWBQAJ#
zF*!!Lb$#Z}ovQ|4i4rADC=%h?aQYc%=p{_Oi(z%S>qX?NufA&4s#Ulgh#dkD@Qc7D
zms}F?908L>C=}b&EaKKpE(r*vwPWY3;GSFKv5@y<|kArYx!pFMka>C&bBs&9k~
z0SJUkpiZ4SnnNGfYjud;&8VowUz0T!FJ7#@%3^p3pSF#D>ZzwRv>a}iL!x7I*sx*Y
zR!L%q00ev_P_be~z1W89YmAbg5RFkIL+>BjoLY++3Kc5kW3HPN^aU4OFk{9HNxr&y
zLxiuWWq^*nzxd*dO~QjM5P*QE1d10guI0CMZ-(n_^J16V{rQ()Hg4QFdi3ae_3C+=
z)y4&;F2n~OcwobZ4H7&PdK%QptmS|)Y-bx+GB5}Nz7puwtCu#O$aA|~gib)tS07EL
zc=XXn)e!eJ_rnUjNs}hircIO7g|Z+aZ5GRzzy9^FVF4jt2tXhp0`fkZG#`4;Qd&0@
z=Pwh=r-Rq8x#pTEoj|JtdhWmfeib<-(1hk)UK#uL+i$ye?HW+6BN_-mAS?npKlc6i
z-w$`bT%OLAKmYvm
zN~1ZQnaRM_&+*0^Z)nf6bmsFN1RxMC0#{#sb%+arlU)zj83-tk)Ew6?)py->SN;0+
zt(e=XeGk-mK6vn8ZJsIFHnt00QPZpk>+QRn$DUWt0QXduESUl0SGuJaN~_P?%A`)*x?rQ
z{bq<3N>`L|iGQ_$*+UOKq#?9Ql_~{uAWqSuMRgXSrl@GpPSUG>*Mu=40?8_BS-X4p
z?h{Wu(RpRzP6$B2dIHUwHPccsy_jd^FSyw>0`lPMOH^mwf&~k-!;Ti5oqY1i>bsUQ
zd00l(B+_E}1`QgtYuE0v#~zc=s;^a%TG6UY$ue0!Y0@OUB6!G$pdbJN(+Ozplon2@
z{-<~Z{+DAAC?V`_-z`7_eqzTK%e|RXUmnYkK{}J_ta-a{{AAkNy|!JWC}mf3wxuC}omo
zmjr4LWli{;K7G250v|qn_`?rBeA{ieX$nlIPMx$`{Jb`8&OPru9p=}jO`G=Z+iMlP
zev%&O(1Vd9M{1Ubj`~}-e!T)rgl5ZdUH)u-+gDmckF8s`_PjO`AOs*_I)Q7hy*4rR
zAcK$KC#wlauGI6HXq-v6)ElV{w|4B4u1N`k
zH4`f#00BP;Xwj8=d!jvr6G`_?qOv8Rp~`Q*`R4M=FZZJ!5hes6U#>-|Vwd~IS{DRV$hAn(2Hph7-`%_&V^6?oC_VH+H%ppyI7X_u3lU@XCmJ
z5P*R11hf<5U;gqJ?bhMKmGB4ztRNuGd;9ITeXmPI009VCL7+#E9-O6Th402lcLbz)
zJukh~ipq$Q5P*R11awA>_F$xY)15!%Df0`Kr=QcOqg)UA&1yfAF&ex5b&2k;lhP=*0#3ab>UEW1OkQ=&{+ySd-n9V3K2R4
zAYd1Pi!Z*|(qpd;cet1rE&;7lP{$^FY})0HuoeQo6FBk26F>j_bID$~9*Wo@;4J|)
z^2UuDr!(JuuR=rs0SH(`piG%EufF;!vp2nUxP%ru0iA!#Qgn+P5vD>QI0F6q_y76l
zpV@CZbT^FwUJ=j+Lu=QrRUbxhwTPG?00CPFv}@OH)22=AL*|vkC7jR+sFC;b%P*@B
z!pp4zIgHC!POsPf&c{U
zB2d46{Z*@0F{GEf!zF}p2_&Xs-Eqeqc2z*Eg#ZMCCNN{h3|8rd>#seJT>_w0DB5Qy
zK9h1)hLz}F5qS9Fhrj*y+xR#~&jrSu0JYDgbWR_cbo0$O_vzDT
z@7}%UnCGh{8kTNgojP^W5=@_BHz!y@*t2I39h)Jz52A#Cbp#GR_+XArH^+TDu|(sq
zi6PisyLQRffBEH?#~**Zj?O7twrooAJK=;A;;_|@8o2A%uNQ=r_C*^FV?
z99X=9qhaZ^5cm=T5P(1`fesxyuv{T92c1#x72sh5=+`FgX)=Mqw<
z=|#^z`|OAjBRY2Mm{0Eg;p=0MJ+@b`UY~vTnI4ZxhANpFkk+O`e*nQB5P$##@*|)V
zr6<1qwmOJ>_2UFyAOU4TLhAnh_rG6p#T7T+cw_hO-48tQ!2B{U?|XG`YI^}q!F}(&
z_Y{KqKh^XpFuG{mpr-IU@4VxLQXVcYAOL|_6Og}<$Baoom}!n);-prcM%^^oL@UfR
z0(buT=hv)R(_x7$RjSmz_ue~k;>3OX_DSuOiSbdpL{>`RyLId45Er*W00I#BjesWj
zsgFn>(6d5#ssci!W;OYE}Tb83GW9I{~@f`|rO$
z4r{qwiYS
z*F1R;0uYEV0gWW7#egSup>6v=q84*vHp=wr)5nY%Gj{CQn{K)(m$IeqyH>4QO`SSb
z!7|$@0bIf_`9J#TqjLevvk-tlObKX6Y3{sv>VMncgY%6;0vcCKNS)eqci(;YLk~Sv
zy?XT=%aabr(Hsq_rrL|-PI5*F^`Jq63S6z2BYz%;00g2*Kr0po4<0N(lI&Kvc0xc>
zl#n{*TgIpn&8QKp*{(99k*}a-V@FQ0s#obk-(*w
zUMj~L=Q$C{UNytDDCwiVwZya(^^2$#11(B{sXd+7;(1(H}>t(~+q
zu~V|DDKKe`;Y&5FpL*&kudovi1RxMc0va08xRDwbxYRrXnvEhL(jFaOpf%(A^zGYM
zg(JV6opUKSlFzHJzFKN3wbMiu{hMb77s&(!N4w=|!A35$c@_c?fWQHF-g)Q$ldp}v
zw@r-C>0iwZ)GjCbs4?g323Mz^dg`)e%V=aLYg>NTdIkC5gAb}!txAoB00bbA69MhV
zziRdB*e*_&GnLM1)z{FWL!Wu(8BOYG(xgdFKpi}M$|?K8g-o5y
z${h!2xdj3cfIvn9#flbv|NZwhK_J=7MEhDkG|{TlVgPOGq0ODIyz)vvx-Rzzui-##
z1+Zw*A{oHcA&13G9h)0BZoKfq3-{;AHxPgT1hOXZBamo8njJ+w^H
zpp=2c3^z%yESj;|s8OS2s$4?=0uacFz>6=v6oI}?jnAn`H*40ck3RZH`(Ek{@tmOK
z_ORB0Xdkj6Lx!j`GXiYGl16EsmZImD6+8_A2teSE1lqQ3tCiPb`87}Cm9C5*KVCCL
z{+P0G{!;T!XWhvpb!JM2JxO3Z1Ci!kcina2QCXiLTWHqNb^%b`N;`@yCl4DUty`KS2Nj
z5O7bRLx&EQE}E9usFCX75kC$gHf-4N^UpumcAcgPhVAw!L-h{Px^-)Z8NqE3fB*!t
zCQ!R}?bWMS+t@p&72n#bLx+rKo#q@KQ1ecQ-YII^1;#-8)o#`{u-cw9hy3AD2tWV=
z840MJr0z`{kJHm?bRArtF>zl0Sh{rS%PzZ2t2-=h78@vXDJE&+>wWj#=VhJ|76c#w
zf&B^e?AcR(+`4h8AT>|Bt4y6bb1XoXF$=5*C)A07&Z)=OxbUDCx5P$##
zyd$9Q&6i((SrX!_Q}aA86|H*6r%v44`jQ^C>hn6v0c@CqJ&$$Dp1D9VPE=sU|cCKmY>i2{dimR42XJ
z)xGJf=IO{i9Wl6U*)nFV_#tvBVQL=igb5SU7frr}00bc5IRPzB*Jz4eYtH=Byh@cS
zz4qE`nnvilg{OHg8lX}Lc^{THr087UpBg3AZlzaUb(QC(ngAgH0SM$v;NE-h)izod
zb#F>V>eQ)|Z+hJRP_}H@Ns}f?{#~Px;7RK4yz|ZrFTAi*r%v6wcR%;sbKiL5jfDP5
zs&zs0F;DaKVxf-Ba^=do%`u*X00bbAJ%Q6sJ57tzUAJuVTv#v{Y8+_eF)gUiKB1YO
zP*6m^>!yaPk*67(wQALJi-G4L009W(
zM&O!juCb+WQ>NyY*597ih}yVu<8p7|Nh2jWvu4f8-K4BS(72%PKjMfZx^(F>d-iO*
z8hmQx34Zb7#j{E>H$wmd5b%sZ)v8rBAJRoDC*Viatm)RRTkgr$-X97ocbQk?Sloiy
z|1-Z->v-{n3m00{+LJ=JZ{I$@2oSuX#_c<);)zsEvo+MVP
zdCW1#_?|rlMKFE(bWhr>0ldLzKB}hY)TmLTpfvG21RwwbKMC~g(c>rM&qU2#^BOd0
zphW@lyt(^#HS$)jT%{?OerAiKur1O&&Axf$kw+4JvyRfnO%Q+p1pFYNjg`%xy_uut
zX#h-9A3YH~Nv7QQ(MKQcM_yzbrY)MMw}johcXL`^w)xBb5P*R91a!2}=rLnV?jlo1
zqNYEbbIv)g6R-Z;95-7@v&7jA`1WPEmJlj8_k;Us}^xJV{sXZQslxXqK$@ch%lB
zUZtIIAOHafxF^uHYgdzJ0IHR+Z{NPN&pz8-3MH27)~)lTk*7BUEt{)cxw1P;ynRY5
z`LvIW`I@I!7Qw%bWqICaKcPVY0&M_Q)>x4Y_qDZ_RvEQ6_`ny{;Jz#tqUEJ
ztO|aM7A*>lCHaOaQ>J*B%sk*;T}aE7sFP1VIoW2eApijg7(qY_x9829XP^&tp?PX)
z$>Dn9yA_VQ5R<2$?4Mg5J9bphXJYl9>qtd@%a$!!Ekk_s%{L{Mu4DB3L7|&Aefs{m
z_yz(HfPfJMh7TVuKjpAT&To&I7rqP4(@36InkOl0*sx)S?9mYiIwD6#<`|B-
zY-!rGX@=;MfBK*Q`5yz@<#`#Wf^-?PSCTV~YY0HVC<1NUwl#1cGReseH{9SHln$)P
zQGW8gwxT}oyz}ylx?a6{TGyx|bepctL&gx*o4EBfRaY|e$O(#pug4m&JAdVCK7
z2tXiY0@?!j(@#G&YH_+;rheu-9>yms0%PIV67x#ym%i6>&^
zmA)qAO6gV0vQeW(PSUH{o?7$ZtpQ%Crrrad95g7~nMY1i%{>r+00cZEFmT{NxqGi1
zU+&>Zy;iPVdFGjCI!xg+&paa?%Dw3$p_QDZ?Pk;l)617HmonuZOIkjf^LhE@mpe?a
z;27AwDKaWW4wc(sO1TXJ5P*R91T=QFYu7HHym?v|pZQDm^=acV=jnX))mL-hP)7b*
z@!o&`{VDMtdE}87UU;Ea-TA$!Lz^-?JfF`@%~K;!n@X1{Qziu$rPm<#>
z5WR3{R8?A=XGYqKB1MX_p%U#;EX_MAqzJve-AYdYaPMtcr*g(snL(l3q
zX_&(~EbZF0lT_qB5~;2^oj)$&*9K;4@@il&V8Ki9(L9<59(W*g;n3dAYuBzdO7qkj
zo;Y!$)^cRFoL?aT0SFjGK!+0Koe7YYM0tL*bIt3~qet$IyoCLdy|H7*YGZ48@URp`
z%}uR{D6mWz)wh}G55N8P+Y2YtNoNxpx)jB6w;yNPDEdP8fNsdAXSF
z$+OmJo-#{q;+I}}$w_8$4+J0p0ka5bDC@oV-V3*_ljtk!*RP)wG#>VCdLmjm24bD&
zC1$T^%T_0u#XS)4i$KMWH~AGb;YOW6|Ni}hpP-*C73wRSHEWiWWopU_G%p!l@YmA3
zxpU{TL?P;4!T9vm8S*i*XM8RhYM_oVUA=m>CUgYvKoc>ll{t0lREKmTrg>`P>hs)l
z&t*=&IdfPv(e$R9c*D+|UHs}l0cjqx=Rf9X#-RE7Z@u+ah~1lU);y`cnz`ETEOXgf
zv}jRSGfR`-ge1=!H*Ty{sZwTZ`4s}b6G&(tvgbSKh(>VR?YHmUyEm+vEOPoEe)!?R
z2OsQ&Zp7MoQWQzPhEy}BU3|jIon+0E?&_rY%%<`y1cD%tr+LU;5V#{3hj#bZDzUJ-
zH>G)69k}CQqK6IqlP@Pd7pH_U+rJ4Zbs*%C8Uzfh=%*vV~>TRCEA2V8x}i<>ecyq9_m&uK}qu_O`4QB`Jro`Hix_C
zo_h)vDwNqyeuY3l1oG27WG|o$h(t6E8Z-!{F`*s?YRjdkF573d645&K+Qw;{LO%e~TTVLm*oM4mA(i
z%NA@5?{C_)X^{JnN%PjPU#}C}vqF9At+(#py*uEcM?X|knx_rE3ru@h<9V82qjrD8
zh7Aj}kQ*QnC;{i1hwKHKF&0Kvv0_E790;tb=dPV6t~#n)o0j>ZLji|5Y}oJui%D3T
zr`&nvl~)Qhj~gHmG=Z!%FClw{ij@r7O5(GG!1dQ(FF8@CRsi1LwdURa`@Y!080ixS
z8L#OfPR^nWMf22*-mzmxt5&TnDYlpyCj!}O9!v
zo(_l0+Qi=>TxcG$=Wmu+9{TX%!-MJGl;(Z$
z#TV{qX&*A%+j(2IY;htgp|tb#YI5nNm%7Uuo`QhC1l(yJvgdEE*c^JdZr!w0T!NI`
zCiCN34K*DO=MGir(xt7|Jk1bk+_Dun8!U32%3Ov%|rHr
z&MHgdD_5@E2OoS8z$6j**3qL!yQZRUSbO(m(bBidl`A_*yAIS{vt~_@nkPNJ>#n;F
zJn%p#=5Y@Mf+moQ<{^7Q=awb$4IDU7XSVt6XEmVZ@PzcV*p58%NKK~KjDJ7cpuUDF
zn>AW?=%I(EO;^g7LbP=0Qor*eFTINBn{U3URjXEt8T<(WQwZd!dB~nA8EKLoC!KVX
zb{6)#doxGP6EJN>W?fTHFB7u4Ns}fHGhAbS8QVz}IHV6bw
zAa~6}_JYnc8{*Ts*nUrp%w6-epzz&y-<5=fev){eW)yg#Ud=b&c%yvz^3F@aDW{wg
z%uHJ4xK1c+(V~TO+qe?~K@;#q^N_uuv(5PUbS}2spRZkNxoe&%R;^lf_Uzdfwf1Cc
zw!1c~7dL?6RjIYkLzf;`s8GT9;)#JVCg7FkA$u_{L}`+)-Yso+;;ZH<@KK{irHMPs
zuheFmH*a1rs!(7vF7pi%yubYAFPe&(RT6{IJY|Ol8ZYnDCo9{y83I8Q@KE!Ry`VGC
z?D+1u^G?5qk<`(bI5gGCO0^8MMvNGt*@nIItJ%
z`KhO#a*@mcG*9Mf$dFSMT;vXq1Wmwm%|rHr&O%e;)9FGZo_}6VF(-|f9Pd%5-@W(V
z+n_-M*A2dd4m#+qx84e72N@TMl^%ce(MRsGx<-u}e#^N8PYXXBE|Yhc3Z8<1Hw669
zJY>(Cyso;&di+VMcv}~uYQLDLbUM%M0x#57?%lh$yAUe>BM``mfZv*@
z@Q>(mFUQR0lu0%}tVtzmHmR}V!s+MoNTQ!p!(jF5)l<{W1;U068!lL|K)rSW3${`%
zA(7fiOM3R~>B91qNBq`2z0+vEQc9%!2?0Y11fY4yo}qaklv(ZCwcEONtEM^RqG9fy
zm3T<8)MBe%y}G;P9(B}F+B;PJrGY7*x(XH7%$YO0cJ1oU=H#c!lqsW;Hvh)+lla
ze|>U{TtmQU0>Nk=vS)N5@Y%S-4m(WST54XUJKf6tDa`@ViRx|Iv~j0=+OP737hcer
za8f?M2l*1SPc;q4lRP=&j59P7)xYsny;7`QyS7rLN}gCtAP@+GKwz4O>;;jXrp9sG
zZMUhXF2u1hX_XY~z4zX$R;`*lNUd76(h@)o6Dk}(5+Y%iI8B^5@$$du`z6_h8*QBz4l>7(;+)e_a}j@LzwE$RhYiE
zN+ta2vaVmhe(nYn0tA915U}PUd%e2NYQ`KOkyt2w3VA(~t78joh>
zXgX+}I&~g@{Bg;i#@;lhrF7+e@=HDVAXAIv%z(fMUYaX&^Vma{NCXI?|4x}Hwg86Aoe&0)SBsQ9v^28HQ
zl^*9u2Fq-@%7hVt5v5Z@P{9M
zcrG^NJ*&eOWRH$Smj>&TU)sI;Udh?DYnS?a^9>Tp4;rFhzDr|D1#>9;3LoCG3e-Lq#;?u+HaMMvy0B@m+KA$xHv
zb7~2F^2sNGwMCq`Z{4G=P6>wQWVUVF)~+1U9ydB&NXARKrQZ(IoBM5AP@!h4C(~{0
z0*dP78)XhfQJMO(L7|&!#p)SC@oshOnuWv
z^^&zg%}_P?)E=!_v!*#PHCII|BD4@9#8$F$O*R;X#heVn5<3W(qj|`l9VI4bD@&Iy
z{o+e61wRm#tZD^bt3f0Gt`WYIPd-`OOy`{FoC2s_ySDa!QKw=kZM$SvVlsUA@SGCE
z!w?9UfKi%1ftx0*k#HxbatCeI+w8
z*csSQ+H{T0#!@nfKP@9*n&!!#s#pG*^Y$z&Mp>Gx6Ir!t+t^XP6bX^u%$qmwrkidG
zc;ZH*Mvb($=jzp~4VmexSu-!c{IaDB8M7>*+#X#525O#6s&?aZ$X;{{-ES$=W+j>p
zVe+V6p7O~~^(#vGX3Uro*v=z$>(-UpDN-qo49GJktc%3#l~YeWHQL3J)J30wnVN^}
zMZV-6bIdUd7cLBCw!LfFP{UNy^#AggzxbLv+BtLDwCS2bqvo9nYL~#Jjk&eiSYmTE
zUz1PZ5b&3Pv6_eM`J3?ugg$7{AhjO!aV19UVyBgC2a`MLN4zn9IDf&1^j
zU%N$nLb-VH;@Zk<{P^*j&T3$Pua*P8{`%{loOa|1E&_podjf`Q9Oj`17i45w|qem`j0r}UAyYscFAX6u%=yT6jPIIk2vB8
z*JQe*H9VCs8qHu1bpH
zDs|e4?Zz8#%rB8PQk=e%Kh;iELrt5uTyez}PI&0R
z*c)!RL5(LpX!vlZChUCl(MJIW0D
z>}dgp=H_HTu1>6NK}{_~%W
zEHW4c0k;Hf)I4O*ZEhR-oL0HrdFP#mpR|xH22QRu61H~j+IH>QU2@4K`d2Fv^^KDU
z4Y@~xr(=VvRjX!bp}{N&xFcYx<{^9TvfJ3FGzPX}#fr!@^%A)7Lv6glGkeFZQi_@MxJ>U1&eYfV72BU
zdpYO7K>@UA*>cUAH4$s-S*nQYhJECbNBr1O)Sv=~De)v=yXGN#Q7X5iMvsoh1Wrq(
zE@8ITjcnPn<+$UHi&D`dEfDw*fe2_GvKN`s+_r7oO`A5^zC_(>!ISaoVIDAGK#?Lv
zB2%tN2n7C*Kr}Q@si<-17>=3!zkIdxN~3x@rU%UnQ8fi$(=|C8*3Jrq)eZ6$OS^pbj*
z!^?bPhJeomqNjPtp3gRExIX(Vsmx^?HAbB^yBOau@Jkw7Fh
z57`T`M0pZ*uU@@as8^s$d3%F2Z``6Ns?pA$#@~Obsbrd+oKXPtTxI-u@)b8$W)$y#)r#
zA>foiv^5Xeb6O6}yRUZb+KU!1{_%$&ymjt`mX?4{yUl#mt$76miy+{NK;$(K*>hDc
zjCkmlTW--zLW>TZODiYlf2ls?kt0XyL?#NOnLCZ2Q`6{re2CrdHAOWO@|I0
zTC7;HY_f%WAz&?m_-G!oXJa8%Q*X(VB|rZ7qlaFepz;Xl2!(IH`R0^UPO-6UU^4{V
z5QvrLA$x9$NZ8K|8Z=0=3Tf))36+NzzyA8`GtWFzym;}jGnx1y5EOy9X&$l{RLSxz
z9v$Sia^*_a>Uk)2iJ;VJtc_QiHEZTsRuK>c%qI{-%|rIgFP1454jw$%bB|Ae8p%^5
zkLj8zg^fQWP9UC|hwRx^tgBbAzIpRzx;H)4X*Ke`{PN2)&N#!al7O`k$dy2BH4oX#
zwde%t5HOVhnwKC@8kr=Nbx4xBDDPtfMhn^*aWBMi@C%!7cB1X4Uce~JLI=VO8MBIt)6
zdWao3v(-GcjI~6eSFc`P0O
z$TPWQV_N_O(t3Qp6ai!}fa2#miU%He;Jfd>V^q(f>}i3=u3fvjckk{RI1fS~A_S1P
z3Pwd{{lynw(9|nX`80K9{P^)^
zW+GNWAZP*wdVFpW0c0=ea_KC--o1OX6sBVVQSj6sWYP71%ap&K++r%kn9~?
zwsO>qgJtO}Q>M)O@4wGly$rIacY={4M_QJzmnc@P3oBY>D?MF80|tpXf&*kP}{@(LSIr0tlFE0?3|Gb!fzh5n71GZe)@@^=j_ky}Mnzc1C3o#z4Rb
z0#tn7QnHuV)`{(f8hN8ejVf8Pq&c~RB@i%$0MeBs0c6jV
zqI|#s0|yRNJ1^!$78WY3TSt&OLbFJB&K%~RU7
z@yc`0J!eRcUBoN~(Ab?aiaL|t2Jju|uNkV6g$9XE0c0=SLVwUf2R-=EL)v^R
zCOw>*tGRC7I;~I)7dx>-zybnDlurbZy>Lsv<}S{iJ6ElrIIK+n<(FUXyz|aNg$jks
zoY)~?0Rf*pKEaAWtq0!VnAs5Pz@Pr~r#*Z2s0|ct+573IpFaHX!*leGiJ<)_Nd;!{`t>uzx%Ft@7^JDCTa-SM8H>%Pv9be?1fk#I&|o;
zef#zZH}%xW8$W)$)ICJVL=6F(2p~%V5J2`qtPi
zq7K_dixyR?R4FW~#0vo{2?XZxiAV&Hy-+I2VTTnFB87m(
z1dyVD2_So+)RyMWo9pQC$V_I@2&Kj;S&$b>St3%q2?Xr%iB1HNz2NIiv0}vr4H~2a
z#3RtdslBAPZrxg^PMzQ}5+ekxCx8frLIBwdzUF9auEmQNYjVHsvZqekTW`IUMqcnm
zh!~?lAQX>Jq#}Uq1z&$|yzxezFlTiSr?we;@4fe!x)OZhA;yRhKz>3bfb0cai^`NK
z^UkD6_RitdFy59eTh2T0ynwk89R#97AViN()FOcF1zeX}opa8nO`B{yNL>r^q}ES7
z@dUk^0hbw~ixdHbCv*bHUcfc#nP;A{aY#=ikvb`&cJ0~$<03i;M2bM@9v=fl0ND$&
zQXPKy;Y*e*v8$;kYj^C}!CulqmJ{NNCIKYJ5CX_vkQJL_0)L3l~~3hf^J!*IaW=5KxE%0+A)$zw49}Ey;fk5;L80hga
zNd%C+VCtSWBwM&}p_vAAAcOW?CsmPuUofnekYdjArMmn$caS+kUhU^=~-u;
zwSD{c5M}RQ-+%w?bIn=4keN7sHLm=)1kPu4=AbY-6U7cgFYuB#8R_v)+
zIez^3a^=eTnkWK?00a)O)Z=5e2q1g@Rp3H}3XK>sBA9gxYEbIj_7hJ$(ZA#nG6W#-
z9|DMn)dVDarRtqYY4(GFHoE-iqmTS<>M48;<8|-e-H#*?CIldmO2BH5kMSaa?4_1z
z@BVJkpuzg}>;02G4dZEo=%7J^yfc}QAOL}k1dxpg5J2`amhqf^y!6sbwe_V>OBd9u
zIeGHrk|jWFikk00PblM8M;d1Q9^?oR|5m?$o)mPdxF2#L4qh7qmEG`SRr_oNz)`
z_HZ)qS>K78BDMcfIxZzNJYd5AbaTxdd}Z=>C$EM
z=FPd!XOZBk$*6@8rAn2`*(d^l00gom5HXKWazp^x%d!$Uz5VvvZ{M?LPmTk7T7Eud
z%9QHWt2>RA`yc=T4+$U=ktBfZd00pCg3=j#!-fsZ<$76cH6$P+;JvZxP^Z|&5nllIAUIE_<+H*(adLk~UFaq!#-0SNd*Ao3oc
z1d0H%=SS^HoHtb9lomAyr%ju7(V*gyaAkNpqZli)2~
zy0lrdW}e4OfDnLyO9F^NtOy``F6&ADj}<9WWboj@KmPPnisor4-s;t>TeohV|D*f@
z0SE+6AXXlq#EJm2=XoX5qP=0mhbQmGsfOQ{En6eYzX@4gwGel|T$VK1mh+Q*)zZ9Vj%<|;D!J$9s+3zAbW1A6VE^Z0)`Vv>+kpy0wRFy8D4oY
z4+0R#ngGrn0vQM(ds){g9)JJ@OeT=Q-|-U!L;%?{x&C4s1R#(d0o*zS3M7E+Wml=V
z8v+n8mOz32jvF8#0?3}RRT#q{0D&wB;Lsu9kN~omMZMxy2tdG00uKEhw?RMzkUcYN
zF;+nU0!|6w${~;y0c6i<)#5$~K)^r(S@}C|hJXkldj?iyOo9Le91*~YL%;D6uiy&C9T0$kQ3PE3J06692q1e#
zRcDNW00askfa8Whjs%ds0_qq)LjVHi5XjNr@i+uT0NFFAMq>#CAdrawE*k=#5J2`a
zRWp8r00ayn;EBH@5D16>vS&!8#taBRAYTGFYY2Eq0NKm8rtt#=AYcLk5B(iMK|lnM
zJrn9RHb4LZ=?UPbA>cUy$=;!j&re_A_!a^XfIx@@Jok452muj5_Cl=GL=6E5;+lJi3_$g+Pb|kUcyR1RxM7fe`&2QA0okki9@FJdr^l<^*uN5D1+BvKMnNMDigJ9D&gN
z9R@%kL4adsgRAz$1cBHRz~Mr`90JH*Y#k8^hd@9C%<*?v0s#>~_5!N;L<50%62Q+w
zz%&BLUOasfsfIuh1WfaH*aHC(K=y*D{KNr)7!tt4LcmM{$X*QH5lM!C?*z>BcUT1h
z5kU5Qum3~V0?1z6JQC@JfWHJx_IKC@0TDp<{B;3@4uMz^z^g*Qd;-W`
zteg^wg@BI)%=dR#2muj5_I&gL1Py`s5Wt^8z!n0?UVQu#DTRPv1Z?ql*a-m