"""UNO dialogs for the Silo Calc extension. Provides login, settings, push summary, and PN conflict resolution dialogs. All dialogs use the UNO dialog toolkit (``com.sun.star.awt``). """ from typing import Any, Dict, List, Optional, Tuple # UNO imports are only available inside LibreOffice try: import uno _HAS_UNO = True except ImportError: _HAS_UNO = False from . import settings as _settings from .client import SiloClient def _get_desktop(): """Return the XSCRIPTCONTEXT desktop, or resolve via component context.""" ctx = uno.getComponentContext() smgr = ctx.ServiceManager return smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) def _msgbox(parent, title: str, message: str, box_type="infobox"): """Show a simple message box.""" if not _HAS_UNO: return ctx = uno.getComponentContext() smgr = ctx.ServiceManager toolkit = smgr.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx) if parent is None: parent = _get_desktop().getCurrentFrame().getContainerWindow() mbt = uno.Enum( "com.sun.star.awt.MessageBoxType", "INFOBOX" if box_type == "infobox" else "ERRORBOX", ) msg_box = toolkit.createMessageBox(parent, mbt, 1, title, message) msg_box.execute() def _input_box( title: str, label: str, default: str = "", password: bool = False ) -> Optional[str]: """Show a simple single-field input dialog. Returns None on cancel.""" if not _HAS_UNO: return None ctx = uno.getComponentContext() smgr = ctx.ServiceManager dlg_provider = smgr.createInstanceWithContext( "com.sun.star.awt.DialogProvider", ctx ) # Build dialog model programmatically dlg_model = smgr.createInstanceWithContext( "com.sun.star.awt.UnoControlDialogModel", ctx ) dlg_model.Width = 220 dlg_model.Height = 80 dlg_model.Title = title # Label lbl = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel") lbl.Name = "lbl" lbl.PositionX = 10 lbl.PositionY = 10 lbl.Width = 200 lbl.Height = 12 lbl.Label = label dlg_model.insertByName("lbl", lbl) # Text field tf = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel") tf.Name = "tf" tf.PositionX = 10 tf.PositionY = 24 tf.Width = 200 tf.Height = 14 tf.Text = default if password: tf.EchoChar = ord("*") dlg_model.insertByName("tf", tf) # OK button btn_ok = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel") btn_ok.Name = "btn_ok" btn_ok.PositionX = 110 btn_ok.PositionY = 50 btn_ok.Width = 45 btn_ok.Height = 16 btn_ok.Label = "OK" btn_ok.PushButtonType = 1 # OK dlg_model.insertByName("btn_ok", btn_ok) # Cancel button btn_cancel = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel") btn_cancel.Name = "btn_cancel" btn_cancel.PositionX = 160 btn_cancel.PositionY = 50 btn_cancel.Width = 45 btn_cancel.Height = 16 btn_cancel.Label = "Cancel" btn_cancel.PushButtonType = 2 # CANCEL dlg_model.insertByName("btn_cancel", btn_cancel) # Create dialog control dlg = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", ctx) dlg.setModel(dlg_model) dlg.setVisible(False) toolkit = smgr.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx) dlg.createPeer(toolkit, None) result = dlg.execute() if result == 1: # OK text = dlg.getControl("tf").getText() dlg.dispose() return text dlg.dispose() return None # --------------------------------------------------------------------------- # Login dialog # --------------------------------------------------------------------------- def show_login_dialog(parent=None) -> bool: """Two-step login: username then password. Returns True on success.""" username = _input_box("Silo Login", "Username:") if not username: return False password = _input_box("Silo Login", f"Password for {username}:", password=True) if not password: return False client = SiloClient() try: result = client.login(username, password) _msgbox( parent, "Silo Login", f"Logged in as {result['username']} ({result.get('role', 'viewer')})", ) return True except RuntimeError as e: _msgbox(parent, "Silo Login Failed", str(e), box_type="errorbox") return False # --------------------------------------------------------------------------- # Settings dialog # --------------------------------------------------------------------------- def show_settings_dialog(parent=None) -> bool: """Show the settings dialog. Returns True if saved.""" if not _HAS_UNO: return False ctx = uno.getComponentContext() smgr = ctx.ServiceManager cfg = _settings.load() dlg_model = smgr.createInstanceWithContext( "com.sun.star.awt.UnoControlDialogModel", ctx ) dlg_model.Width = 300 dlg_model.Height = 200 dlg_model.Title = "Silo Settings" fields = [ ("API URL", "api_url", cfg.get("api_url", "")), ("API Token", "api_token", cfg.get("api_token", "")), ("SSL Cert Path", "ssl_cert_path", cfg.get("ssl_cert_path", "")), ("Projects Dir", "projects_dir", cfg.get("projects_dir", "")), ("Default Schema", "default_schema", cfg.get("default_schema", "kindred-rd")), ] y = 10 for label_text, name, default in fields: lbl = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel") lbl.Name = f"lbl_{name}" lbl.PositionX = 10 lbl.PositionY = y lbl.Width = 80 lbl.Height = 12 lbl.Label = label_text dlg_model.insertByName(f"lbl_{name}", lbl) tf = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel") tf.Name = f"tf_{name}" tf.PositionX = 95 tf.PositionY = y tf.Width = 195 tf.Height = 14 tf.Text = default dlg_model.insertByName(f"tf_{name}", tf) y += 22 # SSL verify checkbox cb = dlg_model.createInstance("com.sun.star.awt.UnoControlCheckBoxModel") cb.Name = "cb_ssl_verify" cb.PositionX = 95 cb.PositionY = y cb.Width = 120 cb.Height = 14 cb.Label = "Verify SSL" cb.State = 1 if cfg.get("ssl_verify", True) else 0 dlg_model.insertByName("cb_ssl_verify", cb) y += 22 # --- OpenRouter AI section --- lbl_ai = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel") lbl_ai.Name = "lbl_ai_section" lbl_ai.PositionX = 10 lbl_ai.PositionY = y lbl_ai.Width = 280 lbl_ai.Height = 12 lbl_ai.Label = "--- OpenRouter AI ---" dlg_model.insertByName("lbl_ai_section", lbl_ai) y += 16 # API Key (masked) lbl_key = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel") lbl_key.Name = "lbl_openrouter_api_key" lbl_key.PositionX = 10 lbl_key.PositionY = y lbl_key.Width = 80 lbl_key.Height = 12 lbl_key.Label = "API Key" dlg_model.insertByName("lbl_openrouter_api_key", lbl_key) tf_key = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel") tf_key.Name = "tf_openrouter_api_key" tf_key.PositionX = 95 tf_key.PositionY = y tf_key.Width = 195 tf_key.Height = 14 tf_key.Text = cfg.get("openrouter_api_key", "") tf_key.EchoChar = ord("*") dlg_model.insertByName("tf_openrouter_api_key", tf_key) y += 22 # AI Model lbl_model = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel") lbl_model.Name = "lbl_openrouter_model" lbl_model.PositionX = 10 lbl_model.PositionY = y lbl_model.Width = 80 lbl_model.Height = 12 lbl_model.Label = "AI Model" dlg_model.insertByName("lbl_openrouter_model", lbl_model) tf_model = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel") tf_model.Name = "tf_openrouter_model" tf_model.PositionX = 95 tf_model.PositionY = y tf_model.Width = 195 tf_model.Height = 14 tf_model.Text = cfg.get("openrouter_model", "") tf_model.HelpText = "openai/gpt-4.1-nano" dlg_model.insertByName("tf_openrouter_model", tf_model) y += 22 # AI Instructions (multi-line) lbl_instr = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel") lbl_instr.Name = "lbl_openrouter_instructions" lbl_instr.PositionX = 10 lbl_instr.PositionY = y lbl_instr.Width = 80 lbl_instr.Height = 12 lbl_instr.Label = "AI Instructions" dlg_model.insertByName("lbl_openrouter_instructions", lbl_instr) tf_instr = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel") tf_instr.Name = "tf_openrouter_instructions" tf_instr.PositionX = 95 tf_instr.PositionY = y tf_instr.Width = 195 tf_instr.Height = 56 tf_instr.Text = cfg.get("openrouter_instructions", "") tf_instr.MultiLine = True tf_instr.VScroll = True tf_instr.HelpText = "Custom system prompt (leave blank for default)" dlg_model.insertByName("tf_openrouter_instructions", tf_instr) y += 62 # Test connection button btn_test = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel") btn_test.Name = "btn_test" btn_test.PositionX = 10 btn_test.PositionY = y btn_test.Width = 80 btn_test.Height = 16 btn_test.Label = "Test Connection" dlg_model.insertByName("btn_test", btn_test) # Status label lbl_status = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel") lbl_status.Name = "lbl_status" lbl_status.PositionX = 95 lbl_status.PositionY = y + 2 lbl_status.Width = 195 lbl_status.Height = 12 lbl_status.Label = "" dlg_model.insertByName("lbl_status", lbl_status) y += 22 # OK / Cancel btn_ok = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel") btn_ok.Name = "btn_ok" btn_ok.PositionX = 190 btn_ok.PositionY = y btn_ok.Width = 45 btn_ok.Height = 16 btn_ok.Label = "Save" btn_ok.PushButtonType = 1 dlg_model.insertByName("btn_ok", btn_ok) btn_cancel = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel") btn_cancel.Name = "btn_cancel" btn_cancel.PositionX = 240 btn_cancel.PositionY = y btn_cancel.Width = 45 btn_cancel.Height = 16 btn_cancel.Label = "Cancel" btn_cancel.PushButtonType = 2 dlg_model.insertByName("btn_cancel", btn_cancel) dlg_model.Height = y + 26 dlg = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", ctx) dlg.setModel(dlg_model) dlg.setVisible(False) toolkit = smgr.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx) dlg.createPeer(toolkit, None) result = dlg.execute() if result == 1: for _, name, _ in fields: cfg[name] = dlg.getControl(f"tf_{name}").getText() cfg["ssl_verify"] = bool(dlg.getControl("cb_ssl_verify").getModel().State) cfg["openrouter_api_key"] = dlg.getControl("tf_openrouter_api_key").getText() cfg["openrouter_model"] = dlg.getControl("tf_openrouter_model").getText() cfg["openrouter_instructions"] = dlg.getControl( "tf_openrouter_instructions" ).getText() _settings.save(cfg) dlg.dispose() return True dlg.dispose() return False # --------------------------------------------------------------------------- # Push summary dialog # --------------------------------------------------------------------------- def show_push_summary( new_count: int, modified_count: int, conflict_count: int, unchanged_count: int, parent=None, ) -> bool: """Show push summary and return True if user confirms.""" lines = [ f"New items: {new_count}", f"Modified items: {modified_count}", f"Conflicts: {conflict_count}", f"Unchanged: {unchanged_count}", ] if conflict_count: lines.append("\nConflicts must be resolved before pushing.") msg = "\n".join(lines) if conflict_count: _msgbox(parent, "Silo Push -- Conflicts Found", msg, box_type="errorbox") return False if new_count == 0 and modified_count == 0: _msgbox(parent, "Silo Push", "Nothing to push -- all rows are up to date.") return False # Confirmation -- for now use a simple info box (OK = proceed) _msgbox(parent, "Silo Push", f"Ready to push:\n\n{msg}\n\nProceed?") return True # --------------------------------------------------------------------------- # PN Conflict Resolution dialog # --------------------------------------------------------------------------- # Return values PN_USE_EXISTING = "use_existing" PN_CREATE_NEW = "create_new" PN_CANCEL = "cancel" def show_pn_conflict_dialog( part_number: str, existing_item: Dict[str, Any], parent=None, ) -> str: """Show PN conflict dialog when a manually entered PN already exists. Returns one of: PN_USE_EXISTING, PN_CREATE_NEW, PN_CANCEL. """ if not _HAS_UNO: return PN_CANCEL ctx = uno.getComponentContext() smgr = ctx.ServiceManager dlg_model = smgr.createInstanceWithContext( "com.sun.star.awt.UnoControlDialogModel", ctx ) dlg_model.Width = 320 dlg_model.Height = 220 dlg_model.Title = f"Part Number Conflict: {part_number}" y = 10 info_lines = [ "This part number already exists in Silo:", "", f" Description: {existing_item.get('description', '')}", f" Type: {existing_item.get('item_type', '')}", f" Category: {existing_item.get('part_number', '')[:3]}", f" Sourcing: {existing_item.get('sourcing_type', '')}", f" Cost: ${existing_item.get('standard_cost', 0):.2f}", ] for line in info_lines: lbl = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel") lbl.Name = f"info_{y}" lbl.PositionX = 10 lbl.PositionY = y lbl.Width = 300 lbl.Height = 12 lbl.Label = line dlg_model.insertByName(f"info_{y}", lbl) y += 13 y += 5 # Radio buttons rb_use = dlg_model.createInstance("com.sun.star.awt.UnoControlRadioButtonModel") rb_use.Name = "rb_use" rb_use.PositionX = 20 rb_use.PositionY = y rb_use.Width = 280 rb_use.Height = 14 rb_use.Label = "Use existing item (add to BOM)" rb_use.State = 1 # selected by default dlg_model.insertByName("rb_use", rb_use) y += 18 rb_new = dlg_model.createInstance("com.sun.star.awt.UnoControlRadioButtonModel") rb_new.Name = "rb_new" rb_new.PositionX = 20 rb_new.PositionY = y rb_new.Width = 280 rb_new.Height = 14 rb_new.Label = "Create new item (auto-generate PN)" rb_new.State = 0 dlg_model.insertByName("rb_new", rb_new) y += 18 rb_cancel = dlg_model.createInstance("com.sun.star.awt.UnoControlRadioButtonModel") rb_cancel.Name = "rb_cancel" rb_cancel.PositionX = 20 rb_cancel.PositionY = y rb_cancel.Width = 280 rb_cancel.Height = 14 rb_cancel.Label = "Cancel" rb_cancel.State = 0 dlg_model.insertByName("rb_cancel", rb_cancel) y += 25 # OK button btn_ok = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel") btn_ok.Name = "btn_ok" btn_ok.PositionX = 210 btn_ok.PositionY = y btn_ok.Width = 45 btn_ok.Height = 16 btn_ok.Label = "OK" btn_ok.PushButtonType = 1 dlg_model.insertByName("btn_ok", btn_ok) btn_cancel_btn = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel") btn_cancel_btn.Name = "btn_cancel_btn" btn_cancel_btn.PositionX = 260 btn_cancel_btn.PositionY = y btn_cancel_btn.Width = 45 btn_cancel_btn.Height = 16 btn_cancel_btn.Label = "Cancel" btn_cancel_btn.PushButtonType = 2 dlg_model.insertByName("btn_cancel_btn", btn_cancel_btn) dlg_model.Height = y + 26 dlg = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", ctx) dlg.setModel(dlg_model) dlg.setVisible(False) toolkit = smgr.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx) dlg.createPeer(toolkit, None) result = dlg.execute() if result != 1: dlg.dispose() return PN_CANCEL if dlg.getControl("rb_use").getModel().State: dlg.dispose() return PN_USE_EXISTING if dlg.getControl("rb_new").getModel().State: dlg.dispose() return PN_CREATE_NEW dlg.dispose() return PN_CANCEL # --------------------------------------------------------------------------- # AI Description review dialog # --------------------------------------------------------------------------- def show_ai_description_dialog( seller_description: str, ai_description: str, parent=None ) -> Optional[str]: """Show AI-generated description for review/editing. Side-by-side layout: seller description (read-only) on the left, AI-generated description (editable) on the right. Returns the accepted/edited description text, or None on cancel. """ if not _HAS_UNO: return None ctx = uno.getComponentContext() smgr = ctx.ServiceManager dlg_model = smgr.createInstanceWithContext( "com.sun.star.awt.UnoControlDialogModel", ctx ) dlg_model.Width = 400 dlg_model.Height = 210 dlg_model.Title = "AI Description Review" # Left: Seller Description (read-only) lbl_seller = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel") lbl_seller.Name = "lbl_seller" lbl_seller.PositionX = 10 lbl_seller.PositionY = 8 lbl_seller.Width = 185 lbl_seller.Height = 12 lbl_seller.Label = "Seller Description" dlg_model.insertByName("lbl_seller", lbl_seller) tf_seller = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel") tf_seller.Name = "tf_seller" tf_seller.PositionX = 10 tf_seller.PositionY = 22 tf_seller.Width = 185 tf_seller.Height = 140 tf_seller.Text = seller_description tf_seller.MultiLine = True tf_seller.VScroll = True tf_seller.ReadOnly = True dlg_model.insertByName("tf_seller", tf_seller) # Right: Generated Description (editable) lbl_gen = dlg_model.createInstance("com.sun.star.awt.UnoControlFixedTextModel") lbl_gen.Name = "lbl_gen" lbl_gen.PositionX = 205 lbl_gen.PositionY = 8 lbl_gen.Width = 185 lbl_gen.Height = 12 lbl_gen.Label = "Generated Description (editable)" dlg_model.insertByName("lbl_gen", lbl_gen) tf_gen = dlg_model.createInstance("com.sun.star.awt.UnoControlEditModel") tf_gen.Name = "tf_gen" tf_gen.PositionX = 205 tf_gen.PositionY = 22 tf_gen.Width = 185 tf_gen.Height = 140 tf_gen.Text = ai_description tf_gen.MultiLine = True tf_gen.VScroll = True dlg_model.insertByName("tf_gen", tf_gen) # Accept button btn_ok = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel") btn_ok.Name = "btn_ok" btn_ok.PositionX = 290 btn_ok.PositionY = 175 btn_ok.Width = 50 btn_ok.Height = 18 btn_ok.Label = "Accept" btn_ok.PushButtonType = 1 # OK dlg_model.insertByName("btn_ok", btn_ok) # Cancel button btn_cancel = dlg_model.createInstance("com.sun.star.awt.UnoControlButtonModel") btn_cancel.Name = "btn_cancel" btn_cancel.PositionX = 345 btn_cancel.PositionY = 175 btn_cancel.Width = 45 btn_cancel.Height = 18 btn_cancel.Label = "Cancel" btn_cancel.PushButtonType = 2 # CANCEL dlg_model.insertByName("btn_cancel", btn_cancel) dlg = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", ctx) dlg.setModel(dlg_model) dlg.setVisible(False) toolkit = smgr.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx) dlg.createPeer(toolkit, None) result = dlg.execute() if result == 1: # OK / Accept text = dlg.getControl("tf_gen").getText() dlg.dispose() return text dlg.dispose() return None # --------------------------------------------------------------------------- # Assembly / Project picker dialogs # --------------------------------------------------------------------------- def show_assembly_picker(client: SiloClient, parent=None) -> Optional[str]: """Show a dialog to pick an assembly by PN. Returns the PN or None.""" pn = _input_box("Pull BOM", "Assembly part number (e.g. A01-0003):") return pn if pn and pn.strip() else None def show_project_picker(client: SiloClient, parent=None) -> Optional[str]: """Show a dialog to pick a project code. Returns the code or None.""" try: projects = client.get_projects() except RuntimeError: projects = [] if not projects: code = _input_box("Pull Project", "Project code:") return code if code and code.strip() else None # Build a choice list choices = [f"{p.get('code', '')} - {p.get('name', '')}" for p in projects] # For simplicity, use an input box with hint. A proper list picker # would use a ListBox control, but this is functional for now. hint = "Available: " + ", ".join(p.get("code", "") for p in projects) code = _input_box("Pull Project", f"Project code ({hint}):") return code if code and code.strip() else None