def make_help_menu(parent: tk.Menu): """Create the application 'Help' menu.""" # Using this name displays this correctly in OS X help = tk.Menu(parent, name='help') parent.add_cascade(menu=help, label=gettext('Help')) icons: Dict[ResIcon, img.Handle] = { icon: img.Handle.sprite('icons/' + icon.value, 16, 16) for icon in ResIcon if icon is not ResIcon.NONE } icons[ResIcon.NONE] = img.Handle.blank(16, 16) credits = Dialog( title=gettext('BEE2 Credits'), text=CREDITS_TEXT, ) for res in WEB_RESOURCES: if res is SEPERATOR: help.add_separator() else: help.add_command( label=res.name, command=functools.partial(webbrowser.open, res.url), compound='left', image=icons[res.icon].get_tk(), ) help.add_separator() help.add_command( label=gettext('Credits...'), command=credits.show, )
def ui_load_backup() -> None: """Prompt and load in a backup file.""" file = filedialog.askopenfilename( title=gettext('Load Backup'), filetypes=[(gettext('Backup zip'), '.zip')], ) if not file: return BACKUPS['backup_path'] = file with open(file, 'rb') as f: # Read the backup zip into memory! data = f.read() BACKUPS['unsaved_file'] = unsaved = BytesIO(data) zip_file = ZipFile( unsaved, mode='a', compression=ZIP_LZMA, ) try: BACKUPS['back'] = load_backup(zip_file) BACKUPS['backup_zip'] = zip_file BACKUPS['backup_name'] = os.path.basename(file) backup_name.set(BACKUPS['backup_name']) refresh_back_details() except loadScreen.Cancelled: zip_file.close()
def make_widgets() -> None: """Create the compiler options pane. """ make_setter('General', 'voiceline_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=gettext("Options on this panel can be changed \n" "without exporting or restarting the game.")).grid( row=0, column=0, sticky='ew', padx=2, pady=2) UI['nbook'] = nbook = ttk.Notebook(window) nbook.grid(row=1, column=0, sticky='nsew') window.columnconfigure(0, weight=1) window.rowconfigure(1, weight=1) nbook.enable_traversal() map_frame = ttk.Frame(nbook) # note: Tab name nbook.add(map_frame, text=gettext('Map Settings')) make_map_widgets(map_frame) comp_frame = ttk.Frame(nbook) # note: Tab name nbook.add(comp_frame, text=gettext('Compile Settings')) make_comp_widgets(comp_frame)
def make_corr_wid(corr_name: str, title: str) -> None: """Create the corridor widget and items.""" length = CORRIDOR_COUNTS[corr_name] CORRIDOR[corr_name] = sel = selector_win.SelectorWin( TK_ROOT, [ selector_win.Item( str(i), 'INVALID: ' + str(i), ) for i in range(1, length + 1) ], save_id='corr_' + corr_name, title=title, none_desc=gettext('Randomly choose a corridor. ' 'This is saved in the puzzle data ' 'and will not change.'), none_icon=img.Handle.builtin('BEE2/random', 96, 96), none_name=gettext('Random'), callback=sel_corr_callback, callback_params=[corr_name], ) chosen_corr = COMPILE_CFG.get_int('Corridor', corr_name) if chosen_corr == 0: sel.sel_item_id('<NONE>') else: sel.sel_item_id(str(chosen_corr))
def init_win_tab(f: ttk.Frame) -> None: keep_inside = make_checkbox( f, section='General', item='keep_win_inside', desc=gettext('Keep windows inside screen'), tooltip=gettext( 'Prevent sub-windows from moving outside the screen borders. ' 'If you have multiple monitors, disable this.'), var=KEEP_WIN_INSIDE, ) keep_inside.grid(row=0, column=0, sticky=W) make_checkbox( f, section='General', item='splash_stay_ontop', desc=gettext('Keep loading screens on top'), var=FORCE_LOAD_ONTOP, tooltip=gettext( "Force loading screens to be on top of other windows. " "Since they don't appear on the taskbar/dock, they can't be " "brought to the top easily again."), ).grid(row=0, column=1, sticky=E) ttk.Button( f, text=gettext('Reset All Window Positions'), # Indirect reference to allow UI to set this later command=lambda: reset_all_win(), ).grid(row=1, column=0, sticky=EW)
def init_widgets() -> None: """Create all the widgets.""" UI['nbook'] = nbook = ttk.Notebook(win, ) UI['nbook'].grid( row=0, column=0, padx=5, pady=5, sticky=NSEW, ) win.columnconfigure(0, weight=1) win.rowconfigure(0, weight=1) UI['fr_general'] = fr_general = ttk.Frame(nbook, ) nbook.add(fr_general, text=gettext('General')) init_gen_tab(fr_general) UI['fr_win'] = fr_win = ttk.Frame(nbook, ) nbook.add(fr_win, text=gettext('Windows')) init_win_tab(fr_win) UI['fr_dev'] = fr_dev = ttk.Frame(nbook, ) nbook.add(fr_dev, text=gettext('Development')) init_dev_tab(fr_dev) ok_cancel = ttk.Frame(win) ok_cancel.grid( row=1, column=0, padx=5, pady=5, sticky=E, ) def ok() -> None: save() win.withdraw() def cancel() -> None: win.withdraw() load() # Rollback changes UI['ok_btn'] = ok_btn = ttk.Button( ok_cancel, text=gettext('OK'), command=ok, ) UI['cancel_btn'] = cancel_btn = ttk.Button( ok_cancel, text=gettext('Cancel'), command=cancel, ) ok_btn.grid(row=0, column=0) cancel_btn.grid(row=0, column=1) win.protocol("WM_DELETE_WINDOW", cancel) save() # And ensure they are applied to other windows
def event_rename(self) -> None: """Rename an existing palette.""" if self.selected.readonly: return name = tk_tools.prompt(gettext("BEE2 - Save Palette"), gettext("Enter a name:")) if name is None: # Cancelled... return self.selected.name = name self.update_state()
def init_toplevel() -> None: """Initialise the window as part of the BEE2.""" global window window = tk.Toplevel(TK_ROOT) window.transient(TK_ROOT) window.withdraw() window.title(gettext('Backup/Restore Puzzles')) def quit_command(): from BEE2_config import GEN_OPTS window.withdraw() GEN_OPTS.save_check() # Don't destroy window when quit! window.protocol("WM_DELETE_WINDOW", quit_command) init() init_backup_settings() # When embedded in the BEE2, use regular buttons and a dropdown! toolbar_frame = ttk.Frame( window, ) ttk.Button( toolbar_frame, text=gettext('New Backup'), command=ui_new_backup, width=14, ).grid(row=0, column=0) ttk.Button( toolbar_frame, text=gettext('Open Backup'), command=ui_load_backup, width=13, ).grid(row=0, column=1) ttk.Button( toolbar_frame, text=gettext('Save Backup'), command=ui_save_backup, width=11, ).grid(row=0, column=2) ttk.Button( toolbar_frame, text='.. As', command=ui_save_backup_as, width=5, ).grid(row=0, column=3) toolbar_frame.grid(row=0, column=0, columnspan=3, sticky='W') ui_new_backup()
def save_backup(): """Save the backup file.""" # We generate it from scratch, since that's the only way to remove # files. new_zip_data = BytesIO() new_zip = ZipFile(new_zip_data, 'w', compression=ZIP_LZMA) maps = [ item.p2c for item in UI['back_details'].items ] if not maps: messagebox.showerror( gettext('BEE2 Backup'), gettext('No maps were chosen to backup!'), ) return copy_loader.set_length('COPY', len(maps)) with copy_loader: for p2c in maps: # type: P2C old_zip = p2c.zip_file map_path = p2c.filename + '.p2c' scr_path = p2c.filename + '.jpg' if scr_path in zip_names(old_zip): with zip_open_bin(old_zip, scr_path) as f: new_zip.writestr(scr_path, f.read()) # Copy the map as bytes, so encoded characters are transfered # unaltered. with zip_open_bin(old_zip, map_path) as f: new_zip.writestr(map_path, f.read()) copy_loader.step('COPY') new_zip.close() # Finalize zip with open(BACKUPS['backup_path'], 'wb') as backup: backup.write(new_zip_data.getvalue()) BACKUPS['unsaved_file'] = new_zip_data # Remake the zipfile object, so it's open again. BACKUPS['backup_zip'] = new_zip = ZipFile( new_zip_data, mode='w', compression=ZIP_LZMA, ) # Update the items, so they use this zip now. for p2c in maps: p2c.zip_file = new_zip
def on_error( exc_type: Type[BaseException], exc_value: BaseException, exc_tb: TracebackType, ) -> None: """Run when the application crashes. Display to the user, log it, and quit.""" # We don't want this to fail, so import everything here, and wrap in # except Exception. import traceback err = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)) # Grab and release the grab so nothing else can block the error message. try: TK_ROOT.grab_set_global() TK_ROOT.grab_release() # Append traceback to the clipboard. TK_ROOT.clipboard_append(err) except Exception: pass if not issubclass(exc_type, Exception): # It's subclassing BaseException (KeyboardInterrupt, SystemExit), # so ignore the error. return # Put it onscreen. try: from tkinter import messagebox from localisation import gettext messagebox.showinfo( title=gettext('BEEMOD {} Error!').format(utils.BEE_VERSION), message=gettext( 'An error occurred: \n{}\n\n' 'This has been copied to the clipboard.').format(err), icon=messagebox.ERROR, ) except Exception: pass try: from BEE2_config import GEN_OPTS # Try to turn on the logging window for next time.. GEN_OPTS.load() GEN_OPTS['Debug']['show_log_win'] = '1' GEN_OPTS['Debug']['window_log_level'] = 'DEBUG' GEN_OPTS.save() except Exception: # Ignore failures... pass
def ui_save_backup_as() -> None: """Prompt for a name, and then save a backup.""" path = filedialog.asksaveasfilename( title=gettext('Save Backup As'), filetypes=[(gettext('Backup zip'), '.zip')], ) if not path: return if not path.endswith('.zip'): path += '.zip' BACKUPS['backup_path'] = path BACKUPS['backup_name'] = os.path.basename(path) backup_name.set(BACKUPS['backup_name']) ui_save_backup()
def event_change_group(self) -> None: """Change the group of a palette.""" if self.selected.readonly: return res = tk_tools.prompt( gettext("BEE2 - Change Palette Group"), gettext( 'Enter the name of the group for this palette, or "" to ungroup.' ), validator=lambda x: x, ) if res is not None: self.selected.group = res.strip('<>') self.selected.save() self.update_state()
def backup_maps(maps): """Copy the given maps to the backup.""" back_zip = BACKUPS['backup_zip'] # type: ZipFile # Allow removing old maps when we overwrite objects map_dict = { p2c.filename: p2c for p2c in BACKUPS['back'] } # You can't remove files from a zip, so we need to create a new one! # Here we'll just add entries into BACKUPS['back']. # Also check for overwriting for p2c in maps: scr_path = p2c.filename + '.jpg' map_path = p2c.filename + '.p2c' if ( map_path in zip_names(back_zip) or scr_path in zip_names(back_zip) ): if not messagebox.askyesno( title='Overwrite File?', message=gettext('This filename is already in the backup.' 'Do you wish to overwrite it? ' '({})').format(p2c.title), parent=window, icon=messagebox.QUESTION, ): continue new_item = p2c.copy() map_dict[p2c.filename] = new_item BACKUPS['back'] = list(map_dict.values()) refresh_back_details()
def add_tabs(): """Add the tabs to the notebook.""" notebook = UI['tabs'] # Save the current tab index so we can restore it after. try: current_tab = notebook.index(notebook.select()) except TclError: # .index() will fail if the voice is empty, current_tab = None # in that case abandon remembering the tab. # Add or remove tabs so only the correct mode is visible. for name, tab in sorted(TABS.items()): notebook.add(tab) # For the special tabs, we use a special image to make # sure they are well-distinguished from the other groups if tab.nb_type is TabTypes.MID: notebook.tab( tab, compound='image', image=img.Handle.builtin('icons/mid_quote', 32, 16).get_tk(), ) if tab.nb_type is TabTypes.RESPONSE: notebook.tab( tab, compound=RIGHT, image=img.Handle.builtin('icons/resp_quote', 16, 16), #Note: 'response' tab name, should be short. text=gettext('Resp')) else: notebook.tab(tab, text=tab.nb_text) if current_tab is not None: notebook.select(current_tab)
def ui_delete_game() -> None: """Delete selected items in the game list.""" game_dir = BACKUPS['game_path'] if game_dir is None: LOGGER.warning('No game selected to delete from?') return game_detail = UI['game_details'] # type: CheckDetails to_delete = [ item.p2c for item in game_detail.items if item.state ] to_keep = [ item.p2c for item in game_detail.items if not item.state ] if not to_delete: return map_count = len(to_delete) if not messagebox.askyesno( gettext('Confirm Deletion'), ngettext( 'Do you wish to delete {} map?\n', 'Do you wish to delete {} maps?\n', map_count, ).format(map_count) + '\n'.join([ '{} ({})'.format(map.title, map.filename) for map in to_delete ]) ): return deleting_loader.set_length('DELETE', len(to_delete)) try: with deleting_loader: for p2c in to_delete: # type: P2C scr_path = p2c.filename + '.jpg' map_path = p2c.filename + '.p2c' abs_scr = os.path.join(game_dir, scr_path) abs_map = os.path.join(game_dir, map_path) try: os.remove(abs_scr) except FileNotFoundError: LOGGER.info('{} not present!', abs_scr) try: os.remove(abs_map) except FileNotFoundError: LOGGER.info('{} not present!', abs_map) BACKUPS['game'] = to_keep except loadScreen.Cancelled: pass refresh_game_details()
def init_application() -> None: """Initialise the standalone application.""" from app import gameMan global window window = TK_ROOT TK_ROOT.title( gettext('BEEMOD {} - Backup / Restore Puzzles').format(utils.BEE_VERSION) ) init() UI['bar'] = bar = tk.Menu(TK_ROOT) window.option_add('*tearOff', False) if utils.MAC: # Name is used to make this the special 'BEE2' menu item file_menu = menus['file'] = tk.Menu(bar, name='apple') else: file_menu = menus['file'] = tk.Menu(bar) file_menu.add_command(label=gettext('New Backup'), command=ui_new_backup) file_menu.add_command(label=gettext('Open Backup'), command=ui_load_backup) file_menu.add_command(label=gettext('Save Backup'), command=ui_save_backup) file_menu.add_command(label=gettext('Save Backup As'), command=ui_save_backup_as) bar.add_cascade(menu=file_menu, label=gettext('File')) game_menu = menus['game'] = tk.Menu(bar) game_menu.add_command(label=gettext('Add Game'), command=gameMan.add_game) game_menu.add_command(label=gettext('Remove Game'), command=gameMan.remove_game) game_menu.add_separator() bar.add_cascade(menu=game_menu, label=gettext('Game')) gameMan.game_menu = game_menu from app import helpMenu # Add the 'Help' menu here too. helpMenu.make_help_menu(bar) window['menu'] = bar window.deiconify() window.update() gameMan.load() ui_new_backup() # UI.py isn't present, so we use this callback gameMan.setgame_callback = load_game gameMan.add_menu_opts(game_menu)
def callback(e): if askokcancel( title='BEE2 - Open URL?', message=gettext( 'Open "{}" in the default browser?').format(url), parent=self, ): webbrowser.open(url)
def event_save_as(self) -> None: """Save the palette with a new name.""" name = tk_tools.prompt(gettext("BEE2 - Save Palette"), gettext("Enter a name:")) if name is None: # Cancelled... return pal = Palette(name, self.get_items()) while pal.uuid in self.palettes: # Should be impossible. pal.uuid = paletteLoader.uuid4() if self.var_save_settings.get(): pal.settings = BEE2_config.get_curr_settings(is_palette=True) pal.save() self.palettes[pal.uuid] = pal self.select_palette(pal.uuid) self.update_state()
def on_hover(slot: dragdrop.Slot[Signage]) -> None: """Show the signage when hovered.""" nonlocal hover_arrow, hover_sign if slot.contents is not None: name_label['text'] = gettext('Signage: {}').format(slot.contents.name) hover_sign = slot.contents hover_arrow = True hover_toggle() else: on_leave(slot)
def init_application() -> None: """Initialise when standalone.""" global window window = TK_ROOT window.title(gettext('Compiler Options - {}').format(utils.BEE_VERSION)) window.resizable(True, False) make_widgets() TK_ROOT.deiconify()
def __init__( self, parent: tk.Misc, *, tool_frame: tk.Frame, tool_img: str, menu_bar: tk.Menu, tool_col: int=0, title: str='', resize_x: bool=False, resize_y: bool=False, name: str='', ) -> None: self.visible = tk.BooleanVar(parent, True) self.win_name = name self.allow_snap = False self.can_save = False self.parent = parent self.relX = 0 self.relY = 0 self.can_resize_x = resize_x self.can_resize_y = resize_y super().__init__(parent, name='pane_' + name) self.withdraw() # Hide by default self.tool_button = make_tool_button( frame=tool_frame, img=tool_img, command=self._toggle_win, ) self.tool_button.state(('pressed',)) self.tool_button.grid( row=0, column=tool_col, # Contract the spacing to allow the icons to fit. padx=(2 if utils.MAC else (5, 2)), ) tooltip.add_tooltip( self.tool_button, text=gettext('Hide/Show the "{}" window.').format(title)) menu_bar.add_checkbutton( label=title, variable=self.visible, command=self._set_state_from_menu, ) self.transient(master=parent) self.resizable(resize_x, resize_y) self.title(title) tk_tools.set_window_icon(self) self.protocol("WM_DELETE_WINDOW", self.hide_win) parent.bind('<Configure>', self.follow_main, add=True) self.bind('<Configure>', self.snap_win) self.bind('<FocusIn>', self.enable_snap)
def set_expanded() -> None: """Configure for the expanded state.""" global is_collapsed is_collapsed = False GEN_OPTS['Last_Selected']['music_collapsed'] = '0' base_lbl['text'] = gettext('Base: ') toggle_btn_exit() for wid in exp_widgets: wid.grid() pane.update_idletasks() pane.move()
def ui_new_backup() -> None: """Create a new backup file.""" BACKUPS['back'].clear() BACKUPS['backup_name'] = None BACKUPS['backup_path'] = None backup_name.set(gettext('Unsaved Backup')) BACKUPS['unsaved_file'] = unsaved = BytesIO() BACKUPS['backup_zip'] = ZipFile( unsaved, mode='w', compression=ZIP_LZMA, )
def event_remove(self) -> None: """Remove the currently selected palette.""" pal = self.selected if not pal.readonly and messagebox.askyesno( title='BEE2', message=gettext( 'Are you sure you want to delete "{}"?').format(pal.name), parent=TK_ROOT, ): pal.delete_from_disk() del self.palettes[pal.uuid] self.select_palette(paletteLoader.UUID_PORTAL2) self.set_items(self.selected)
def open_win(e) -> None: """Display the color selection window.""" widget_sfx() r, g, b = parse_color(var.get()) new_color, tk_color = askcolor( color=(r, g, b), parent=parent.winfo_toplevel(), title=gettext('Choose a Color'), ) if new_color is not None: r, g, b = map(int, new_color) # Returned as floats, which is wrong. var.set('{} {} {}'.format(int(r), int(g), int(b)))
def set_collapsed() -> None: """Configure for the collapsed state.""" global is_collapsed is_collapsed = True GEN_OPTS['Last_Selected']['music_collapsed'] = '1' base_lbl['text'] = gettext('Music: ') toggle_btn_exit() # Set all music to the children - so those are used. set_suggested(WINDOWS[MusicChannel.BASE].chosen_id) for wid in exp_widgets: wid.grid_remove()
def clear_caches() -> None: """Wipe the cache times in configs. This will force package resources to be extracted again. """ import packages message = gettext('Package cache times have been reset. ' 'These will now be extracted during the next export.') for game in gameMan.all_games: game.mod_times.clear() game.save() GEN_OPTS['General']['cache_time'] = '0' for pack_id in packages.packages: packages.PACK_CONFIG[pack_id]['ModTime'] = '0' # This needs to be disabled, since otherwise we won't actually export # anything... if PRESERVE_RESOURCES.get(): PRESERVE_RESOURCES.set(False) message += '\n\n' + gettext( '"Preserve Game Resources" has been disabled.') save() # Save any option changes.. gameMan.CONFIG.save_check() GEN_OPTS.save_check() packages.PACK_CONFIG.save_check() # Since we've saved, dismiss this window. win.withdraw() messagebox.showinfo( title=gettext('Packages Reset'), message=message, )
def make_desc(var: StyleVar) -> str: """Generate the description text for a StyleVar. This adds 'Default: on/off', and which styles it's used in. """ if var.desc: desc = [var.desc, ''] else: desc = [] # i18n: StyleVar default value. desc.append( gettext('Default: On') if var.default else gettext('Default: Off')) if var.styles is None: # i18n: StyleVar which is totally unstyled. desc.append(gettext('Styles: Unstyled')) else: app_styles = [ style for style in STYLES.values() if var.applies_to_style(style) ] if len(app_styles) == len(STYLES): # i18n: StyleVar which matches all styles. desc.append(gettext('Styles: All')) else: style_list = sorted(style.selitem_data.short_name for style in app_styles) desc.append( ngettext( # i18n: The styles a StyleVar is allowed for. 'Style: {}', 'Styles: {}', len(style_list), ).format(', '.join(style_list))) return '\n'.join(desc)
def make_pane(tool_frame: tk.Frame, menu_bar: tk.Menu) -> None: """Initialise when part of the BEE2.""" global window window = SubPane.SubPane( TK_ROOT, title=gettext('Compile Options'), name='compiler', menu_bar=menu_bar, resize_x=True, resize_y=False, tool_frame=tool_frame, tool_img='icons/win_compiler', tool_col=4, ) window.columnconfigure(0, weight=1) window.rowconfigure(0, weight=1) make_widgets()
def set_version_combobox(box: ttk.Combobox, item: 'UI.Item') -> list: """Set values on the variant combobox. This is in a function so itemconfig can reuse it. It returns a list of IDs in the same order as the names. """ ver_lookup, version_names = item.get_version_names() if len(version_names) <= 1: # There aren't any alternates to choose from, disable the box box.state(['disabled']) box['values'] = [gettext('No Alternate Versions')] box.current(0) else: box.state(['!disabled']) box['values'] = version_names box.current(ver_lookup.index(item.selected_ver)) return ver_lookup