diff --git a/src/app/contextWin.py b/src/app/contextWin.py index 9e7a94cf..91b2199e 100644 --- a/src/app/contextWin.py +++ b/src/app/contextWin.py @@ -110,6 +110,10 @@ class SPR(Enum): 'This fizzler has an output. Due to an editor bug, this cannot be used directly. Instead ' 'the Fizzler Output Relay item should be placed on top of this fizzler.' ) +TRANS_URL_FAIL = TransToken.ui( + 'Failed to open a web browser. Do you wish for the URL ' + 'to be copied to the clipboard instead?' +) TRANS_TOOL_FIZZOUT_TIMED = TransToken.ui( 'This fizzler has a timed output. Due to an editor bug, this cannot be used directly. Instead ' 'the Fizzler Output Relay item should be placed on top of this fizzler.' @@ -120,6 +124,11 @@ class SPR(Enum): 'limits this to 2048 in total. This provides a guide to how many of ' 'these items can be placed in a map at once.' ) +TRANS_MISSING_ITEM = TransToken.ui( + 'The item <{id}> is missing from package definitions. Check for missing packages. ' + 'Alternatively, this item may have been replaced or merged into another.\n\n' + 'Export is not possible while this is present on the palette.' +) def pos_for_item(item: Item, ind: int) -> int | None: @@ -175,6 +184,8 @@ class ContextWinBase: current_style: AsyncValue[packages.PakRef[packages.Style]] # If set, the item properties window is open and suppressing us. props_open: bool + # If set, a special warning is visible for items that are not defined. + missing_item_visible: bool moreinfo_url: AsyncValue[str | None] moreinfo_trigger: EdgeTrigger[str] @@ -193,6 +204,7 @@ def __init__( self.picker = item_picker self.current_style = current_style self.props_open = False + self.missing_item_visible = False self.packset = packages.PackagesSet.blank() # The current URL in the more-info button, if available. @@ -404,12 +416,16 @@ def load_item_data(self) -> None: blurb = ('Output force-enabled!\n' + blurb).strip() self.ui_set_sprite_tool(SPR.OUTPUT, TransToken.untranslated(blurb)) - def hide_context(self, e: object = None) -> None: + def hide_context(self, _: object = None, /) -> None: """Hide the properties window, if it's open.""" if self.is_visible: self.ui_hide_window() sound.fx('contract') self.selected = self.selected_slot = self.selected_pal_pos = None + if self.missing_item_visible: + self.ui_hide_missing_win() + sound.fx('contract') + self.missing_item_visible = False def show_prop( self, @@ -424,25 +440,35 @@ def show_prop( it stays on top of the selected subitem. - If from the palette, pal_pos is the position. """ + self.ui_hide_missing_win() if warp_cursor and self.is_visible: offset = self.ui_get_cursor_offset() else: offset = None - self.selected = slot.contents - if self.selected is None: + selected = slot.contents + if selected is None: LOGGER.warning('Selected empty slot?') self.hide_context() return + + x, y = slot.get_coords() + # Check to see if it's actually a valid item too. - if self.selected.item.resolve(self.packset) is None: - LOGGER.info('Item not defined, nothing to show.') + if selected.item.resolve(self.packset) is None: + LOGGER.info('Item {} not defined!', selected.item) self.hide_context() + self.missing_item_visible = True + self.ui_show_missing_win( + x, y, + TRANS_MISSING_ITEM.format(id=selected.item), + ) return + self.selected = selected self.selected_slot = slot self.selected_pal_pos = pal_pos - x, y = slot.get_coords() + sound.fx('expand') self.ui_show_window(x, y) self.adjust_position() @@ -460,10 +486,7 @@ async def _moreinfo_task(self) -> None: except webbrowser.Error: if await self.dialog.ask_yes_no( title=TransToken.ui("BEE2 - Error"), - message=TransToken.ui( - 'Failed to open a web browser. Do you wish for the URL ' - 'to be copied to the clipboard instead?' - ), + message=TRANS_URL_FAIL, icon=self.dialog.ERROR, detail=f'"{url}"', ): @@ -479,7 +502,7 @@ def set_sprite(self, pos: SPR, sprite: str) -> None: self.ui_set_sprite_img(pos, img.Handle.sprite('icons/' + sprite, 32, 32)) self.ui_set_sprite_tool(pos, SPRITE_TOOL[sprite]) - def sub_sel(self, pos: int, e: object = None) -> None: + def sub_sel(self, pos: int, _: object = None, /) -> None: """Change the currently-selected sub-item.""" if self.selected is None or self.selected_slot is None: return @@ -495,7 +518,7 @@ def sub_sel(self, pos: int, e: object = None) -> None: # Redisplay the window to refresh data and move it to match self.show_prop(self.selected_slot, self.selected_pal_pos, warp_cursor=True) - def sub_open(self, pos: int, e: object = None) -> None: + def sub_open(self, pos: int, _: object = None, /) -> None: """Move the context window to apply to the given item.""" assert self.selected is not None item = self.selected.item.resolve(packages.get_loaded_packages()) @@ -510,7 +533,7 @@ def sub_open(self, pos: int, e: object = None) -> None: if slot is not None: self.show_prop(slot, pal_pos) - def adjust_position(self, e: object = None) -> None: + def adjust_position(self, _: object = None, /) -> None: """Move the properties window onto the selected item. We call this constantly, so the property window will not go outside @@ -573,6 +596,14 @@ def ui_show_window(self, x: int, y: int) -> None: """Show the window, at the specified position.""" raise NotImplementedError + def ui_show_missing_win(self, x: int, y: int, text: TransToken) -> None: + """Show an error window identifying missing items.""" + raise NotImplementedError + + def ui_hide_missing_win(self) -> None: + """Hide the missing-item window.""" + raise NotImplementedError + def ui_get_cursor_offset(self) -> tuple[int, int]: """Fetch the offset of the cursor relative to the window, for restoring when it moves.""" raise NotImplementedError diff --git a/src/app/item_picker.py b/src/app/item_picker.py index 0eb49f6f..6b4883f1 100644 --- a/src/app/item_picker.py +++ b/src/app/item_picker.py @@ -2,7 +2,7 @@ from typing import Final from abc import ABC, abstractmethod -from collections.abc import Callable, Mapping +from collections.abc import Awaitable, Callable, Mapping from contextlib import aclosing import random @@ -203,7 +203,6 @@ async def open_contextwin_task( while True: slot = await self.drag_man.on_config.wait() if slot.contents is not None: - sound.fx('expand') open_func(slot, self.slots_pal.get(slot)) def _drag_info(self, ref: SubItemRef) -> DragInfo: diff --git a/src/ui_tk/context_win.py b/src/ui_tk/context_win.py index 69888f45..da603449 100644 --- a/src/ui_tk/context_win.py +++ b/src/ui_tk/context_win.py @@ -191,6 +191,20 @@ def __init__( # When the main window moves, move the context window also. TK_ROOT.bind("", self.adjust_position, add='+') + self.missing_item_win = tk.Toplevel(TK_ROOT, name='contextWin_missing') + self.missing_item_win.overrideredirect(True) + self.missing_item_win.resizable(False, False) + self.missing_item_win.transient(master=TK_ROOT) + if utils.LINUX: + self.missing_item_win.wm_attributes('-type', 'popup_menu') + self.missing_item_win.withdraw() # starts hidden + + missing_win_frame = ttk.Frame(self.missing_item_win, relief="raised", borderwidth="4") + missing_win_frame.grid() + self.wid_missing_lbl = ttk.Label(missing_win_frame) + self.wid_missing_lbl.grid() + self.wid_missing_lbl['wraplength'] = 300 + def _evt_moreinfo_clicked(self) -> None: """Handle the more-info button being clicked.""" url = self.moreinfo_url.value @@ -286,10 +300,29 @@ def ui_hide_window(self) -> None: @override def ui_show_window(self, x: int, y: int) -> None: """Show the window.""" - loc_x, loc_y = tk_tools.adjust_inside_screen(x=x, y=y, win=self.window) + x, y = tk_tools.adjust_inside_screen(x, y, self.window) self.window.deiconify() self.window.lift() - self.window.geometry(f'+{loc_x!s}+{loc_y!s}') + self.window.geometry(f'+{x!s}+{y!s}') + + @override + def ui_show_missing_win(self, x: int, y: int, label: TransToken) -> None: + """Show the missing-item window.""" + set_text(self.wid_missing_lbl, label) + self.missing_item_win.deiconify() + self.missing_item_win.lift() + self.missing_item_win.update_idletasks() + self.missing_item_win.bell() + + x += (IMG_ALPHA.width - self.missing_item_win.winfo_width()) // 2 + y += IMG_ALPHA.height // 4 + x, y = tk_tools.adjust_inside_screen(x, y, self.missing_item_win) + self.missing_item_win.geometry(f'+{x!s}+{y!s}') + + @override + def ui_hide_missing_win(self) -> None: + """Hide the missing-item window.""" + self.missing_item_win.withdraw() @override def ui_get_cursor_offset(self) -> tuple[int, int]: