From 90aa4768c44c6da9899673b8dbe4f81e13967294 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 14 Sep 2017 10:46:41 +1000 Subject: [PATCH 01/31] Add function to retrieve paths for settings files. --- src/utils.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/utils.py b/src/utils.py index e1b10320f..fbfc2d330 100644 --- a/src/utils.py +++ b/src/utils.py @@ -6,6 +6,7 @@ import stat import shutil import sys +from pathlib import Path from enum import Enum from typing import ( @@ -63,6 +64,40 @@ # 211480: 'In Motion' } +# Appropriate locations to store config options for each OS. +if WIN: + _SETTINGS_ROOT = Path(os.environ['APPDATA']) +elif MAC: + _SETTINGS_ROOT = Path('~/Library/Preferences/').expanduser() +elif LINUX: + _SETTINGS_ROOT = Path('~/.config').expanduser() +else: + # Defer the error until used, so it goes in logs and whatnot. + # Utils is early, so it'll get lost in stderr. + _SETTINGS_ROOT = None + +# We always go in a BEE2 subfolder +if _SETTINGS_ROOT: + _SETTINGS_ROOT /= 'BEEMOD2' + +def conf_location(path: str) -> Path: + """Return the full path to save settings to. + + The passed-in path is relative to the settings folder. + Any additional subfolders will be created if necessary. + """ + if _SETTINGS_ROOT is None: + raise FileNotFoundError("Don't know a good config directory!") + + loc = _SETTINGS_ROOT / path + if loc.is_dir(): + folder = loc + else: + folder = loc.parent + # Create folders if needed. + folder.mkdir(parents=True, exist_ok=True) + return loc + def fix_cur_directory() -> None: """Change directory to the location of the executable. From 8b03c4e182bef769de2e03e0ea77dce63154dc0f Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 12:20:14 +1000 Subject: [PATCH 02/31] Move configs to appdata folder --- src/BEE2_config.py | 17 +++++++++++++---- src/sound.py | 8 +++----- src/utils.py | 5 ++++- src/voiceEditor.py | 1 - 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/BEE2_config.py b/src/BEE2_config.py index e84afb9dc..ad1d769a4 100644 --- a/src/BEE2_config.py +++ b/src/BEE2_config.py @@ -6,6 +6,7 @@ from configparser import ConfigParser, NoOptionError import os +import utils import srctools.logger @@ -20,20 +21,28 @@ class ConfigFile(ConfigParser): get_val, get_bool, and get_int are modified to return defaults instead of erroring. """ - def __init__(self, filename, root='../config', auto_load=True): + def __init__(self, filename, root=None, auto_load=True): """Initialise the config file. - filename is the name of the config file, in the 'root' directory. - If auto_load is true, this file will immediately be read and parsed. + `filename` is the name of the config file, in the `root` directory. + If `auto_load` is true, this file will immediately be read and parsed. + If `root` is not set, it will be set to the 'config/' folder in the BEE2 + folder. """ super().__init__() - self.filename = os.path.join(root, filename) + + if root is None: + self.filename = utils.conf_location(os.path.join('config/', filename)) + else: + self.filename = os.path.join(root, filename) + self.writer = srctools.AtomicWriter(self.filename) self.has_changed = False if auto_load: self.load() + def load(self): if self.filename is None: return diff --git a/src/sound.py b/src/sound.py index c09917f01..5d82b248b 100644 --- a/src/sound.py +++ b/src/sound.py @@ -6,6 +6,7 @@ """ import shutil import os +import utils from tk_tools import TK_ROOT from srctools.filesys import RawFileSystem, FileSystemChain @@ -23,7 +24,7 @@ play_sound = True -SAMPLE_WRITE_PATH = '../config/music_sample_temp' +SAMPLE_WRITE_PATH = utils.conf_location('config/music_sample_temp') # This starts holding the filenames, but then caches the actual sound object. SOUNDS = { @@ -178,10 +179,7 @@ def play_sample(self, e=None): else: # In a filesystem, we need to extract it. # SAMPLE_WRITE_PATH + the appropriate extension. - disk_filename = ( - SAMPLE_WRITE_PATH + - os.path.splitext(self.cur_file)[1] - ) + disk_filename = SAMPLE_WRITE_PATH.with_suffix(os.path.splitext(self.cur_file)[1]) with self.system.get_system(file), file.open_bin() as fsrc: with open(disk_filename, 'wb') as fdest: shutil.copyfileobj(fsrc, fdest) diff --git a/src/utils.py b/src/utils.py index fbfc2d330..07bacb8db 100644 --- a/src/utils.py +++ b/src/utils.py @@ -80,17 +80,20 @@ if _SETTINGS_ROOT: _SETTINGS_ROOT /= 'BEEMOD2' + def conf_location(path: str) -> Path: """Return the full path to save settings to. The passed-in path is relative to the settings folder. Any additional subfolders will be created if necessary. + If it ends with a '/' or '\', it is treated as a folder. """ if _SETTINGS_ROOT is None: raise FileNotFoundError("Don't know a good config directory!") loc = _SETTINGS_ROOT / path - if loc.is_dir(): + + if path.endswith(('\\', '/')) and not loc.suffix: folder = loc else: folder = loc.parent diff --git a/src/voiceEditor.py b/src/voiceEditor.py index ca268f01b..ce3031a27 100644 --- a/src/voiceEditor.py +++ b/src/voiceEditor.py @@ -241,7 +241,6 @@ def show(quote_pack): quote_data = quote_pack.config - os.makedirs('config/voice', exist_ok=True) config = ConfigFile('voice/' + quote_pack.id + '.cfg') config_mid = ConfigFile('voice/MID_' + quote_pack.id + '.cfg') config_resp = ConfigFile('voice/RESP_' + quote_pack.id + '.cfg') From 94e7db6092282a8146db6f2e0b184d51c4dd184b Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 12:20:53 +1000 Subject: [PATCH 03/31] Move user-made palettes to AppData --- src/BEE2.py | 3 +-- src/paletteLoader.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/BEE2.py b/src/BEE2.py index 01c45cb16..2255019e6 100644 --- a/src/BEE2.py +++ b/src/BEE2.py @@ -28,7 +28,6 @@ DEFAULT_SETTINGS = { 'Directories': { - 'palette': 'palettes/', 'package': 'packages/', }, 'General': { @@ -108,7 +107,7 @@ LOGGER.info('Done!') LOGGER.info('Loading Palettes...') -paletteLoader.load_palettes(GEN_OPTS['Directories']['palette']) +paletteLoader.load_palettes() LOGGER.info('Done!') # Check games for Portal 2's basemodui.txt file, so we can translate items. diff --git a/src/paletteLoader.py b/src/paletteLoader.py index 70f2659d3..aba08117b 100644 --- a/src/paletteLoader.py +++ b/src/paletteLoader.py @@ -11,7 +11,7 @@ LOGGER = srctools.logger.get_logger(__name__) -PAL_DIR = "palettes\\" +PAL_DIR = utils.conf_location('palettes/') PAL_EXT = '.bee2_palette' @@ -35,6 +35,7 @@ 'APTAG': _('Aperture Tag'), } + class Palette: """A palette, saving an arrangement of items for editoritems.txt""" def __init__( @@ -141,15 +142,17 @@ def delete_from_disk(self): os.remove(os.path.join(PAL_DIR, self.filename)) -def load_palettes(pal_dir): +def load_palettes(): """Scan and read in all palettes in the specified directory.""" - global PAL_DIR - PAL_DIR = os.path.abspath(os.path.join('..', pal_dir)) - full_dir = os.path.join(os.getcwd(), PAL_DIR) - for name in os.listdir(full_dir): # this is both files and dirs + # Load our builtin palettes: + for name in os.listdir('../palettes/'): + LOGGER.info('Loading builtin "{}"', name) + pal_list.append(Palette.parse(os.path.join('../palettes/', name))) + + for name in os.listdir(PAL_DIR): # this is both files and dirs LOGGER.info('Loading "{}"', name) - path = os.path.join(full_dir, name) + path = os.path.join(PAL_DIR, name) pos_file, prop_file = None, None try: if name.endswith(PAL_EXT): From 7d0fbb9ffa3baef05ff0e03a30b8c9c18118533f Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 12:30:13 +1000 Subject: [PATCH 04/31] Redo config file parameter options --- src/BEE2_config.py | 27 ++++++++++++++++----------- src/vbsp.py | 10 ++-------- src/vbsp_options.py | 2 +- src/voiceLine.py | 6 +++--- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/BEE2_config.py b/src/BEE2_config.py index ad1d769a4..70cca2a93 100644 --- a/src/BEE2_config.py +++ b/src/BEE2_config.py @@ -21,27 +21,31 @@ class ConfigFile(ConfigParser): get_val, get_bool, and get_int are modified to return defaults instead of erroring. """ - def __init__(self, filename, root=None, auto_load=True): + def __init__(self, filename, *, in_conf_folder=True, auto_load=True): """Initialise the config file. `filename` is the name of the config file, in the `root` directory. If `auto_load` is true, this file will immediately be read and parsed. - If `root` is not set, it will be set to the 'config/' folder in the BEE2 - folder. + If in_conf_folder is set, The folder is relative to the 'config/' + folder in the BEE2 folder. """ super().__init__() - if root is None: - self.filename = utils.conf_location(os.path.join('config/', filename)) - else: - self.filename = os.path.join(root, filename) - - self.writer = srctools.AtomicWriter(self.filename) self.has_changed = False - if auto_load: - self.load() + if filename is not None: + if in_conf_folder: + self.filename = utils.conf_location('config/' + filename) + else: + self.filename = filename + self.writer = srctools.AtomicWriter(self.filename) + self.has_changed = False + + if auto_load: + self.load() + else: + self.filename = self.writer = None def load(self): if self.filename is None: @@ -158,6 +162,7 @@ def set(self, section, option, value=None): remove_section.__doc__ = ConfigParser.remove_section.__doc__ set.__doc__ = ConfigParser.set.__doc__ + # Define this here so app modules can easily access the config # Don't load it though, since this is imported by VBSP too. GEN_OPTS = ConfigFile('config.cfg', auto_load=False) diff --git a/src/vbsp.py b/src/vbsp.py index be32d7933..29f26f358 100644 --- a/src/vbsp.py +++ b/src/vbsp.py @@ -398,14 +398,8 @@ def load_settings(): # set in the 'Compiler Pane'. bee2_loc = vbsp_options.get(str, 'BEE2_loc') if bee2_loc: - BEE2_config = ConfigFile( - 'config/compile.cfg', - root=bee2_loc, - ) - vbsp_options.ITEM_CONFIG = ConfigFile( - 'config/item_cust_configs.cfg', - root=bee2_loc, - ) + BEE2_config = ConfigFile('compile.cfg') + vbsp_options.ITEM_CONFIG = ConfigFile('item_cust_configs.cfg') else: BEE2_config = ConfigFile(None) diff --git a/src/vbsp_options.py b/src/vbsp_options.py index c93d7fdad..f64a64abe 100644 --- a/src/vbsp_options.py +++ b/src/vbsp_options.py @@ -15,7 +15,7 @@ SETTINGS = {} # Overwritten by VBSP to get the actual values. -ITEM_CONFIG = ConfigFile('', root='', auto_load=False) +ITEM_CONFIG = ConfigFile(None) class TYPE(Enum): diff --git a/src/voiceLine.py b/src/voiceLine.py index 5b2c35fa5..3e8481354 100644 --- a/src/voiceLine.py +++ b/src/voiceLine.py @@ -67,7 +67,7 @@ def generate_resp_script(file, allow_dings): """Write the responses section into a file.""" use_dings = allow_dings - config = ConfigFile('resp_voice.cfg', root='bee2') + config = ConfigFile('bee2/resp_voice.cfg', in_conf_folder=False) file.write("BEE2_RESPONSES <- {\n") for section in QUOTE_DATA.find_key('CoopResponses', []): if not section.has_children() and section.name == 'use_dings': @@ -436,8 +436,8 @@ def add_voice( map_attr = has_items style_vars = style_vars_ - norm_config = ConfigFile('voice.cfg', root='bee2') - mid_config = ConfigFile('mid_voice.cfg', root='bee2') + norm_config = ConfigFile('bee2/voice.cfg', in_conf_folder=False) + mid_config = ConfigFile('bee2/mid_voice.cfg', in_conf_folder=False) quote_base = QUOTE_DATA['base', False] quote_loc = Vec.from_str(QUOTE_DATA['quote_loc', '-10000 0 0'], x=-10000) From 7dc3c2294c253eb5ad1842e8b1d1cebbce1d9ad2 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 12:43:32 +1000 Subject: [PATCH 05/31] Clean up audio samples when exiting --- src/UI.py | 4 ++++ src/sound.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/UI.py b/src/UI.py index 1e59016d1..5e71caf85 100644 --- a/src/UI.py +++ b/src/UI.py @@ -431,10 +431,14 @@ def quit_application() -> None: GEN_OPTS['win_state']['main_window_x'] = str(TK_ROOT.winfo_rootx()) GEN_OPTS['win_state']['main_window_y'] = str(TK_ROOT.winfo_rooty()) + # Clean up sounds. + snd.clean_folder() + GEN_OPTS.save_check() item_opts.save_check() CompilerPane.COMPILE_CFG.save_check() gameMan.save() + # Destroy the TK windows TK_ROOT.quit() sys.exit(0) diff --git a/src/sound.py b/src/sound.py index 5d82b248b..f1a93bb72 100644 --- a/src/sound.py +++ b/src/sound.py @@ -24,7 +24,7 @@ play_sound = True -SAMPLE_WRITE_PATH = utils.conf_location('config/music_sample_temp') +SAMPLE_WRITE_PATH = utils.conf_location('config/music_sample/temp') # This starts holding the filenames, but then caches the actual sound object. SOUNDS = { @@ -78,6 +78,9 @@ def fx_blockable(sound): def block_fx(): """Block fx_blockable() for a short time.""" + def clean_folder(): + pass + initiallised = False pyglet = avbin = None # type: ignore SamplePlayer = None # type: ignore @@ -129,6 +132,15 @@ def block_fx(): _play_repeat_sfx = False TK_ROOT.after(50, _reset_fx_blockable) + def clean_folder(): + """Delete files used by the sample player.""" + for file in SAMPLE_WRITE_PATH.parent.iterdir(): + LOGGER.info('Cleaning up "{}"...', file) + try: + file.unlink() + except (PermissionError, FileNotFoundError): + pass + class SamplePlayer: """Handles playing a single audio file, and allows toggling it on/off.""" def __init__(self, start_callback, stop_callback, system: FileSystemChain): From 0aab1dcdd9f9e06e922be304423b719ea5f2f247 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 13:42:57 +1000 Subject: [PATCH 06/31] Add gear overlays for palettes which have settings --- src/UI.py | 69 ++++++++++++++++++++++++++++++++++++++++---- src/paletteLoader.py | 15 +++++++++- src/tk_tools.py | 9 ++++++ 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/src/UI.py b/src/UI.py index 5e71caf85..a30132f88 100644 --- a/src/UI.py +++ b/src/UI.py @@ -14,6 +14,7 @@ from selectorWin import selWin, Item as selWinItem, AttrDef as SelAttr from loadScreen import main_loader as loader import srctools.logger +from srctools.filesys import FileSystem, FileSystemChain import sound as snd import paletteLoader import packageLoader @@ -34,7 +35,8 @@ import backup as backup_win import tooltip -from typing import List, Dict +from typing import Iterable, List, Dict + LOGGER = srctools.logger.get_logger(__name__) @@ -730,12 +732,58 @@ def refresh_pal_ui(): listbox = UI['palette'] # type: Listbox listbox.delete(0, END) + + gear_icons = listbox.gear_icons # type: Dict[int, Label] + + for gear in gear_icons.values(): + gear.destroy() + + gear_icons.clear() + for i, pal in enumerate(paletteLoader.pal_list): - listbox.insert(i, pal.name) + if pal.settings is not None: + listbox.insert(i, ' ' + pal.name) + else: + listbox.insert(i, pal.name) + if pal.prevent_overwrite: - listbox.itemconfig(i, foreground='grey', background='white') + listbox.itemconfig( + i, + foreground='grey', + background=tk_tools.LISTBOX_BG_COLOR, + selectbackground=tk_tools.LISTBOX_BG_SEL_COLOR, + ) else: - listbox.itemconfig(i, foreground='black', background='white') + listbox.itemconfig( + i, + foreground='black', + background=tk_tools.LISTBOX_BG_COLOR, + selectbackground=tk_tools.LISTBOX_BG_SEL_COLOR, + ) + + listbox.update() + + for i, pal in enumerate(paletteLoader.pal_list): + if pal.settings is not None: + gear = Label( + listbox, + bg='white', + image=img.png('icons/gear'), + width=10, + height=10, + borderwidth=0, + padx=0, + pady=0, + ) + x, y, width, height = listbox.bbox(i) + gear_icons[i] = gear + gear.place( + x=x, + y=y + (height-10)//2, + width=10, + height=10, + anchor='nw', + ) for ind in range(menus['pal'].index(END), 0, -1): # Delete all the old radiobuttons @@ -1062,6 +1110,14 @@ def set_palette(e=None): LOGGER.warning('Invalid palette index!') selectedPalette = 0 + # Update gear icons (if any) to match the background of their label. + # Do this before palettes update, so you can't see the change. + for ind, gear in UI['palette'].gear_icons.items(): + if ind == selectedPalette: + gear['bg'] = tk_tools.LISTBOX_BG_SEL_COLOR + else: + gear['bg'] = tk_tools.LISTBOX_BG_COLOR + GEN_OPTS['Last_Selected']['palette'] = str(selectedPalette) pal_clear() menus['pal'].entryconfigure( @@ -1211,10 +1267,13 @@ def init_palette(f): UI['palette'] = Listbox(f, width=10) UI['palette'].grid(row=1, sticky="NSEW") + # Dict to store gear overlays for palettes with settings. + UI['palette'].gear_icons = {} + def set_pal_listbox(e=None): global selectedPalette cur_selection = UI['palette'].curselection() - if cur_selection: # Might be blank if none selected + if cur_selection: # Might be blank if none selected selectedPalette = int(cur_selection[0]) selectedPalette_radio.set(selectedPalette) set_palette() diff --git a/src/paletteLoader.py b/src/paletteLoader.py index aba08117b..97e43ead1 100644 --- a/src/paletteLoader.py +++ b/src/paletteLoader.py @@ -2,11 +2,12 @@ import shutil import zipfile import random +import utils import srctools.logger from srctools import Property -from typing import List, Tuple +from typing import List, Tuple, Optional, Dict LOGGER = srctools.logger.get_logger(__name__) @@ -45,6 +46,7 @@ def __init__( trans_name='', prevent_overwrite=False, filename: str=None, + settings: Optional[Dict[str, Property]]=None, ): # Name of the palette self.name = name @@ -64,6 +66,9 @@ def __init__( # (premade palettes or ) self.prevent_overwrite = prevent_overwrite + # If not None, settings associated with the palette. + self.settings = settings + def __str__(self): return self.name @@ -79,12 +84,20 @@ def parse(cls, path: str): trans_name = props['TransName', ''] + settings = { + prop.name: prop + for prop in props.find_children('settings') + } + if not settings: + settings = None + return Palette( name, items, trans_name=trans_name, prevent_overwrite=props.bool('readonly'), filename=os.path.basename(path), + settings=settings, ) def save(self, ignore_readonly=False): diff --git a/src/tk_tools.py b/src/tk_tools.py index 1d5cdebb9..13f36d8ff 100644 --- a/src/tk_tools.py +++ b/src/tk_tools.py @@ -46,6 +46,9 @@ def set_window_icon(window: tk.Toplevel): ) except (AttributeError, WindowsError, ValueError): pass # It's not too bad if it fails. + + LISTBOX_BG_SEL_COLOR = '#0078D7' + LISTBOX_BG_COLOR = 'white' elif utils.MAC: def set_window_icon(window: Union[tk.Toplevel, tk.Tk]): """ Call OS-X's specific api for setting the window icon.""" @@ -59,6 +62,9 @@ def set_window_icon(window: Union[tk.Toplevel, tk.Tk]): ) set_window_icon(TK_ROOT) + + LISTBOX_BG_SEL_COLOR = '#C2DDFF' + LISTBOX_BG_COLOR = 'white' else: # Linux # Get the tk image object. import img @@ -69,6 +75,9 @@ def set_window_icon(window: tk.Toplevel): # Weird argument order for default=True... window.wm_iconphoto(True, app_icon) + LISTBOX_BG_SEL_COLOR = 'blue' + LISTBOX_BG_COLOR = 'white' + TK_ROOT.withdraw() # Hide the window until everything is loaded. From 3dc47e9f786d8e1b99642c9d9953eb9b7448281a Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 13:53:34 +1000 Subject: [PATCH 07/31] Add checkbox for saving settings in palettes --- src/UI.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/UI.py b/src/UI.py index a30132f88..6d8790922 100644 --- a/src/UI.py +++ b/src/UI.py @@ -1315,20 +1315,26 @@ def init_option(pane: SubPane): ttk.Button( frame, - text=_("Save Palette..."), - command=pal_save, + textvariable=EXPORT_CMD_VAR, + command=export_editoritems, ).grid(row=0, sticky="EW", padx=5) + ttk.Button( frame, - text=_("Save Palette As..."), - command=pal_save_as, + text=_("Save Palette..."), + command=pal_save, ).grid(row=1, sticky="EW", padx=5) ttk.Button( frame, - textvariable=EXPORT_CMD_VAR, - command=export_editoritems, + text=_("Save Palette As..."), + command=pal_save_as, ).grid(row=2, sticky="EW", padx=5) + ttk.Checkbutton( + frame, + text=_('Save Settings in Palettes'), + ).grid(row=3, sticky="EW", padx=5) + props = ttk.LabelFrame(frame, text=_("Properties"), width="50") props.columnconfigure(1, weight=1) props.grid(row=4, sticky="EW") From af75d587b5d1226e37ca37c00b61cfff513b440a Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 13:53:51 +1000 Subject: [PATCH 08/31] Centralized functions for handling saving/loading options. --- src/BEE2_config.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/BEE2_config.py b/src/BEE2_config.py index 70cca2a93..c42a40238 100644 --- a/src/BEE2_config.py +++ b/src/BEE2_config.py @@ -4,6 +4,7 @@ Most functions are also altered to allow defaults instead of erroring. """ from configparser import ConfigParser, NoOptionError +from srctools import AtomicWriter, Property import os import utils @@ -12,6 +13,34 @@ LOGGER = srctools.logger.get_logger(__name__) +# Functions for saving or loading application settings. +# Call with a block to load, or with no args to return the current +# values. +option_handler = utils.FuncLookup('OptionHandlers') + + +def get_curr_settings() -> Property: + """Return a property tree defining the current options.""" + props = Property('', []) + + for opt_id, opt_func in option_handler.items(): + opt_prop = opt_func() + opt_prop.name = opt_id.title() + props.append(opt_prop) + + return props + + +def apply_settings(props: Property): + """Given a property tree, apply it to the widgets.""" + for opt_prop in props: + try: + func = option_handler[opt_prop.name] + except KeyError: + LOGGER.warning('No handler for option type "{}"!', opt_prop.real_name) + else: + func(opt_prop) + class ConfigFile(ConfigParser): """A version of ConfigParser which can easily save itself. @@ -39,7 +68,7 @@ def __init__(self, filename, *, in_conf_folder=True, auto_load=True): else: self.filename = filename - self.writer = srctools.AtomicWriter(self.filename) + self.writer = AtomicWriter(self.filename) self.has_changed = False if auto_load: From ce2846a066599c3278293144bcf1d06e10a6e953 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 14:22:33 +1000 Subject: [PATCH 09/31] Implement logic for saving and loading palettes. --- src/UI.py | 18 ++++++++++++++++-- src/paletteLoader.py | 27 ++++++++++++++++++--------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/UI.py b/src/UI.py index 6d8790922..98cfd5f8d 100644 --- a/src/UI.py +++ b/src/UI.py @@ -65,6 +65,8 @@ selectedPalette_radio = IntVar(value=0) # Variable used for export button (changes to include game name) EXPORT_CMD_VAR = StringVar(value=_('Export...')) +# If set, save settings into the palette in addition to items. +var_pal_save_settings = BooleanVar(value=True) # Maps item IDs to our wrapper for the object. item_list = {} # type: Dict[str, Item] @@ -1214,6 +1216,7 @@ def pal_save_as(e: Event=None): paletteLoader.save_pal( [(it.id, it.subKey) for it in pal_picked], name, + var_pal_save_settings.get(), ) refresh_pal_ui() @@ -1223,6 +1226,7 @@ def pal_save(e=None): paletteLoader.save_pal( [(it.id, it.subKey) for it in pal_picked], pal.name, + var_pal_save_settings.get(), ) refresh_pal_ui() @@ -1333,6 +1337,7 @@ def init_option(pane: SubPane): ttk.Checkbutton( frame, text=_('Save Settings in Palettes'), + variable=var_pal_save_settings, ).grid(row=3, sticky="EW", padx=5) props = ttk.LabelFrame(frame, text=_("Properties"), width="50") @@ -1706,8 +1711,7 @@ def init_menu_bar(win): gameMan.add_menu_opts(menus['file'], callback=set_game) gameMan.game_menu = menus['file'] - menus['pal'] = Menu(bar) - pal_menu = menus['pal'] + pal_menu = menus['pal'] = Menu(bar) # Menu name bar.add_cascade(menu=pal_menu, label=_('Palette')) pal_menu.add_command( @@ -1723,6 +1727,16 @@ def init_menu_bar(win): label=_('Fill Palette'), command=pal_shuffle, ) + + pal_menu.add_separator() + + pal_menu.add_checkbutton( + label=_('Save Settings in Palettes'), + variable=var_pal_save_settings, + ) + + pal_menu.add_separator() + pal_menu.add_command( label=_('Save Palette'), command=pal_save, diff --git a/src/paletteLoader.py b/src/paletteLoader.py index 97e43ead1..b4c476bf9 100644 --- a/src/paletteLoader.py +++ b/src/paletteLoader.py @@ -6,8 +6,10 @@ import srctools.logger from srctools import Property +import BEE2_config +from srctools import Property, NoKeyError -from typing import List, Tuple, Optional, Dict +from typing import List, Tuple, Optional LOGGER = srctools.logger.get_logger(__name__) @@ -46,7 +48,7 @@ def __init__( trans_name='', prevent_overwrite=False, filename: str=None, - settings: Optional[Dict[str, Property]]=None, + settings: Optional[Property]=None, ): # Name of the palette self.name = name @@ -84,11 +86,9 @@ def parse(cls, path: str): trans_name = props['TransName', ''] - settings = { - prop.name: prop - for prop in props.find_children('settings') - } - if not settings: + try: + settings = props.find_key('Settings') + except NoKeyError: settings = None return Palette( @@ -128,6 +128,10 @@ def save(self, ignore_readonly=False): if not self.prevent_overwrite: del props['ReadOnly'] + if self.settings is not None: + self.settings.name = 'Settings' + props.append(self.settings.copy()) + # We need to write a new file, determine a valid path. # Use a hash to ensure it's a valid path (without '-' if negative) # If a conflict occurs, add ' ' and hash again to get a different @@ -238,7 +242,7 @@ def parse_legacy(posfile, propfile, path): return Palette(name, pos) -def save_pal(items, name): +def save_pal(items, name: str, include_settings: bool): """Save a palette under the specified name.""" for pal in pal_list: if pal.name == name and not pal.prevent_overwrite: @@ -248,6 +252,11 @@ def save_pal(items, name): pal = Palette(name, list(items)) pal_list.append(pal) + if include_settings: + pal.settings = BEE2_config.get_curr_settings() + else: + pal.settings = None + pal.save() return pal @@ -261,6 +270,6 @@ def check_exists(name): if __name__ == '__main__': - results = load_palettes('palettes\\') + results = load_palettes() for palette in results: print(palette) From 9520d5702fed77e1a420430efd4d51de40ec2ecd Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 14:57:29 +1000 Subject: [PATCH 10/31] These need to be defined ourselves Redefining iter makes the mixins invalid. --- src/utils.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/utils.py b/src/utils.py index 07bacb8db..4d2f8df41 100644 --- a/src/utils.py +++ b/src/utils.py @@ -443,9 +443,22 @@ def __eq__(self, other: Any) -> bool: return self._registry == dict(other.items()) def __iter__(self) -> Iterator[Callable[..., Any]]: - yield from self.values() + """Yield all the functions.""" + return iter(self.values()) - def __len__(self) -> int: + def keys(self): + """Yield all the valid IDs.""" + return self._registry.keys() + + def values(self): + """Yield all the functions.""" + return self._registry.values() + + def items(self): + """Return pairs of (ID, func).""" + return self._registry.items() + + def __len__(self): return len(set(self._registry.values())) def __getitem__(self, names: Union[str, Tuple[str]]) -> Callable[..., Any]: From 295cfe4f592f0affca3ad0a928fa331e3c5ef192 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 14:58:02 +1000 Subject: [PATCH 11/31] Apply settings when palette is selected. --- src/UI.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/UI.py b/src/UI.py index 98cfd5f8d..64e1c3df8 100644 --- a/src/UI.py +++ b/src/UI.py @@ -16,6 +16,7 @@ import srctools.logger from srctools.filesys import FileSystem, FileSystemChain import sound as snd +import BEE2_config import paletteLoader import packageLoader import img @@ -1120,13 +1121,15 @@ def set_palette(e=None): else: gear['bg'] = tk_tools.LISTBOX_BG_COLOR + chosen_pal = paletteLoader.pal_list[selectedPalette] + GEN_OPTS['Last_Selected']['palette'] = str(selectedPalette) pal_clear() menus['pal'].entryconfigure( 1, - label=_('Delete Palette "{}"').format(paletteLoader.pal_list[selectedPalette].name), + label=_('Delete Palette "{}"').format(chosen_pal.name), ) - for item, sub in paletteLoader.pal_list[selectedPalette].pos: + for item, sub in chosen_pal.pos: try: item_group = item_list[item] except KeyError: @@ -1147,6 +1150,9 @@ def set_palette(e=None): is_pre=True, )) + if chosen_pal.settings is not None: + BEE2_config.apply_settings(chosen_pal.settings) + if len(paletteLoader.pal_list) < 2 or paletteLoader.pal_list[selectedPalette].prevent_overwrite: UI['pal_remove'].state(('disabled',)) menus['pal'].entryconfigure(1, state=DISABLED) From 47eebff6ae45dc5db8aa5660cedbaa69c9be1aa3 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 14:58:34 +1000 Subject: [PATCH 12/31] Save and load StyleVars. --- src/StyleVarPane.py | 54 ++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/StyleVarPane.py b/src/StyleVarPane.py index caec6f9ba..c9eb8b6d3 100644 --- a/src/StyleVarPane.py +++ b/src/StyleVarPane.py @@ -3,19 +3,19 @@ from tkinter import ttk from collections import namedtuple -import functools import operator -import img as png - -from BEE2_config import GEN_OPTS from SubPane import SubPane +from srctools import Property import packageLoader import tooltip import utils import itemconfig +import BEE2_config +import img + +from typing import Union, List, Dict -from typing import Union stylevar = namedtuple('stylevar', 'id name default desc') @@ -88,9 +88,9 @@ checkbox_all = {} checkbox_chosen = {} checkbox_other = {} -tk_vars = {} +tk_vars = {} # type: Dict[str, IntVar] -VAR_LIST = [] +VAR_LIST = [] # type: List[packageLoader.StyleVar] STYLES = {} window = None @@ -99,7 +99,7 @@ def update_filter(): - pass + """Callback function replaced by tagsPane, to update items if needed.""" def add_vars(style_vars, styles): @@ -112,18 +112,25 @@ def add_vars(style_vars, styles): sorted(style_vars, key=operator.attrgetter('id')) ) - for var in VAR_LIST: # type: packageLoader.StyleVar - var.enabled = GEN_OPTS.get_bool('StyleVar', var.id, var.default) + for var in VAR_LIST: + var.enabled = BEE2_config.GEN_OPTS.get_bool('StyleVar', var.id, var.default) for style in styles: STYLES[style.id] = style -def set_stylevar(var): - """Save the value for a particular stylevar.""" - val = str(tk_vars[var].get()) - GEN_OPTS['StyleVar'][var] = val - if var == 'UnlockDefault': +@BEE2_config.option_handler('StyleVar') +def save_load_stylevars(props: Property=None): + """Save and load variables from configs.""" + if props is None: + props = Property('', []) + for var_id, var in sorted(tk_vars.items()): + props[var_id] = str(int(var.get())) + return props + else: + # Loading + for prop in props: + tk_vars[prop.real_name].set(prop.value) update_filter() @@ -216,12 +223,12 @@ def make_pane(tool_frame): global window window = SubPane( TK_ROOT, - options=GEN_OPTS, + options=BEE2_config.GEN_OPTS, title=_('Style/Item Properties'), name='style', resize_y=True, tool_frame=tool_frame, - tool_img=png.png('icons/win_stylevar'), + tool_img=img.png('icons/win_stylevar'), tool_col=3, ) @@ -284,17 +291,19 @@ def make_pane(tool_frame): all_pos = 0 for all_pos, var in enumerate(styleOptions): # Add the special stylevars which apply to all styles - tk_vars[var.id] = IntVar( - value=GEN_OPTS.get_bool('StyleVar', var.id, var.default) - ) + tk_vars[var.id] = int_var = IntVar(value=var.default) checkbox_all[var.id] = ttk.Checkbutton( frame_all, - variable=tk_vars[var.id], + variable=int_var, text=var.name, - command=functools.partial(set_stylevar, var.id) ) checkbox_all[var.id].grid(row=all_pos, column=0, sticky="W", padx=3) + # Special case - this needs to refresh the filter when swapping, + # so the items disappear or reappear. + if var.id == 'UnlockDefault': + checkbox_all[var.id]['command'] = lambda e: update_filter() + tooltip.add_tooltip( checkbox_all[var.id], make_desc(var, is_hardcoded=True), @@ -305,7 +314,6 @@ def make_pane(tool_frame): args = { 'variable': tk_vars[var.id], 'text': var.name, - 'command': functools.partial(set_stylevar, var.id) } desc = make_desc(var) if var.applies_to_all(): From c65cd6f0a993a8e4de7818496c0a0c527dc9d2a8 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 15:47:46 +1000 Subject: [PATCH 13/31] The event argument is not required. --- src/StyleVarPane.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/StyleVarPane.py b/src/StyleVarPane.py index c9eb8b6d3..cec6d41e2 100644 --- a/src/StyleVarPane.py +++ b/src/StyleVarPane.py @@ -302,7 +302,9 @@ def make_pane(tool_frame): # Special case - this needs to refresh the filter when swapping, # so the items disappear or reappear. if var.id == 'UnlockDefault': - checkbox_all[var.id]['command'] = lambda e: update_filter() + def cmd(): + update_filter() + checkbox_all[var.id]['command'] = cmd tooltip.add_tooltip( checkbox_all[var.id], From 49806c245a2eade774a1010b7f23248c36bb472d Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 15:48:18 +1000 Subject: [PATCH 14/31] Save and load settings on quit and restart --- src/BEE2_config.py | 20 ++++++++++++++++++++ src/UI.py | 2 ++ 2 files changed, 22 insertions(+) diff --git a/src/BEE2_config.py b/src/BEE2_config.py index c42a40238..13d765bf8 100644 --- a/src/BEE2_config.py +++ b/src/BEE2_config.py @@ -42,6 +42,26 @@ def apply_settings(props: Property): func(opt_prop) +def read_settings(): + """Read and apply the settings from disk.""" + try: + file = open(utils.conf_location('config/config.props'), encoding='utf8') + except FileNotFoundError: + return + with file: + props = Property.parse(file) + apply_settings(props) + + +def write_settings(): + """Write the settings to disk.""" + props = get_curr_settings() + props.name = None + with open(utils.conf_location('config/config.props'), 'w', encoding='utf8') as file: + for line in props.export(): + file.write(line) + + class ConfigFile(ConfigParser): """A version of ConfigParser which can easily save itself. diff --git a/src/UI.py b/src/UI.py index 64e1c3df8..b17948c06 100644 --- a/src/UI.py +++ b/src/UI.py @@ -439,6 +439,7 @@ def quit_application() -> None: # Clean up sounds. snd.clean_folder() + BEE2_config.write_settings() GEN_OPTS.save_check() item_opts.save_check() CompilerPane.COMPILE_CFG.save_check() @@ -2049,4 +2050,5 @@ def style_select_callback(style_id): style_select_callback(style_win.chosen_id) set_palette() # Set_palette needs to run first, so it can fix invalid palette indexes. + BEE2_config.read_settings() refresh_pal_ui() From 4f30cf410ec48118e44adf92eeee34422837321b Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 15:53:31 +1000 Subject: [PATCH 15/31] Use VDF as the suffix. --- src/BEE2_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BEE2_config.py b/src/BEE2_config.py index 13d765bf8..9868c731c 100644 --- a/src/BEE2_config.py +++ b/src/BEE2_config.py @@ -45,7 +45,7 @@ def apply_settings(props: Property): def read_settings(): """Read and apply the settings from disk.""" try: - file = open(utils.conf_location('config/config.props'), encoding='utf8') + file = open(utils.conf_location('config/config.vdf'), encoding='utf8') except FileNotFoundError: return with file: @@ -57,7 +57,7 @@ def write_settings(): """Write the settings to disk.""" props = get_curr_settings() props.name = None - with open(utils.conf_location('config/config.props'), 'w', encoding='utf8') as file: + with open(utils.conf_location('config/config.vdf'), 'w', encoding='utf8') as file: for line in props.export(): file.write(line) From 582384b10bd5536e173920a80abd7eba091c9b7b Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 15:54:39 +1000 Subject: [PATCH 16/31] Don't re-select the selected item. --- src/selectorWin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/selectorWin.py b/src/selectorWin.py index 9e5b04d1f..1c26ccd7b 100644 --- a/src/selectorWin.py +++ b/src/selectorWin.py @@ -1056,6 +1056,9 @@ def do_callback(self): def sel_item_id(self, it_id): """Select the item with the given ID.""" + if self.selected.name == it_id: + return True + if it_id == '': self.sel_item(self.noneItem) self.set_disp() From 8799d944de10bd0052de55ab133d58fb1ef65916 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 17 Sep 2017 15:55:00 +1000 Subject: [PATCH 17/31] Save and load selector windows. --- src/UI.py | 56 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/UI.py b/src/UI.py index b17948c06..dc8278d2e 100644 --- a/src/UI.py +++ b/src/UI.py @@ -6,6 +6,7 @@ import operator import random +from srctools import Property, NoKeyError import music_conf from tk_tools import TK_ROOT from query_dialogs import ask_string @@ -465,6 +466,30 @@ def load_settings(): optionWindow.load() +@BEE2_config.option_handler('LastSelected') +def save_load_selector_win(props: Property=None): + """Save and load options on the selector window.""" + sel_win = [ + ('Style', style_win), + ('Skybox', skybox_win), + ('Voice', voice_win), + ('Music', music_win), + ('Elevator', elev_win), + ] + if props is None: + props = Property('', []) + for win_name, win in sel_win: + props.append(Property(win_name, win.chosen_id or '')) + return props + + # Loading + for win_name, win in sel_win: + try: + win.sel_item_id(props[win_name]) + except NoKeyError: + pass + + def load_packages(data): """Import in the list of items and styles from the packages. @@ -535,11 +560,8 @@ def load_packages(data): def win_callback(style_id, win_name): """Callback for the selector windows. - This saves into the config file the last selected item. + This just refreshes if the 'apply selection' option is enabled. """ - if style_id is None: - style_id = '' - GEN_OPTS['Last_Selected'][win_name] = style_id suggested_refresh() def voice_callback(style_id): @@ -551,7 +573,6 @@ def voice_callback(style_id): voiceEditor.save() try: if style_id is None: - style_id = '' UI['conf_voice'].state(['disabled']) UI['conf_voice']['image'] = img.png('icons/gear_disabled') else: @@ -560,7 +581,6 @@ def voice_callback(style_id): except KeyError: # When first initialising, conf_voice won't exist! pass - GEN_OPTS['Last_Selected']['Voice'] = style_id suggested_refresh() skybox_win = selWin( @@ -639,23 +659,14 @@ def voice_callback(style_id): ] ) - last_style = GEN_OPTS.get_val('Last_Selected', 'Style', 'BEE2_CLEAN') - if last_style in style_win: - style_win.sel_item_id(last_style) - selected_style = last_style - else: - selected_style = 'BEE2_CLEAN' - style_win.sel_item_id('BEE2_CLEAN') + # Defaults, which will be reset at the end. + selected_style = 'BEE2_CLEAN' + style_win.sel_item_id('BEE2_CLEAN') - obj_types = [ - (voice_win, 'Voice'), - (skybox_win, 'Skybox'), - (elev_win, 'Elevator'), - ] - for (sel_win, opt_name), default in zip(obj_types, current_style().suggested): - sel_win.sel_item_id( - GEN_OPTS.get_val('Last_Selected', opt_name, default) - ) + voice_win.sel_suggested() + music_win.sel_suggested() + skybox_win.sel_suggested() + elev_win.sel_suggested() def current_style() -> packageLoader.Style: @@ -2016,7 +2027,6 @@ def style_select_callback(style_id): """Callback whenever a new style is chosen.""" global selected_style selected_style = style_id - GEN_OPTS['Last_Selected']['Style'] = style_id style_obj = current_style() From 2872ccdbe174fc28ce0ef415de1c3eb9b2088dc8 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Sun, 24 Sep 2017 08:55:36 +1000 Subject: [PATCH 18/31] Move export button back to original position. --- src/UI.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/UI.py b/src/UI.py index dc8278d2e..4d0050c39 100644 --- a/src/UI.py +++ b/src/UI.py @@ -1335,32 +1335,34 @@ def init_option(pane: SubPane): frame.grid(row=0, column=0, sticky=NSEW) frame.columnconfigure(0, weight=1) - ttk.Button( - frame, - textvariable=EXPORT_CMD_VAR, - command=export_editoritems, - ).grid(row=0, sticky="EW", padx=5) - ttk.Button( frame, text=_("Save Palette..."), command=pal_save, - ).grid(row=1, sticky="EW", padx=5) + ).grid(row=0, sticky="EW", padx=5) ttk.Button( frame, text=_("Save Palette As..."), command=pal_save_as, - ).grid(row=2, sticky="EW", padx=5) + ).grid(row=1, sticky="EW", padx=5) ttk.Checkbutton( frame, text=_('Save Settings in Palettes'), variable=var_pal_save_settings, - ).grid(row=3, sticky="EW", padx=5) + ).grid(row=2, sticky="EW", padx=5) + + ttk.Separator(frame, orient='horizontal').grid(row=3, sticky="EW") + + ttk.Button( + frame, + textvariable=EXPORT_CMD_VAR, + command=export_editoritems, + ).grid(row=4, sticky="EW", padx=5) - props = ttk.LabelFrame(frame, text=_("Properties"), width="50") + props = ttk.Frame(frame, width="50") props.columnconfigure(1, weight=1) - props.grid(row=4, sticky="EW") + props.grid(row=5, sticky="EW") music_frame = ttk.Labelframe(props, text=_('Music: ')) music_win = music_conf.make_widgets(music_frame, pane) From b25ad9519a9ea832abe6c8e47832138c4582a464 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 6 Dec 2018 09:32:56 +1000 Subject: [PATCH 19/31] Make music channels work properly with palette settings --- src/UI.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/UI.py b/src/UI.py index 4d0050c39..55149ffc7 100644 --- a/src/UI.py +++ b/src/UI.py @@ -6,7 +6,7 @@ import operator import random -from srctools import Property, NoKeyError +from srctools import Property import music_conf from tk_tools import TK_ROOT from query_dialogs import ask_string @@ -473,9 +473,12 @@ def save_load_selector_win(props: Property=None): ('Style', style_win), ('Skybox', skybox_win), ('Voice', voice_win), - ('Music', music_win), ('Elevator', elev_win), ] + for channel, win in music_conf.WINDOWS.items(): + sel_win.append(('Music_' + channel.name.title(), win)) + + # Saving if props is None: props = Property('', []) for win_name, win in sel_win: @@ -486,7 +489,7 @@ def save_load_selector_win(props: Property=None): for win_name, win in sel_win: try: win.sel_item_id(props[win_name]) - except NoKeyError: + except IndexError: pass @@ -664,9 +667,10 @@ def voice_callback(style_id): style_win.sel_item_id('BEE2_CLEAN') voice_win.sel_suggested() - music_win.sel_suggested() skybox_win.sel_suggested() elev_win.sel_suggested() + for win in music_conf.WINDOWS.values(): + win.sel_suggested() def current_style() -> packageLoader.Style: From 03aa8bdf4ba847fc373e8538fc1a930669d50f97 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 6 Dec 2018 09:33:12 +1000 Subject: [PATCH 20/31] Add gear to palette menu --- src/UI.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/UI.py b/src/UI.py index 55149ffc7..881cd263a 100644 --- a/src/UI.py +++ b/src/UI.py @@ -812,7 +812,10 @@ def refresh_pal_ui(): # Add a set of options to pick the palette into the menu system for val, pal in enumerate(paletteLoader.pal_list): menus['pal'].add_radiobutton( - label=pal.name, + label=( + pal.name if pal.settings is None + else '⚙' + pal.name + ), variable=selectedPalette_radio, value=val, command=set_pal_radio, From 7fc9b45e18fdcd6828f4fb596068e9146d69b35e Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 6 Dec 2018 09:33:20 +1000 Subject: [PATCH 21/31] Unused imports --- src/UI.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/UI.py b/src/UI.py index 881cd263a..63b82c776 100644 --- a/src/UI.py +++ b/src/UI.py @@ -15,7 +15,6 @@ from selectorWin import selWin, Item as selWinItem, AttrDef as SelAttr from loadScreen import main_loader as loader import srctools.logger -from srctools.filesys import FileSystem, FileSystemChain import sound as snd import BEE2_config import paletteLoader @@ -37,8 +36,7 @@ import backup as backup_win import tooltip -from typing import Iterable, List, Dict - +from typing import List, Dict LOGGER = srctools.logger.get_logger(__name__) From cbfc6be2028148e28d8782a7d4717fa17f00f87a Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 6 Dec 2018 09:33:40 +1000 Subject: [PATCH 22/31] Skip unknown stylevars --- src/StyleVarPane.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/StyleVarPane.py b/src/StyleVarPane.py index cec6d41e2..96082b156 100644 --- a/src/StyleVarPane.py +++ b/src/StyleVarPane.py @@ -7,6 +7,7 @@ from SubPane import SubPane from srctools import Property +from srctools.logger import get_logger import packageLoader import tooltip import utils @@ -17,6 +18,9 @@ from typing import Union, List, Dict +LOGGER = get_logger(__name__) + + stylevar = namedtuple('stylevar', 'id name default desc') # Special StyleVars that are hardcoded into the BEE2 @@ -130,7 +134,10 @@ def save_load_stylevars(props: Property=None): else: # Loading for prop in props: - tk_vars[prop.real_name].set(prop.value) + try: + tk_vars[prop.real_name].set(prop.value) + except KeyError: + LOGGER.warning('No stylevar "{}", skipping.', prop.real_name) update_filter() From 1543e23b1e09d5ba6a584439613f1ac9ea2541a6 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 6 Dec 2018 09:52:27 +1000 Subject: [PATCH 23/31] Type hint tweaks for options --- src/BEE2_config.py | 16 ++++++++++------ src/utils.py | 19 +++++++++---------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/BEE2_config.py b/src/BEE2_config.py index 9868c731c..6127961fd 100644 --- a/src/BEE2_config.py +++ b/src/BEE2_config.py @@ -1,4 +1,9 @@ -"""Modified version of ConfigParser that can be easily resaved. +"""Settings related logic for the application. + +Functions can be registered with a name, which will be called to save/load +settings. + +This also contains a version of ConfigParser that can be easily resaved. It only saves if the values are modified. Most functions are also altered to allow defaults instead of erroring. @@ -6,7 +11,6 @@ from configparser import ConfigParser, NoOptionError from srctools import AtomicWriter, Property -import os import utils import srctools.logger @@ -16,7 +20,7 @@ # Functions for saving or loading application settings. # Call with a block to load, or with no args to return the current # values. -option_handler = utils.FuncLookup('OptionHandlers') +option_handler = utils.FuncLookup('OptionHandlers') # type: utils.FuncLookup def get_curr_settings() -> Property: @@ -24,7 +28,7 @@ def get_curr_settings() -> Property: props = Property('', []) for opt_id, opt_func in option_handler.items(): - opt_prop = opt_func() + opt_prop = opt_func() # type: Property opt_prop.name = opt_id.title() props.append(opt_prop) @@ -42,7 +46,7 @@ def apply_settings(props: Property): func(opt_prop) -def read_settings(): +def read_settings() -> None: """Read and apply the settings from disk.""" try: file = open(utils.conf_location('config/config.vdf'), encoding='utf8') @@ -53,7 +57,7 @@ def read_settings(): apply_settings(props) -def write_settings(): +def write_settings() -> None: """Write the settings to disk.""" props = get_curr_settings() props.name = None diff --git a/src/utils.py b/src/utils.py index 4d2f8df41..2d21c19ee 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,5 +1,5 @@ """Various functions shared among the compiler and application.""" -import collections.abc +import collections import functools import logging import os @@ -11,9 +11,10 @@ from typing import ( Tuple, List, Set, Sequence, - Iterator, Iterable, SupportsInt, + Iterator, Iterable, SupportsInt, Mapping, TypeVar, Any, Union, Callable, Generator, + KeysView, ValuesView, ItemsView, ) try: @@ -394,7 +395,7 @@ class CONN_TYPES(Enum): RetT = TypeVar('RetT') -class FuncLookup(collections.abc.Mapping): +class FuncLookup(Mapping[str, Callable[..., Any]]): """A dict for holding callback functions. Functions are added by using this as a decorator. Positional arguments @@ -438,7 +439,7 @@ def callback(func: 'Callable[..., RetT]') -> 'Callable[..., RetT]': def __eq__(self, other: Any) -> bool: if isinstance(other, FuncLookup): return self._registry == other._registry - if not isinstance(other, collections.abc.Mapping): + if not isinstance(other, collections.Mapping): return NotImplemented return self._registry == dict(other.items()) @@ -446,19 +447,19 @@ def __iter__(self) -> Iterator[Callable[..., Any]]: """Yield all the functions.""" return iter(self.values()) - def keys(self): + def keys(self) -> KeysView[str]: """Yield all the valid IDs.""" return self._registry.keys() - def values(self): + def values(self) -> ValuesView[Callable[..., Any]]: """Yield all the functions.""" return self._registry.values() - def items(self): + def items(self) -> ItemsView[str, Callable[..., Any]]: """Return pairs of (ID, func).""" return self._registry.items() - def __len__(self): + def __len__(self) -> int: return len(set(self._registry.values())) def __getitem__(self, names: Union[str, Tuple[str]]) -> Callable[..., Any]: @@ -506,8 +507,6 @@ def functions(self) -> Set[Callable[..., Any]]: """Return the set of functions in this mapping.""" return set(self._registry.values()) - values = functions - def clear(self) -> None: """Delete all functions.""" self._registry.clear() From 065e0ec32da03f0250cace0a5fbc12f035dc4c19 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 6 Dec 2018 14:13:22 +1000 Subject: [PATCH 24/31] Remove duplicate import --- src/paletteLoader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/paletteLoader.py b/src/paletteLoader.py index b4c476bf9..70a160516 100644 --- a/src/paletteLoader.py +++ b/src/paletteLoader.py @@ -5,7 +5,6 @@ import utils import srctools.logger -from srctools import Property import BEE2_config from srctools import Property, NoKeyError From a196a818523523213b33c8aa42e440d8c33394d5 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Thu, 6 Dec 2018 16:29:13 +1000 Subject: [PATCH 25/31] Redo palette gear icons using a character This works far far better. --- src/UI.py | 70 ++++++++++++++----------------------------------------- 1 file changed, 18 insertions(+), 52 deletions(-) diff --git a/src/UI.py b/src/UI.py index 63b82c776..b5135c552 100644 --- a/src/UI.py +++ b/src/UI.py @@ -75,6 +75,10 @@ # A config file which remembers changed property options, chosen # versions, etc +# "Wheel of Dharma" / white sun, close enough and should be +# in most fonts. +CHR_GEAR = '☼ ' + class Item: """Represents an item that can appear on the list.""" @@ -750,16 +754,9 @@ def refresh_pal_ui(): listbox = UI['palette'] # type: Listbox listbox.delete(0, END) - gear_icons = listbox.gear_icons # type: Dict[int, Label] - - for gear in gear_icons.values(): - gear.destroy() - - gear_icons.clear() - for i, pal in enumerate(paletteLoader.pal_list): if pal.settings is not None: - listbox.insert(i, ' ' + pal.name) + listbox.insert(i, CHR_GEAR + pal.name) else: listbox.insert(i, pal.name) @@ -778,30 +775,6 @@ def refresh_pal_ui(): selectbackground=tk_tools.LISTBOX_BG_SEL_COLOR, ) - listbox.update() - - for i, pal in enumerate(paletteLoader.pal_list): - if pal.settings is not None: - gear = Label( - listbox, - bg='white', - image=img.png('icons/gear'), - width=10, - height=10, - borderwidth=0, - padx=0, - pady=0, - ) - x, y, width, height = listbox.bbox(i) - gear_icons[i] = gear - gear.place( - x=x, - y=y + (height-10)//2, - width=10, - height=10, - anchor='nw', - ) - for ind in range(menus['pal'].index(END), 0, -1): # Delete all the old radiobuttons # Iterate backward to ensure indexes stay the same. @@ -812,7 +785,7 @@ def refresh_pal_ui(): menus['pal'].add_radiobutton( label=( pal.name if pal.settings is None - else '⚙' + pal.name + else CHR_GEAR + pal.name ), variable=selectedPalette_radio, value=val, @@ -1130,14 +1103,6 @@ def set_palette(e=None): LOGGER.warning('Invalid palette index!') selectedPalette = 0 - # Update gear icons (if any) to match the background of their label. - # Do this before palettes update, so you can't see the change. - for ind, gear in UI['palette'].gear_icons.items(): - if ind == selectedPalette: - gear['bg'] = tk_tools.LISTBOX_BG_SEL_COLOR - else: - gear['bg'] = tk_tools.LISTBOX_BG_COLOR - chosen_pal = paletteLoader.pal_list[selectedPalette] GEN_OPTS['Last_Selected']['palette'] = str(selectedPalette) @@ -1291,31 +1256,32 @@ def init_palette(f): command=pal_clear, ).grid(row=0, sticky="EW") - UI['palette'] = Listbox(f, width=10) - UI['palette'].grid(row=1, sticky="NSEW") - - # Dict to store gear overlays for palettes with settings. - UI['palette'].gear_icons = {} + UI['palette'] = listbox = Listbox(f, width=10) + listbox.grid(row=1, sticky="NSEW") def set_pal_listbox(e=None): global selectedPalette - cur_selection = UI['palette'].curselection() + cur_selection = listbox.curselection() if cur_selection: # Might be blank if none selected selectedPalette = int(cur_selection[0]) selectedPalette_radio.set(selectedPalette) + + # Actually set palette.. set_palette() else: - UI['palette'].selection_set(selectedPalette, selectedPalette) - UI['palette'].bind("<>", set_pal_listbox) - UI['palette'].bind("", set_pal_listbox_selection) + listbox.selection_set(selectedPalette, selectedPalette) + + listbox.bind("<>", set_pal_listbox) + listbox.bind("", set_pal_listbox_selection) + # Set the selected state when hovered, so users can see which is # selected. - UI['palette'].selection_set(0) + listbox.selection_set(0) pal_scroll = tk_tools.HidingScroll( f, orient=VERTICAL, - command=UI['palette'].yview, + command=listbox.yview, ) pal_scroll.grid(row=1, column=1, sticky="NS") UI['palette']['yscrollcommand'] = pal_scroll.set From ba2d3b3747ef0f5ba3a6d0b699f2c895ed1a7a0f Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Fri, 7 Dec 2018 10:58:52 +1000 Subject: [PATCH 26/31] Implement palette option handler for itemVars --- src/itemconfig.py | 174 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 128 insertions(+), 46 deletions(-) diff --git a/src/itemconfig.py b/src/itemconfig.py index f10bf4f4a..8c6932950 100644 --- a/src/itemconfig.py +++ b/src/itemconfig.py @@ -7,7 +7,7 @@ from srctools import Property, Vec, conv_int, conv_bool from packageLoader import PakObject, ExportData, ParseData, desc_parse -from BEE2_config import ConfigFile +import BEE2_config from tooltip import add_tooltip import tkMarkdown import utils @@ -15,7 +15,7 @@ import img import sound -from typing import Union, Callable, List, Dict, Tuple +from typing import Union, Callable, List, Tuple, Optional LOGGER = srctools.logger.get_logger(__name__) @@ -28,7 +28,7 @@ # frame. WidgetLookupMulti = utils.FuncLookup('Multi-Widgets') -CONFIG = ConfigFile('item_cust_configs.cfg') +CONFIG = BEE2_config.ConfigFile('item_cust_configs.cfg') CONFIG_ORDER = [] # type: List[ConfigGroup] @@ -39,6 +39,46 @@ # For itemvariant, we need to refresh on style changes. ITEM_VARIANT_LOAD = [] + +@BEE2_config.option_handler('ItemVar') +def save_load_itemvar(prop: Property=None) -> Optional[Property]: + """Save or load item variables into the palette.""" + if prop is None: + prop = Property('', []) + for group in CONFIG_ORDER: + conf = Property(group.id, []) + for widget in group.widgets: # ItemVariant special case. + if widget.values is not None: + conf.append(Property(widget.id, widget.values.get())) + for widget in group.multi_widgets: + conf.append(Property(widget.id, [ + Property(str(tim_val), var.get()) + for tim_val, var in + widget.values + ])) + prop.append(conf) + return prop + else: + # Loading. + for group in CONFIG_ORDER: + conf = prop.find_key(group.id, []) + for widget in group.widgets: + if widget.values is not None: # ItemVariants + try: + widget.values.set(conf[widget.id]) + except LookupError: + pass + + for widget in group.multi_widgets: + time_conf = conf.find_key(widget.id, []) + for tim_val, var in widget.values: + try: + var.set(time_conf[str(tim_val)]) + except LookupError: + pass + return None + + class Widget: """Represents a widget that can appear on a ConfigGroup.""" def __init__( @@ -49,7 +89,7 @@ def __init__( create_func: Callable[[tk.Frame, tk.StringVar, Property], None], multi_func: Callable[[tk.Frame, List[Tuple[str, tk.StringVar]], Property], None], config: Property, - values: Union[tk.StringVar, Dict[Union[int, str], tk.StringVar]], + values: Union[tk.StringVar, List[Tuple[Union[int, str], tk.StringVar]]], is_timer: bool, use_inf: bool, ): @@ -92,8 +132,8 @@ def parse(cls, data: ParseData) -> 'PakObject': desc = desc_parse(props, data.id) - widgets = [] - multi_widgets = [] + widgets = [] # type: List[Widget] + multi_widgets = [] # type: List[Widget] for wid in props.find_all('Widget'): try: @@ -123,6 +163,7 @@ def parse(cls, data: ParseData) -> 'PakObject': if is_timer: LOGGER.warning("Item Variants can't be timers! ({}.{})", data.id, wid_id) is_timer = use_inf = False + # Dummy, not used. values = None elif is_timer: if default.has_children(): @@ -434,19 +475,22 @@ def widget_checkmark(parent: tk.Frame, var: tk.StringVar, conf: Property): @WidgetLookupMulti('boolean', 'bool', 'checkbox') -def widget_checkmark( - parent: tk.Frame, values: List[Tuple[str, tk.StringVar]], conf: Property): +def widget_checkmark_multi( + parent: tk.Frame, + values: List[Tuple[str, tk.StringVar]], + conf: Property, +) -> tk.Widget: """For checkmarks, display in a more compact form.""" for row, column, tim_text, var in multi_grid(values): checkbox = widget_checkmark(parent, var, conf) checkbox.grid(row=row, column=column) add_tooltip(checkbox, tim_text, delay=0) + return parent @WidgetLookup('range', 'slider') -def widget_slider(parent: tk.Frame, var: tk.StringVar, conf: Property) -> tk.Misc: +def widget_slider(parent: tk.Frame, var: tk.StringVar, conf: Property) -> tk.Widget: """Provides a slider for setting a number in a range.""" - scale = tk.Scale( parent, orient='horizontal', @@ -460,7 +504,11 @@ def widget_slider(parent: tk.Frame, var: tk.StringVar, conf: Property) -> tk.Mis @WidgetLookup('color', 'colour', 'rgb') -def widget_color_single(parent: tk.Frame, var: tk.StringVar, conf: Property) -> tk.Misc: +def widget_color_single( + parent: tk.Frame, + var: tk.StringVar, + conf: Property, +) -> tk.Widget: """Provides a colour swatch for specifying colours. Values can be provided as #RRGGBB, but will be written as 3 0-255 values. @@ -486,42 +534,57 @@ def make_color_swatch(parent: tk.Frame, var: tk.StringVar, size=16) -> ttk.Label """Make a single swatch.""" # Note: tkinter requires RGB as ints, not float! - color = var.get() - if color.startswith('#'): - try: - r, g, b = int(var[0:2], base=16), int(var[2:4], base=16), int(var[4:], base=16) - except ValueError: - LOGGER.warning('Invalid RGB value: "{}"!', color) - r = g = b = 128 - else: - r, g, b = map(int, Vec.from_str(color, 128, 128, 128)) + def get_color(): + """Parse out the color.""" + color = var.get() + if color.startswith('#'): + try: + r = int(color[1:3], base=16) + g = int(color[3:5], base=16) + b = int(color[5:], base=16) + except ValueError: + LOGGER.warning('Invalid RGB value: "{}"!', color) + r = g = b = 128 + else: + r, g, b = map(int, Vec.from_str(color, 128, 128, 128)) + return r, g, b def open_win(e): """Display the color selection window.""" - nonlocal r, g, b widget_sfx() + r, g, b = get_color() new_color, tk_color = askcolor( color=(r, g, b), parent=parent.winfo_toplevel(), title=_('Choose a Color'), ) if new_color is not None: - r, g, b = map(int, new_color) # Returned as floats, which is wrong. + r, g, b = map(int, new_color) # Returned as floats, which is wrong. var.set('{} {} {}'.format(int(r), int(g), int(b))) - swatch['image'] = img.color_square(round(Vec(r, g, b)), size) swatch = ttk.Label( parent, relief='raised', - image=img.color_square(Vec(r, g, b), size), ) + + def update_image(var_name: str, var_index: str, operation: str): + r, g, b = get_color() + swatch['image'] = img.color_square(round(Vec(r, g, b)), size) + + update_image('', '', '') + + # Register a function to be called whenever this variable is changed. + var.trace_add('write', update_image) + + LOGGER.info('Trace info: {}', var.trace_info()) + utils.bind_leftclick(swatch, open_win) return swatch @lru_cache(maxsize=20) -def timer_values(min_value, max_value): +def timer_values(min_value: int, max_value: int) -> List[str]: """Return 0:38-like strings up to the max value.""" return [ '{}:{:02}'.format(i//60, i % 60) @@ -553,31 +616,49 @@ def widget_minute_seconds(parent: tk.Frame, var: tk.StringVar, conf: Property) - values = timer_values(min_value, max_value) - default_value = conv_int(var.get(), -1) - if min_value <= default_value <= max_value: - default_text = values[default_value - min_value] - else: - LOGGER.warning('Bad timer value "{}" for "{}"!', var.get(), conf['id']) - default_text = '0:01' - var.set('1') - + # Stores the 'pretty' value in the actual textbox. disp_var = tk.StringVar() + existing_value = var.get() + + def update_disp(var_name: str, var_index: str, operation: str) -> None: + """Whenever the string changes, update the displayed text.""" + seconds = conv_int(var.get(), -1) + if min_value <= seconds <= max_value: + disp_var.set('{}:{:02}'.format(seconds // 60, seconds % 60)) + else: + LOGGER.warning('Bad timer value "{}" for "{}"!', var.get(), conf['id']) + # Recurse, with a known safe value. + var.set(values[0]) + + # Whenever written to, call this. + var.trace_add('write', update_disp) + def set_var(): """Set the variable to the current value.""" try: minutes, seconds = disp_var.get().split(':') - var.set(int(minutes) * 60 + int(seconds)) + var.set(str(int(minutes) * 60 + int(seconds))) except (ValueError, TypeError): pass def validate(reason: str, operation_type: str, cur_value: str, new_char: str, new_value: str): - """Validate the values for the text.""" + """Validate the values for the text. + + This is called when the textbox is modified, to allow cancelling bad + inputs. + + Reason is the reason this was fired: 'key', 'focusin', 'focusout', 'forced'. + operation_type is '1' for insert, '0' for delete', '-1' for programmatic changes. + cur_val is the value before the change occurs. + new_char is the added/removed text. + new_value is the value after the change, if accepted. + """ if operation_type == '0' or reason == 'forced': # Deleting or done by the program, allow that always. return True - if operation_type == '1': + if operation_type == '1': # Inserted text. # Disallow non number and colons if new_char not in '0123456789:': return False @@ -593,16 +674,16 @@ def validate(reason: str, operation_type: str, cur_value: str, new_char: str, ne if reason == 'focusout': # When leaving focus, apply range limits and set the var. try: - minutes, seconds = new_value.split(':') - seconds = int(minutes) * 60 + int(seconds) + str_min, str_sec = new_value.split(':') + seconds = int(str_min) * 60 + int(str_sec) except (ValueError, TypeError): - seconds = default_value - if seconds < min_value: seconds = min_value - if seconds > max_value: - seconds = max_value - disp_var.set('{}:{:02}'.format(seconds // 60, seconds % 60)) - var.set(seconds) + else: + if seconds < min_value: + seconds = min_value + if seconds > max_value: + seconds = max_value + var.set(str(seconds)) # This then re-writes the textbox. return True validate_cmd = parent.register(validate) @@ -617,9 +698,10 @@ def validate(reason: str, operation_type: str, cur_value: str, new_char: str, ne width=5, validate='all', - # %args substitute the values for the args to validate_cmd. + # These define which of the possible values will be passed along. + # http://tcl.tk/man/tcl8.6/TkCmd/spinbox.htm#M26 validatecommand=(validate_cmd, '%V', '%d', '%s', '%S', '%P'), ) # We need to set this after, it gets reset to the first one. - disp_var.set(default_text) + var.set(existing_value) return spinbox From 2846760f8047a4dcd82914e3c8f08cd2af2534f9 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Fri, 7 Dec 2018 11:19:10 +1000 Subject: [PATCH 27/31] Add type hints to BEE2_config.ConfigFile --- src/BEE2_config.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/BEE2_config.py b/src/BEE2_config.py index 6127961fd..510293a1d 100644 --- a/src/BEE2_config.py +++ b/src/BEE2_config.py @@ -8,7 +8,9 @@ It only saves if the values are modified. Most functions are also altered to allow defaults instead of erroring. """ -from configparser import ConfigParser, NoOptionError +from configparser import ConfigParser, NoOptionError, SectionProxy +from typing import Any, Mapping + from srctools import AtomicWriter, Property import utils @@ -74,7 +76,13 @@ class ConfigFile(ConfigParser): get_val, get_bool, and get_int are modified to return defaults instead of erroring. """ - def __init__(self, filename, *, in_conf_folder=True, auto_load=True): + def __init__( + self, + filename: str, + *, + in_conf_folder: bool=True, + auto_load: bool=True, + ) -> None: """Initialise the config file. `filename` is the name of the config file, in the `root` directory. @@ -100,7 +108,8 @@ def __init__(self, filename, *, in_conf_folder=True, auto_load=True): else: self.filename = self.writer = None - def load(self): + def load(self) -> None: + """Load config options from disk.""" if self.filename is None: return @@ -116,7 +125,7 @@ def load(self): # We're not different to the file on disk.. self.has_changed = False - def save(self): + def save(self) -> None: """Write our values out to disk.""" LOGGER.info('Saving changes in config "{}"!', self.filename) if self.filename is None: @@ -126,12 +135,12 @@ def save(self): self.write(conf) self.has_changed = False - def save_check(self): + def save_check(self) -> None: """Check to see if we have different values, and save if needed.""" if self.has_changed: self.save() - def set_defaults(self, def_settings): + def set_defaults(self, def_settings: Mapping[str, Mapping[str, Any]]) -> None: """Set the default values if the settings file has no values defined.""" for sect, values in def_settings.items(): if sect not in self: @@ -155,15 +164,15 @@ def get_val(self, section: str, value: str, default: str) -> str: self[section][value] = default return default - def __getitem__(self, section): + def __getitem__(self, section: str) -> SectionProxy: + """Allows setting/getting config[section][value].""" try: return super().__getitem__(section) except KeyError: self[section] = {} return super().__getitem__(section) - - def getboolean(self, section, value, default=False) -> bool: + def getboolean(self, section: str, value: str, default: bool=False) -> bool: """Get the value in the specified section, coercing to a Boolean. If either does not exist, set to the default and return it. @@ -180,7 +189,7 @@ def getboolean(self, section, value, default=False) -> bool: get_bool = getboolean - def getint(self, section, value, default=0) -> int: + def getint(self, section: str, value: str, default: int=0) -> int: """Get the value in the specified section, coercing to a Integer. If either does not exist, set to the default and return it. @@ -196,15 +205,15 @@ def getint(self, section, value, default=0) -> int: get_int = getint - def add_section(self, section): + def add_section(self, section: str) -> None: self.has_changed = True super().add_section(section) - def remove_section(self, section): + def remove_section(self, section: str) -> None: self.has_changed = True super().remove_section(section) - def set(self, section, option, value=None): + def set(self, section: str, option: str, value: str) -> None: orig_val = self.get(section, option, fallback=None) value = str(value) if orig_val is None or orig_val != value: From 4a4b8a94293145b802839c4545598a20c67990bd Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Fri, 7 Dec 2018 11:19:27 +1000 Subject: [PATCH 28/31] Write config.vdf out with AtomicWriter --- src/BEE2_config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/BEE2_config.py b/src/BEE2_config.py index 510293a1d..ec279b3b2 100644 --- a/src/BEE2_config.py +++ b/src/BEE2_config.py @@ -63,7 +63,10 @@ def write_settings() -> None: """Write the settings to disk.""" props = get_curr_settings() props.name = None - with open(utils.conf_location('config/config.vdf'), 'w', encoding='utf8') as file: + with AtomicWriter( + str(utils.conf_location('config/config.vdf')), + is_bytes=False, + ) as file: for line in props.export(): file.write(line) From 4685b0f9a339243dbaa09ec8c00e74985ef9ee57 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Fri, 7 Dec 2018 11:19:48 +1000 Subject: [PATCH 29/31] Save new config file also when exporting --- src/UI.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/UI.py b/src/UI.py index b5135c552..22a46a877 100644 --- a/src/UI.py +++ b/src/UI.py @@ -878,6 +878,7 @@ def export_editoritems(e=None): # Save the configs since we're writing to disk lots anyway. GEN_OPTS.save_check() item_opts.save_check() + BEE2_config.write_settings() message = _('Selected Items and Style successfully exported!') if not vpk_success: From 59de432a8e886f7ff74b4a85721c458801464cb7 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Fri, 7 Dec 2018 12:47:09 +1000 Subject: [PATCH 30/31] Tweak type hint --- src/itemconfig.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/itemconfig.py b/src/itemconfig.py index 8c6932950..bc14f455b 100644 --- a/src/itemconfig.py +++ b/src/itemconfig.py @@ -157,14 +157,14 @@ def parse(cls, data: ParseData) -> 'PakObject': name = wid['Label'] tooltip = wid['Tooltip', ''] default = wid.find_key('Default', '') + values = None # type: Union[List[Tuple[str, tk.StringVar]], tk.StringVar] # Special case - can't be timer, and no values. if create_func is widget_item_variant: if is_timer: LOGGER.warning("Item Variants can't be timers! ({}.{})", data.id, wid_id) is_timer = use_inf = False - # Dummy, not used. - values = None + # Values remains a dummy None value, we don't use it. elif is_timer: if default.has_children(): defaults = { @@ -576,8 +576,6 @@ def update_image(var_name: str, var_index: str, operation: str): # Register a function to be called whenever this variable is changed. var.trace_add('write', update_image) - LOGGER.info('Trace info: {}', var.trace_info()) - utils.bind_leftclick(swatch, open_win) return swatch From c367cc57220b088d31525d4f60625c04bdec5b12 Mon Sep 17 00:00:00 2001 From: TeamSpen210 Date: Fri, 7 Dec 2018 13:41:20 +1000 Subject: [PATCH 31/31] Add option handler for compiler pane --- src/CompilerPane.py | 118 +++++++++++++++++++++++++++++++------------- 1 file changed, 83 insertions(+), 35 deletions(-) diff --git a/src/CompilerPane.py b/src/CompilerPane.py index e6f5322dc..def9cb8c2 100644 --- a/src/CompilerPane.py +++ b/src/CompilerPane.py @@ -1,7 +1,7 @@ -import tkMarkdown -from srctools.logger import init_logging, get_logger import tk_tools import utils +from srctools.logger import init_logging, get_logger + if __name__ == '__main__': utils.fix_cur_directory() LOGGER = init_logging( @@ -20,16 +20,17 @@ from tkinter import filedialog from PIL import Image, ImageTk -from typing import Dict, Tuple - -import os - -from BEE2_config import ConfigFile, GEN_OPTS +from BEE2_config import ConfigFile, GEN_OPTS, option_handler from packageLoader import CORRIDOR_COUNTS, CorrDesc from tooltip import add_tooltip, set_tooltip +from srctools import Property import selectorWin +import tkMarkdown import SubPane import img +import os + +from typing import Dict, Tuple, Optional # The size of PeTI screenshots PETI_WIDTH = 555 @@ -83,7 +84,7 @@ COMPILE_CFG = ConfigFile('compile.cfg') COMPILE_CFG.set_defaults(COMPILE_DEFAULTS) window = None -UI = {} +UI = {} # type: Dict[str, Widget] chosen_thumb = StringVar( value=COMPILE_CFG.get_val('Screenshot', 'Type', 'AUTO') @@ -162,6 +163,66 @@ value=COMPILE_CFG.get_bool('Screenshot', 'del_old', True) ) + +@option_handler('CompilerPane') +def save_load_compile_pane(props: Optional[Property]=None) -> Optional[Property]: + """Save/load compiler options from the palette. + + Note: We specifically do not save/load the following: + - packfile dumping + - compile counts + This is because these are more system-dependent than map dependent. + """ + if props is None: # Saving + corr_prop = Property('corridor', []) + props = Property('', [ + Property('sshot_type', chosen_thumb.get()), + Property('sshot_cleanup', str(cleanup_screenshot.get())), + Property('spawn_elev', str(start_in_elev.get())), + Property('player_model', PLAYER_MODELS_REV[player_model_var.get()]), + Property('use_voice_priority', str(VOICE_PRIORITY_VAR.get())), + corr_prop, + ]) + for group, win in CORRIDOR.items(): + corr_prop[group] = win.chosen_id or '' + + return props + + # else: Loading + + chosen_thumb.set(props['sshot_type', chosen_thumb.get()]) + cleanup_screenshot.set(props.bool('sshot_cleanup', cleanup_screenshot.get())) + + # Refresh these. + set_screen_type() + set_screenshot() + + start_in_elev.set(props.bool('spawn_elev', start_in_elev.get())) + + try: + player_mdl = props['player_model'] + except LookupError: + pass + else: + player_model_var.set(PLAYER_MODELS[player_mdl]) + COMPILE_CFG['General']['player_model'] = player_mdl + + VOICE_PRIORITY_VAR.set(props.bool('use_voice_priority', VOICE_PRIORITY_VAR.get())) + + corr_prop = props.find_key('corridor', []) + for group, win in CORRIDOR.items(): + try: + sel_id = corr_prop[group] + except LookupError: + "No config option, ok." + else: + win.sel_item_id(sel_id) + COMPILE_CFG['Corridor'][group] = '0' if sel_id == '' else sel_id + + COMPILE_CFG.save_check() + return None + + def load_corridors() -> None: """Parse corridors out of the config file.""" corridor_conf = COMPILE_CFG['CorridorNames'] @@ -323,12 +384,12 @@ def refresh_counts(reload=True): flash_count() -def set_pack_dump_dir(path): +def set_pack_dump_dir(path: str) -> None: COMPILE_CFG['General']['packfile_dump_dir'] = path COMPILE_CFG.save_check() -def set_pack_dump_enabled(): +def set_pack_dump_enabled() -> None: is_enabled = packfile_dump_enable.get() COMPILE_CFG['General']['packfile_dump_enable'] = str(is_enabled) COMPILE_CFG.save_check() @@ -409,34 +470,25 @@ def set_model(e=None): COMPILE_CFG.save() -def set_corr(corr_name, e): - """Save the chosen corridor when it's changed. - - This is shared by all three dropdowns. - """ - COMPILE_CFG['Corridor'][corr_name] = str(e.widget.current()) - COMPILE_CFG.save() - - -def set_corr_dropdown(corr_name, widget): - """Set the values in the dropdown when it's opened.""" - widget['values'] = CORRIDOR[corr_name] - - -def make_setter(section, config, variable): +def make_setter(section: str, config: str, variable: Variable) -> None: """Create a callback which sets the given config from a variable.""" - - def callback(): + def callback(var_name: str, var_ind: str, cback_name: str) -> None: + """Automatically called when the variable is written to.""" COMPILE_CFG[section][config] = str(variable.get()) COMPILE_CFG.save_check() - return callback + variable.trace_add('write', callback) -def make_widgets(): +def make_widgets() -> None: """Create the compiler options pane. """ + make_setter('General', 'use_voice_priority', VOICE_PRIORITY_VAR) + make_setter('General', 'spawn_elev', start_in_elev) + make_setter('Screenshot', 'del_old', cleanup_screenshot) + make_setter('General', 'vrad_force_full', vrad_light_type) + ttk.Label(window, justify='center', text=_( "Options on this panel can be changed \n" "without exporting or restarting the game." @@ -515,7 +567,6 @@ def make_comp_widgets(frame: ttk.Frame): thumb_frame, text=_('Cleanup old screenshots'), variable=cleanup_screenshot, - command=make_setter('Screenshot', 'del_old', cleanup_screenshot), ) UI['thumb_auto'].grid(row=0, column=0, sticky='W') @@ -572,7 +623,6 @@ def make_comp_widgets(frame: ttk.Frame): text=_('Fast'), value=0, variable=vrad_light_type, - command=make_setter('General', 'vrad_force_full', vrad_light_type), ) UI['light_fast'].grid(row=0, column=0) UI['light_full'] = ttk.Radiobutton( @@ -580,7 +630,6 @@ def make_comp_widgets(frame: ttk.Frame): text=_('Full'), value=1, variable=vrad_light_type, - command=make_setter('General', 'vrad_force_full', vrad_light_type), ) UI['light_full'].grid(row=0, column=1) @@ -709,6 +758,7 @@ def make_map_widgets(frame: ttk.Frame): These are things which mainly affect the geometry or gameplay of the map. """ + frame.columnconfigure(0, weight=1) voice_frame = ttk.LabelFrame( @@ -722,7 +772,6 @@ def make_map_widgets(frame: ttk.Frame): voice_frame, text=_("Use voiceline priorities"), variable=VOICE_PRIORITY_VAR, - command=make_setter('General', 'use_voice_priority', VOICE_PRIORITY_VAR), ) voice_priority.grid(row=0, column=0) add_tooltip( @@ -742,12 +791,12 @@ def make_map_widgets(frame: ttk.Frame): elev_frame.columnconfigure(0, weight=1) elev_frame.columnconfigure(1, weight=1) + UI['elev_preview'] = ttk.Radiobutton( elev_frame, text=_('Entry Door'), value=0, variable=start_in_elev, - command=make_setter('General', 'spawn_elev', start_in_elev), ) UI['elev_elevator'] = ttk.Radiobutton( @@ -755,7 +804,6 @@ def make_map_widgets(frame: ttk.Frame): text=_('Elevator'), value=1, variable=start_in_elev, - command=make_setter('General', 'spawn_elev', start_in_elev), ) UI['elev_preview'].grid(row=0, column=0, sticky=W)