Advertisement
PaffcioStudio

Untitled

May 18th, 2025
538
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 157.96 KB | None | 0 0
  1. import os
  2. import sys
  3. import threading
  4. import traceback
  5. import time
  6. import re
  7. import platform
  8. import json
  9. import subprocess
  10. import tempfile
  11. import shutil # Added for recursive directory deletion
  12. from datetime import datetime
  13.  
  14. # Conditional imports for AI APIs
  15. try:
  16.     import google.generativeai as genai
  17.     HAS_GEMINI = True
  18. except ImportError:
  19.     print("Warning: google-generativeai not found. Gemini API support disabled.")
  20.     HAS_GEMINI = False
  21.     class MockGeminiModel: # Mock class to prevent errors if genai is missing
  22.         def __init__(self, model_name): self.model_name = model_name
  23.         def start_chat(self, history): return MockChatSession()
  24.     class MockChatSession:
  25.         def send_message(self, message, stream=True):
  26.             class MockChunk: text = "Mock Gemini Response (API not available)"
  27.             return [MockChunk()]
  28.     genai = type('genai', (object,), {'GenerativeModel': MockGeminiModel, 'configure': lambda *args, **kwargs: None})()
  29.  
  30. try:
  31.     from mistralai.client import MistralClient
  32.     from mistralai.models.chat_models import ChatMessage
  33.     HAS_MISTRAL = True
  34. except ImportError:
  35.     print("Warning: mistralai not found. Mistral API support disabled.")
  36.     HAS_MISTRAL = False
  37.     class MockMistralClient: # Mock class to prevent errors if mistralai is missing
  38.         def __init__(self, api_key): pass
  39.         def chat(self, model, messages, stream=True):
  40.             class MockChunk:
  41.                 choices = [type('MockChoice', (object,), {'delta': type('MockDelta', (object,), {'content': "Mock Mistral Response (API not available)"})()})()]
  42.             return [MockChunk()]
  43.     ChatMessage = lambda role, content: {'role': role, 'content': content} # Mock ChatMessage
  44.     MistralClient = MockMistralClient
  45.  
  46. from PyQt6.QtWidgets import (
  47.     QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
  48.     QPushButton, QListWidget, QLineEdit, QLabel, QMessageBox,
  49.     QTextEdit, QScrollArea, QSizePolicy,
  50.     QDialog, QDialogButtonBox, QComboBox, QFileDialog,
  51.     QTabWidget, QSplitter, QTreeView,
  52.     QMenu, QStatusBar, QToolBar, QToolButton, QSystemTrayIcon,
  53.     QSpinBox, QCheckBox, QInputDialog, QAbstractItemView
  54. )
  55.  
  56. from PyQt6.QtGui import (
  57.     QIcon, QFontMetrics, QFont, QTextOption, QColor,
  58.     QGuiApplication, QClipboard, QPalette, QBrush,
  59.     QTextCursor, QAction, QDesktopServices, QTextCharFormat,
  60.     QSyntaxHighlighter, QTextDocument, QFileSystemModel, QPainter, QTextFormat
  61. )
  62.  
  63. from PyQt6.QtCore import (
  64.     Qt, QThread, pyqtSignal, QSize, QMutex, QTimer, QObject,
  65.     QRect, QFileInfo, QDir, QStandardPaths, QUrl, QModelIndex
  66. )
  67.  
  68. from PyQt6.QtPrintSupport import QPrintDialog, QPrinter
  69. # Importy Pygments do kolorowania składni
  70.  
  71. from pygments import highlight
  72. from pygments.lexers import get_lexer_by_name, guess_lexer, ClassNotFound
  73. from pygments.formatters import HtmlFormatter
  74. from pygments.util import ClassNotFound as PygmentsClassNotFound
  75. # --- Constants ---
  76.  
  77. SETTINGS_FILE = "./editor_settings.json"
  78. # List of models the application should attempt to use.
  79. # Structure: (API_TYPE, MODEL_IDENTIFIER, DISPLAY_NAME)
  80. # API_TYPE can be "gemini" or "mistral"
  81. # MODEL_IDENTIFIER is the string used by the respective API library
  82. # DISPLAY_NAME is what's shown to the user
  83. AVAILABLE_MODELS_CONFIG = [
  84.    ("gemini", "gemini-1.5-flash-latest", "Gemini 1.5 Flash (Latest)"),
  85.    ("gemini", "gemini-1.5-pro-latest", "Gemini 1.5 Pro (Latest)"),
  86.    ("gemini", "gemini-2.0-flash-thinking-exp-1219", "Gemini 2.0 Flash (Experimental)"),
  87.    ("gemini", "gemini-2.5-flash-preview-04-17", "Gemini 2.5 Flash (Preview)"),
  88.    ("mistral", "codestral-latest", "Codestral (Latest)"),  # Example Codestral model
  89.    ("mistral", "mistral-large-latest", "Mistral Large (Latest)"),
  90.    ("mistral", "mistral-medium", "Mistral Medium"),
  91.    ("mistral", "mistral-small", "Mistral Small"),
  92.    ("mistral", "mistral-tiny", "Mistral Tiny"),
  93. ]
  94.  
  95.  
  96. # Determine which models are actually available based on installed libraries
  97. ACTIVE_MODELS_CONFIG = []
  98. for api_type, identifier, name in AVAILABLE_MODELS_CONFIG:
  99.    if api_type == "gemini" and HAS_GEMINI:
  100.        ACTIVE_MODELS_CONFIG.append((api_type, identifier, name))
  101.    elif api_type == "mistral" and HAS_MISTRAL:
  102.        ACTIVE_MODELS_CONFIG.append((api_type, identifier, name))
  103.  
  104. if not ACTIVE_MODELS_CONFIG:
  105.    # QMessageBox.critical(None, "Błąd API", "Brak dostępnych API. Proszę zainstalować google-generativeai lub mistralai.")
  106.    print("Warning: No AI APIs available. AI features will be disabled.")
  107.    # Fallback to a dummy entry if no APIs are available, to prevent crashes
  108.    ACTIVE_MODELS_CONFIG = [("none", "none", "Brak dostępnych modeli")]
  109.  
  110.  
  111. DEFAULT_MODEL_CONFIG = ACTIVE_MODELS_CONFIG[0] if ACTIVE_MODELS_CONFIG else ("none", "none", "Brak") # Use the first active model as default
  112.  
  113. RECENT_FILES_MAX = 10
  114. DEFAULT_FONT_SIZE = 12
  115. DEFAULT_THEME = "dark"
  116. GEMINI_API_KEY_FILE = "./.api_key" # Keep original Google key file
  117.  
  118. # --- Syntax Highlighter Classes ---
  119. # (PythonHighlighter, CSSHighlighter, HTMLHighlighter, JSHighlighter, GMLHighlighter - copied from your code)
  120. # ... (Paste your Syntax Highlighter classes here) ...
  121. class PythonHighlighter(QSyntaxHighlighter):
  122.    def __init__(self, document):
  123.        super().__init__(document)
  124.        self.highlight_rules = []
  125.  
  126.        keywords = [
  127.            'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del',
  128.            'elif', 'else', 'except', 'False', 'finally', 'for', 'from', 'global',
  129.            'if', 'import', 'in', 'is', 'lambda', 'None', 'nonlocal', 'not', 'or',
  130.            'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield'
  131.        ]
  132.        keyword_format = QTextCharFormat()
  133.        keyword_format.setForeground(QColor("#569CD6"))  # Blue
  134.        keyword_format.setFontWeight(QFont.Weight.Bold)
  135.        self.highlight_rules.extend([(r'\b%s\b' % kw, keyword_format) for kw in keywords])
  136.  
  137.        string_format = QTextCharFormat()
  138.        string_format.setForeground(QColor("#CE9178"))  # Orange
  139.        self.highlight_rules.append((r'"[^"\\]*(\\.[^"\\]*)*"', string_format))
  140.        self.highlight_rules.append((r"'[^'\\]*(\\.[^'\\]*)*'", string_format))
  141.  
  142.        function_format = QTextCharFormat()
  143.        function_format.setForeground(QColor("#DCDCAA"))  # Light yellow
  144.        self.highlight_rules.append((r'\b[A-Za-z_][A-Za-z0-9_]*\s*(?=\()', function_format))
  145.  
  146.        number_format = QTextCharFormat()
  147.        number_format.setForeground(QColor("#B5CEA8"))  # Green
  148.        self.highlight_rules.append((r'\b[0-9]+\b', number_format))
  149.  
  150.        comment_format = QTextCharFormat()
  151.        comment_format.setForeground(QColor("#6A9955"))  # Green
  152.        comment_format.setFontItalic(True)
  153.        self.highlight_rules.append((r'#[^\n]*', comment_format))
  154.  
  155.    def highlightBlock(self, text):
  156.        for pattern, format in self.highlight_rules:
  157.            expression = re.compile(pattern)
  158.            matches = expression.finditer(text)
  159.            for match in matches:
  160.                start = match.start()
  161.                length = match.end() - start
  162.                self.setFormat(start, length, format)
  163.  
  164. class CSSHighlighter(QSyntaxHighlighter):
  165.    def __init__(self, document):
  166.        super().__init__(document)
  167.        self.highlight_rules = []
  168.  
  169.        keywords = ['color', 'font', 'margin', 'padding', 'display', 'position', 'transition']
  170.        keyword_format = QTextCharFormat()
  171.        keyword_format.setForeground(QColor("#ff6ac1"))  # Pinkish
  172.        keyword_format.setFontWeight(QFont.Weight.Bold)
  173.        self.highlight_rules.extend([(r'\b%s\b' % kw, keyword_format) for kw in keywords])
  174.  
  175.        value_format = QTextCharFormat()
  176.        value_format.setForeground(QColor("#ce9178"))  # Orange
  177.        self.highlight_rules.append((r'#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})', value_format))
  178.        self.highlight_rules.append((r'rgb[a]?\([^)]+\)', value_format))
  179.  
  180.        selector_format = QTextCharFormat()
  181.        selector_format.setForeground(QColor("#dcdcAA"))  # Light yellow
  182.        self.highlight_rules.append((r'^\s*[^{]+(?={)', selector_format))
  183.  
  184.        comment_format = QTextCharFormat()
  185.        comment_format.setForeground(QColor("#6A9955"))  # Green
  186.        comment_format.setFontItalic(True)
  187.        self.highlight_rules.append((r'/\*.*?\*/', comment_format), re.DOTALL)
  188.  
  189.    def highlightBlock(self, text):
  190.        for pattern, format in self.highlight_rules:
  191.            expression = re.compile(pattern)
  192.            for match in expression.finditer(text):
  193.                start = match.start()
  194.                length = match.end() - start
  195.                self.setFormat(start, length, format)
  196.  
  197. class HTMLHighlighter(QSyntaxHighlighter):
  198.    def __init__(self, document):
  199.        super().__init__(document)
  200.        self.highlight_rules = []
  201.  
  202.        tag_format = QTextCharFormat()
  203.        tag_format.setForeground(QColor("#569CD6"))  # Blue
  204.        self.highlight_rules.append((r'</?[\w-]+>', tag_format))
  205.  
  206.        attr_format = QTextCharFormat()
  207.        attr_format.setForeground(QColor("#9cdcfe"))  # Light blue
  208.        self.highlight_rules.append((r'[\w-]+(?=\s*=)', attr_format))
  209.  
  210.        value_format = QTextCharFormat()
  211.        value_format.setForeground(QColor("#ce9178"))  # Orange
  212.        self.highlight_rules.append((r'="[^"]*"', value_format))
  213.  
  214.        comment_format = QTextCharFormat()
  215.        comment_format.setForeground(QColor("#6A9955"))  # Green
  216.        comment_format.setFontItalic(True)
  217.        self.highlight_rules.append((r'<!--[\s\S]*?-->', comment_format))
  218.  
  219.    def highlightBlock(self, text):
  220.        for pattern, format in self.highlight_rules:
  221.            expression = re.compile(pattern)
  222.            for match in expression.finditer(text):
  223.                start = match.start()
  224.                length = match.end() - start
  225.                self.setFormat(start, length, format)
  226.  
  227. class JSHighlighter(QSyntaxHighlighter):
  228.    def __init__(self, document):
  229.        super().__init__(document)
  230.        self.highlight_rules = []
  231.  
  232.        keywords = ['var', 'let', 'const', 'function', 'if', 'else', 'return', 'for', 'while']
  233.        keyword_format = QTextCharFormat()
  234.        keyword_format.setForeground(QColor("#c586c0"))  # Purple
  235.        keyword_format.setFontWeight(QFont.Weight.Bold)
  236.        self.highlight_rules.extend([(r'\b%s\b' % kw, keyword_format) for kw in keywords])
  237.  
  238.        string_format = QTextCharFormat()
  239.        string_format.setForeground(QColor("#ce9178"))  # Orange
  240.        self.highlight_rules.append((r'"[^"\\]*(\\.[^"\\]*)*"', string_format))
  241.        self.highlight_rules.append((r"'[^'\\]*(\\.[^'\\]*)*'", string_format))
  242.  
  243.        function_format = QTextCharFormat()
  244.        function_format.setForeground(QColor("#dcdcaa"))  # Light yellow
  245.        self.highlight_rules.append((r'\b[A-Za-z_][A-Za-z0-9_]*\s*(?=\()', function_format))
  246.  
  247.        comment_format = QTextCharFormat()
  248.        comment_format.setForeground(QColor("#6A9955"))  # Green
  249.        comment_format.setFontItalic(True)
  250.        self.highlight_rules.append((r'//[^\n]*', comment_format))
  251.        self.highlight_rules.append((r'/\*[\s\S]*?\*/', comment_format), re.DOTALL)
  252.  
  253.    def highlightBlock(self, text):
  254.        for pattern, format in self.highlight_rules:
  255.            expression = re.compile(pattern)
  256.            for match in expression.finditer(text):
  257.                start = match.start()
  258.                length = match.end() - start
  259.                self.setFormat(start, length, format)
  260.  
  261. class GMLHighlighter(QSyntaxHighlighter):
  262.    def __init__(self, document):
  263.        super().__init__(document)
  264.        self.highlight_rules = []
  265.  
  266.        keywords = ['if', 'else', 'switch', 'case', 'break', 'return', 'var', 'with', 'while']
  267.        keyword_format = QTextCharFormat()
  268.        keyword_format.setForeground(QColor("#c586c0"))  # Purple
  269.        keyword_format.setFontWeight(QFont.Weight.Bold)
  270.        self.highlight_rules.extend([(r'\b%s\b' % kw, keyword_format) for kw in keywords])
  271.  
  272.        var_format = QTextCharFormat()
  273.        var_format.setForeground(QColor("#4ec9b0"))  # Teal
  274.        self.highlight_rules.append((r'_[a-zA-Z][a-zA-Z0-9]*', var_format))
  275.  
  276.        func_format = QTextCharFormat()
  277.        func_format.setForeground(QColor("#dcdcaa"))  # Light yellow
  278.        gml_funcs = ['instance_create', 'ds_list_add', 'draw_text']
  279.        self.highlight_rules.extend([(r'\b%s\b(?=\()', func_format) for func in gml_funcs])
  280.  
  281.        string_format = QTextCharFormat()
  282.        string_format.setForeground(QColor("#ce9178"))  # Orange
  283.        self.highlight_rules.append((r'"[^"\\]*(\\.[^"\\]*)*"', string_format))
  284.  
  285.        comment_format = QTextCharFormat()
  286.        comment_format.setForeground(QColor("#6A9955"))  # Green
  287.        comment_format.setFontItalic(True)
  288.        self.highlight_rules.append((r'//[^\n]*', comment_format))
  289.        self.highlight_rules.append((r'/\*[\s\S]*?\*/', comment_format), re.DOTALL)
  290.  
  291.  
  292.    def highlightBlock(self, text):
  293.        for pattern, format in self.highlight_rules:
  294.            expression = re.compile(pattern)
  295.            for match in expression.finditer(text):
  296.                start = match.start()
  297.                length = match.end() - start
  298.                self.setFormat(start, length, format)
  299. # --- End of Syntax Highlighter Classes ---
  300.  
  301. # --- API Key Loading ---
  302.  
  303. def load_gemini_api_key(filepath=GEMINI_API_KEY_FILE):
  304.    """Reads Gemini API key from a file."""
  305.    if not os.path.exists(filepath):
  306.        # Don't show critical error if file is just missing, allow user to configure in settings
  307.        print(f"Gemini API key file not found: {filepath}. Please add key in settings.")
  308.        return None
  309.    try:
  310.        with open(filepath, "r") as f:
  311.            key = f.read().strip()
  312.            if not key:
  313.                print(f"Gemini API key file is empty: {filepath}. Please add key in settings.")
  314.                return None
  315.            return key
  316.    except Exception as e:
  317.        print(f"Error reading Gemini API key file: {filepath}\nError: {e}")
  318.        # QMessageBox.warning(None, "Błąd odczytu klucza API", f"Nie można odczytać pliku klucza API Google Gemini: {filepath}\nBłąd: {e}")
  319.        return None
  320.  
  321. # Load Gemini key initially, but allow overriding/setting in settings
  322. GEMINI_API_KEY_GLOBAL = load_gemini_api_key()
  323.  
  324. # --- Configure APIs (Initial) ---
  325. # This configuration should happen *after* loading settings in the main window,
  326. # where the Mistral key from settings is also available.
  327. # The current global configuration is okay for checking HAS_GEMINI but actual
  328. # worker instances need potentially updated keys from settings.
  329.  
  330. # --- Settings Persistence ---
  331.  
  332. def load_settings():
  333.    """Loads settings from a JSON file."""
  334.    # Determine default model based on active APIs
  335.    default_model_config = ACTIVE_MODELS_CONFIG[0] if ACTIVE_MODELS_CONFIG else ("none", "none", "Brak")
  336.    default_api_type = default_model_config[0]
  337.    default_model_identifier = default_model_config[1]
  338.  
  339.    default_settings = {
  340.        "api_type": default_api_type, # New field to store active API type
  341.        "model_identifier": default_model_identifier, # New field to store model identifier
  342.        "mistral_api_key": None, # New field for Mistral key
  343.        "recent_files": [],
  344.        "font_size": DEFAULT_FONT_SIZE,
  345.        "theme": DEFAULT_THEME,
  346.        "workspace": "",
  347.        "show_sidebar": True,
  348.        "show_statusbar": True,
  349.        "show_toolbar": True
  350.    }
  351.  
  352.    try:
  353.        if os.path.exists(SETTINGS_FILE):
  354.            with open(SETTINGS_FILE, 'r') as f:
  355.                settings = json.load(f)
  356.                # Handle potential old format or missing new fields
  357.                if "api_type" not in settings or "model_identifier" not in settings:
  358.                    # Attempt to migrate from old "model_name" if it exists
  359.                    old_model_name = settings.get("model_name", "")
  360.                    found_match = False
  361.                    for api_type, identifier, name in ACTIVE_MODELS_CONFIG:
  362.                        if identifier == old_model_name or name == old_model_name: # Check both identifier and display name from old settings
  363.                             settings["api_type"] = api_type
  364.                             settings["model_identifier"] = identifier
  365.                             found_match = True
  366.                             break
  367.                    if not found_match:
  368.                        # Fallback to default if old name not found or no old name
  369.                        settings["api_type"] = default_api_type
  370.                        settings["model_identifier"] = default_model_identifier
  371.                    if "model_name" in settings:
  372.                         del settings["model_name"] # Remove old field
  373.  
  374.                # Add defaults for any other missing keys (including new mistral_api_key)
  375.                for key in default_settings:
  376.                    if key not in settings:
  377.                        settings[key] = default_settings[key]
  378.  
  379.                # Validate loaded model against active configurations
  380.                is_active = any(s[0] == settings.get("api_type") and s[1] == settings.get("model_identifier") for s in ACTIVE_MODELS_CONFIG)
  381.                if not is_active:
  382.                     print(f"Warning: Loaded model config ({settings.get('api_type')}, {settings.get('model_identifier')}) is not active. Falling back to default.")
  383.                     settings["api_type"] = default_api_type
  384.                     settings["model_identifier"] = default_model_identifier
  385.  
  386.  
  387.                return settings
  388.        return default_settings
  389.    except Exception as e:
  390.        print(f"Błąd ładowania ustawień: {e}. Używam ustawień domyślnych.")
  391.        return default_settings
  392.  
  393. def save_settings(settings: dict):
  394.    """Saves settings to a JSON file."""
  395.    try:
  396.        with open(SETTINGS_FILE, 'w') as f:
  397.            json.dump(settings, f, indent=4)
  398.    except Exception as e:
  399.        print(f"Błąd zapisywania ustawień: {e}")
  400.  
  401. # --- API Formatting Helper ---
  402. def format_chat_history(messages: list, api_type: str) -> list:
  403.    """Formats chat history for different API types."""
  404.    formatted_history = []
  405.    for role, content, metadata in messages:
  406.        # Skip assistant placeholder messages and internal error/empty messages
  407.        if not (role == "assistant" and metadata is not None and metadata.get("type") in ["placeholder", "error", "empty_response"]):
  408.            if api_type == "gemini":
  409.                 # Gemini uses "user" and "model" roles
  410.                 formatted_history.append({
  411.                     "role": "user" if role == "user" else "model",
  412.                     "parts": [content] # Gemini uses 'parts' with content
  413.                 })
  414.            elif api_type == "mistral":
  415.                 # Mistral uses "user" and "assistant" roles
  416.                 formatted_history.append(ChatMessage(role='user' if role == 'user' else 'assistant', content=content))
  417.            # Add other API types here if needed
  418.    return formatted_history
  419.  
  420. # --- API Worker Threads ---
  421.  
  422. class GeminiWorker(QThread):
  423.    response_chunk = pyqtSignal(str)
  424.    response_complete = pyqtSignal()
  425.    error = pyqtSignal(str)
  426.  
  427.    def __init__(self, api_key: str, user_message: str, chat_history: list, model_identifier: str, parent=None):
  428.        super().__init__(parent)
  429.        self.api_key = api_key
  430.        self.user_message = user_message
  431.        self.chat_history = chat_history # Raw history from main window
  432.        self.model_identifier = model_identifier
  433.        self._is_running = True
  434.        self._mutex = QMutex()
  435.        print(f"GeminiWorker created for model: {model_identifier}")
  436.  
  437.  
  438.    def stop(self):
  439.        self._mutex.lock()
  440.        try:
  441.            self._is_running = False
  442.        finally:
  443.            self._mutex.unlock()
  444.  
  445.    def run(self):
  446.        if not self.api_key:
  447.            self.error.emit("Klucz API Google Gemini nie został skonfigurowany.")
  448.            return
  449.        if not self.user_message.strip():
  450.            self.error.emit("Proszę podać niepustą wiadomość tekstową.")
  451.            return
  452.  
  453.        try:
  454.            # Format history for Gemini API
  455.            api_history = format_chat_history(self.chat_history, "gemini")
  456.  
  457.            try:
  458.                 # Attempt to get the model instance
  459.                 genai.configure(api_key=self.api_key) # Ensure API key is used in this thread
  460.                 model_instance = genai.GenerativeModel(self.model_identifier)
  461.  
  462.                 # Start chat with history
  463.                 chat = model_instance.start_chat(history=api_history)
  464.  
  465.                 # Send message and get stream
  466.                 response_stream = chat.send_message(self.user_message, stream=True)
  467.  
  468.            except Exception as api_err:
  469.                error_str = str(api_err)
  470.                if "BlockedPromptException" in error_str or ("FinishReason" in error_str and "SAFETY" in error_str):
  471.                     self.error.emit(f"Odpowiedź zablokowana przez filtry bezpieczeństwa.")
  472.                elif "Candidate.content is empty" in error_str:
  473.                     self.error.emit(f"Otrzymano pustą treść z API (możliwe, że zablokowana lub niepowodzenie).")
  474.                elif "returned an invalid response" in error_str or "Could not find model" in error_str or "Invalid model name" in error_str:
  475.                     self.error.emit(f"API Gemini zwróciło nieprawidłową odpowiedź lub model '{self.model_identifier}' nie znaleziono. Proszę sprawdzić ustawienia modelu i klucz API.\nSzczegóły: {api_err}")
  476.                elif "AUTHENTICATION_ERROR" in error_str or "Invalid API key" in error_str:
  477.                     self.error.emit(f"Błąd autoryzacji API Gemini. Proszę sprawdzić klucz API w ustawieniach.")
  478.                else:
  479.                    error_details = f"{type(api_err).__name__}: {api_err}"
  480.                    if hasattr(api_err, 'status_code'):
  481.                         error_details += f" (Status: {api_err.status_code})"
  482.                    self.error.emit(f"Wywołanie API Gemini nie powiodło się:\n{error_details}")
  483.                return
  484.  
  485.            try:
  486.                full_response_text = ""
  487.                # Process the response stream chunk by chunk
  488.                for chunk in response_stream:
  489.                    self._mutex.lock()
  490.                    is_running = self._is_running
  491.                    self._mutex.unlock()
  492.  
  493.                    if not is_running:
  494.                        break
  495.  
  496.                    if not chunk.candidates:
  497.                         continue
  498.  
  499.                    try:
  500.                        # Concatenate text parts from the chunk
  501.                        # Safely access candidates and content
  502.                        text_parts = [part.text for candidate in chunk.candidates for part in candidate.content.parts if part.text]
  503.                        current_chunk = "".join(text_parts)
  504.                    except (AttributeError, IndexError) as e:
  505.                         print(f"Warning: Could not access chunk text: {e}")
  506.                         current_chunk = "" # Handle cases where structure isn't as expected
  507.  
  508.                    if current_chunk:
  509.                        full_response_text += current_chunk
  510.                        self.response_chunk.emit(current_chunk)
  511.  
  512.                self._mutex.lock()
  513.                stopped_manually = not self._is_running
  514.                self._mutex.unlock()
  515.  
  516.                if not stopped_manually:
  517.                    self.response_complete.emit()
  518.  
  519.            except Exception as stream_err:
  520.                 self._mutex.lock()
  521.                 was_stopped = not self._is_running
  522.                 self._mutex.unlock()
  523.  
  524.                 if not was_stopped:
  525.                     error_details = f"{type(stream_err).__name__}: {stream_err}"
  526.                     self.error.emit(f"Błąd podczas strumieniowania odpowiedzi z API Gemini:\n{error_details}")
  527.  
  528.        except Exception as e:
  529.            error_details = f"{type(e).__name__}: {e}"
  530.            self.error.emit(f"Wystąpił nieoczekiwany błąd w wątku roboczym Gemini:\n{error_details}\n{traceback.format_exc()}")
  531.  
  532.  
  533. class MistralWorker(QThread):
  534.    response_chunk = pyqtSignal(str)
  535.    response_complete = pyqtSignal()
  536.    error = pyqtSignal(str)
  537.  
  538.    def __init__(self, api_key: str, user_message: str, chat_history: list, model_identifier: str, parent=None):
  539.        super().__init__(parent)
  540.        self.api_key = api_key
  541.        self.user_message = user_message
  542.        self.chat_history = chat_history # Raw history from main window
  543.        self.model_identifier = model_identifier
  544.        self._is_running = True
  545.        self._mutex = QMutex()
  546.        print(f"MistralWorker created for model: {model_identifier}")
  547.  
  548.    def stop(self):
  549.        self._mutex.lock()
  550.        try:
  551.            self._is_running = False
  552.        finally:
  553.            self._mutex.unlock()
  554.  
  555.    def run(self):
  556.        if not self.api_key:
  557.            self.error.emit("Klucz API Mistral nie został skonfigurowany w ustawieniach.")
  558.            return
  559.        if not self.user_message.strip():
  560.            self.error.emit("Proszę podać niepustą wiadomość tekstową.")
  561.            return
  562.  
  563.        try:
  564.            # Format history for Mistral API
  565.            # Mistral API expects a list of ChatMessage objects or dicts {'role': '...', 'content': '...'}
  566.            # The last message is the current user message, others are history
  567.            api_messages = format_chat_history(self.chat_history, "mistral")
  568.            api_messages.append(ChatMessage(role='user', content=self.user_message))
  569.  
  570.  
  571.            try:
  572.                 client = MistralClient(api_key=self.api_key)
  573.  
  574.                 response_stream = client.chat(
  575.                     model=self.model_identifier,
  576.                     messages=api_messages,
  577.                     stream=True
  578.                 )
  579.  
  580.            except Exception as api_err:
  581.                error_str = str(api_err)
  582.                # Add more specific error handling for Mistral API if needed
  583.                if "authentication_error" in error_str.lower():
  584.                     self.error.emit(f"Błąd autoryzacji API Mistral. Proszę sprawdzić klucz API w ustawieniach.")
  585.                elif "model_not_found" in error_str.lower():
  586.                     self.error.emit(f"Model Mistral '{self.model_identifier}' nie znaleziono lub jest niedostępny dla tego klucza API.")
  587.                else:
  588.                    error_details = f"{type(api_err).__name__}: {api_err}"
  589.                    self.error.emit(f"Wywołanie API Mistral nie powiodło się:\n{error_details}")
  590.                return
  591.  
  592.  
  593.            try:
  594.                full_response_text = ""
  595.                # Process the response stream chunk by chunk
  596.                for chunk in response_stream:
  597.                    self._mutex.lock()
  598.                    is_running = self._is_running
  599.                    self._mutex.unlock()
  600.  
  601.                    if not is_running:
  602.                        break
  603.  
  604.                    # Mistral stream chunk structure: chunk.choices[0].delta.content
  605.                    current_chunk = ""
  606.                    if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content:
  607.                        current_chunk = chunk.choices[0].delta.content
  608.  
  609.                    if current_chunk:
  610.                        full_response_text += current_chunk
  611.                        self.response_chunk.emit(current_chunk)
  612.  
  613.                self._mutex.lock()
  614.                stopped_manually = not self._is_running
  615.                self._mutex.unlock()
  616.  
  617.                if not stopped_manually:
  618.                    self.response_complete.emit()
  619.  
  620.            except Exception as stream_err:
  621.                 self._mutex.lock()
  622.                 was_stopped = not self._is_running
  623.                 self._mutex.unlock()
  624.  
  625.                 if not was_stopped:
  626.                     error_details = f"{type(stream_err).__name__}: {stream_err}"
  627.                     self.error.emit(f"Błąd podczas strumieniowania odpowiedzi z API Mistral:\n{error_details}")
  628.  
  629.        except Exception as e:
  630.            error_details = f"{type(e).__name__}: {e}"
  631.            self.error.emit(f"Wystąpił nieoczekiwany błąd w wątku roboczym Mistral:\n{error_details}\n{traceback.format_exc()}")
  632.  
  633.  
  634. # --- Pygments Helper for Syntax Highlighting ---
  635. # (highlight_code_html and related CSS - copied from your code)
  636. # ... (Paste your Pygments helper functions and CSS here) ...
  637. PYGMENTS_STYLE_NAME = 'dracula'
  638. try:
  639.    PYGMENTS_CSS = HtmlFormatter(style=PYGMENTS_STYLE_NAME, full=False, cssclass='highlight').get_style_defs('.highlight')
  640. except ClassNotFound:
  641.    print(f"Ostrzeżenie: Styl Pygments '{PYGMENTS_STYLE_NAME}' nie znaleziono. Używam 'default'.")
  642.    PYGMENTS_STYLE_NAME = 'default'
  643.    PYGMENTS_CSS = HtmlFormatter(style=PYGMENTS_STYLE_NAME, full=False, cssclass='highlight').get_style_defs('.highlight')
  644.  
  645. CUSTOM_CODE_CSS = f"""
  646. .highlight {{
  647.     padding: 0 !important;
  648.     margin: 0 !important;
  649. }}
  650. .highlight pre {{
  651.     margin: 0 !important;
  652.     padding: 0 !important;
  653.     border: none !important;
  654.     white-space: pre-wrap;
  655.     word-wrap: break-word;
  656. }}
  657. """
  658. FINAL_CODE_CSS = PYGMENTS_CSS + CUSTOM_CODE_CSS
  659.  
  660. def highlight_code_html(code, language=''):
  661.    try:
  662.        if language:
  663.            lexer = get_lexer_by_name(language, stripall=True)
  664.        else:
  665.            lexer = guess_lexer(code)
  666.        if lexer.name == 'text':
  667.            raise PygmentsClassNotFound # Don't use 'text' lexer
  668.    except (PygmentsClassNotFound, ValueError):
  669.        try:
  670.            # Fallback to a generic lexer or plain text
  671.            lexer = get_lexer_by_name('text', stripall=True)
  672.        except PygmentsClassNotFound:
  673.             # This fallback should theoretically always work, but as a safeguard:
  674.             return f"<pre><code>{code}</code></pre>"
  675.  
  676.  
  677.    formatter = HtmlFormatter(style=PYGMENTS_STYLE_NAME, full=False, cssclass='highlight')
  678.    return highlight(code, lexer, formatter)
  679.  
  680. # --- Custom Widgets for Chat Messages ---
  681. # (CodeDisplayTextEdit, MessageWidget - copied from your code)
  682. # ... (Paste your CodeDisplayTextEdit and MessageWidget classes here) ...
  683. class CodeDisplayTextEdit(QTextEdit):
  684.    def __init__(self, parent=None):
  685.        super().__init__(parent)
  686.        self.setReadOnly(True)
  687.        self.setAcceptRichText(True)
  688.        self.setWordWrapMode(QTextOption.WrapMode.NoWrap) # Code blocks shouldn't wrap standardly
  689.        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
  690.        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
  691.        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
  692.        self.setMinimumHeight(QFontMetrics(self.font()).lineSpacing() * 3 + 16)
  693.        self.setFrameStyle(QTextEdit.Shape.Box | QTextEdit.Shadow.Plain)
  694.        self.document().setDocumentMargin(0)
  695.        self.setContentsMargins(0,0,0,0)
  696.  
  697.        self.setStyleSheet(f"""
  698.             QTextEdit {{
  699.                 background-color: #2d2d2d; /* Dark background for code */
  700.                 color: #ffffff; /* White text */
  701.                 border: 1px solid #4a4a4a;
  702.                 border-radius: 5px;
  703.                 padding: 8px;
  704.                 font-family: "Consolas", "Courier New", monospace;
  705.                 font-size: 9pt; /* Smaller font for code blocks */
  706.             }}
  707.             {FINAL_CODE_CSS} /* Pygments CSS for syntax highlighting */
  708.         """)
  709.  
  710.    def setHtml(self, html: str):
  711.        super().setHtml(html)
  712.        self.document().adjustSize()
  713.        doc_height = self.document().size().height()
  714.        buffer = 5
  715.        self.setFixedHeight(int(doc_height) + buffer)
  716.  
  717. class MessageWidget(QWidget):
  718.    def __init__(self, role: str, content: str, metadata: dict = None, parent=None):
  719.        super().__init__(parent)
  720.        self.role = role
  721.        self.content = content
  722.        self.metadata = metadata
  723.        self.is_placeholder = (role == "assistant" and metadata is not None and metadata.get("type") == "placeholder")
  724.        self.segments = []
  725.  
  726.        self.layout = QVBoxLayout(self)
  727.        self.layout.setContentsMargins(0, 5, 0, 5)
  728.        self.layout.setSpacing(3)
  729.  
  730.        bubble_widget = QWidget()
  731.        self.content_layout = QVBoxLayout(bubble_widget)
  732.        self.content_layout.setContentsMargins(12, 8, 12, 8)
  733.        self.content_layout.setSpacing(6)
  734.  
  735.        user_color = "#dcf8c6"
  736.        assistant_color = "#e0e0e0"
  737.  
  738.        bubble_style = f"""
  739.             QWidget {{
  740.                 background-color: {'{user_color}' if role == 'user' else '{assistant_color}'};
  741.                 border-radius: 15px;
  742.                 padding: 0px;
  743.                 border: 1px solid #e0e0e0;
  744.             }}
  745.         """
  746.        if self.is_placeholder:
  747.            bubble_style = """
  748.                 QWidget {
  749.                     background-color: #f0f0f0;
  750.                     border-radius: 15px;
  751.                     padding: 0px;
  752.                     border: 1px dashed #cccccc;
  753.                 }
  754.             """
  755.        bubble_widget.setStyleSheet(bubble_style)
  756.        bubble_widget.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum)
  757.  
  758.        outer_layout = QHBoxLayout()
  759.        outer_layout.setContentsMargins(0, 0, 0, 0)
  760.        outer_layout.setSpacing(0)
  761.  
  762.        screen_geometry = QGuiApplication.primaryScreen().availableGeometry()
  763.        max_bubble_width = int(screen_geometry.width() * 0.75)
  764.        bubble_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
  765.        bubble_widget.setMinimumWidth(1)
  766.  
  767.        spacer_left = QWidget()
  768.        spacer_right = QWidget()
  769.        spacer_left.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
  770.        spacer_right.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
  771.  
  772.        if role == 'user':
  773.            outer_layout.addWidget(spacer_left)
  774.            outer_layout.addWidget(bubble_widget, 1)
  775.            outer_layout.addWidget(spacer_right, 0)
  776.        else:
  777.            outer_layout.addWidget(spacer_left, 0)
  778.            outer_layout.addWidget(bubble_widget, 1)
  779.            outer_layout.addWidget(spacer_right)
  780.  
  781.        self.layout.addLayout(outer_layout)
  782.  
  783.        if self.is_placeholder:
  784.            placeholder_label = QLabel(content)
  785.            placeholder_label.setStyleSheet("QLabel { color: #505050; font-style: italic; padding: 10px; }")
  786.            placeholder_label.setWordWrap(True)
  787.            placeholder_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
  788.            self.content_layout.addWidget(placeholder_label)
  789.            self.placeholder_label = placeholder_label
  790.            placeholder_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
  791.            placeholder_label.setMinimumWidth(1)
  792.        else:
  793.            self.display_content(content, self.content_layout)
  794.  
  795.        self.content_layout.addStretch(1)
  796.  
  797.    def display_content(self, content, layout):
  798.        block_pattern = re.compile(r'(^|\n)(`{3,})(\w*)\n(.*?)\n\2(?:\n|$)', re.DOTALL)
  799.        last_end = 0
  800.  
  801.        for match in block_pattern.finditer(content):
  802.            text_before = content[last_end:match.start()].strip()
  803.            if text_before:
  804.                self.add_text_segment(text_before, layout)
  805.  
  806.            code = match.group(4)
  807.            language = match.group(3).strip()
  808.  
  809.            code_area = CodeDisplayTextEdit()
  810.            highlighted_html = highlight_code_html(code, language)
  811.            code_area.setHtml(highlighted_html)
  812.            layout.addWidget(code_area)
  813.            self.segments.append(code_area)
  814.  
  815.            copy_button = QPushButton("Kopiuj kod")
  816.            copy_button.setIcon(QIcon.fromTheme("edit-copy", QIcon(":/icons/copy.png")))
  817.            copy_button.setFixedSize(100, 25)
  818.            copy_button.setStyleSheet("""
  819.                 QPushButton {
  820.                     background-color: #3c3c3c;
  821.                     color: #ffffff;
  822.                     border: 1px solid #5a5a5a;
  823.                     border-radius: 4px;
  824.                     padding: 2px 8px;
  825.                     font-size: 9pt;
  826.                 }
  827.                 QPushButton:hover {
  828.                     background-color: #4a4a4a;
  829.                     border-color: #6a6a6a;
  830.                 }
  831.                 QPushButton:pressed {
  832.                     background-color: #2a2a2a;
  833.                     border-color: #5a5a5a;
  834.                 }
  835.             """)
  836.  
  837.            clipboard = QApplication.clipboard()
  838.            if clipboard:
  839.                copy_button.clicked.connect(lambda checked=False, code_widget=code_area: self.copy_code_to_clipboard(code_widget))
  840.            else:
  841.                copy_button.setEnabled(False)
  842.  
  843.            btn_layout = QHBoxLayout()
  844.            btn_layout.addStretch()
  845.            btn_layout.addWidget(copy_button)
  846.            btn_layout.setContentsMargins(0, 0, 0, 0)
  847.            btn_layout.setSpacing(0)
  848.            layout.addLayout(btn_layout)
  849.  
  850.            last_end = match.end()
  851.  
  852.        remaining_text = content[last_end:].strip()
  853.        if remaining_text:
  854.            self.add_text_segment(remaining_text, layout)
  855.  
  856.    def add_text_segment(self, text: str, layout: QVBoxLayout):
  857.        if not text:
  858.            return
  859.  
  860.        text_edit = QTextEdit()
  861.        text_edit.setReadOnly(True)
  862.        text_edit.setFrameStyle(QTextEdit.Shape.NoFrame)
  863.        text_edit.setContentsMargins(0, 0, 0, 0)
  864.        text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
  865.        text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
  866.        text_edit.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
  867.        text_edit.setWordWrapMode(QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)
  868.        text_edit.setAcceptRichText(True)
  869.  
  870.        text_edit.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
  871.        text_edit.customContextMenuRequested.connect(lambda pos, te=text_edit: self.show_context_menu(pos, te))
  872.  
  873.        text_edit.setStyleSheet(f"""
  874.             QTextEdit {{
  875.                 background-color: transparent;
  876.                 border: none;
  877.                 padding: 0;
  878.                 font-size: 10pt;
  879.                 color: {'#333333' if self.role == 'user' else '#ffffff'};
  880.             }}
  881.         """)
  882.  
  883.        html_text = text.replace('&', '&').replace('<', '<').replace('>', '>')
  884.        html_text = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', html_text)
  885.        inline_code_style = "font-family: Consolas, 'Courier New', monospace; background-color: #f0f0f0; padding: 1px 3px; border-radius: 3px; font-size: 9pt;"
  886.        html_text = re.sub(r'`([^`]+)`', rf'<span style="{inline_code_style}">\1</span>', html_text)
  887.        html_text = html_text.replace('\n', '<br>')
  888.  
  889.        text_edit.setHtml(html_text)
  890.        self.segments.append(text_edit)
  891.  
  892.        text_edit.document().adjustSize()
  893.        doc_size = text_edit.document().size()
  894.        buffer = 5
  895.        text_edit.setFixedHeight(int(doc_size.height()) + buffer)
  896.  
  897.        layout.addWidget(text_edit)
  898.  
  899.    def show_context_menu(self, position, text_edit):
  900.        menu = QMenu(text_edit)
  901.  
  902.        copy_action = menu.addAction("Kopiuj")
  903.        copy_action.setIcon(QIcon.fromTheme("edit-copy"))
  904.        copy_action.triggered.connect(text_edit.copy)
  905.  
  906.        menu.addSeparator()
  907.  
  908.        select_all_action = menu.addAction("Zaznacz wszystko")
  909.        select_all_action.setIcon(QIcon.fromTheme("edit-select-all"))
  910.        select_all_action.setShortcut("Ctrl+A")
  911.        select_all_action.triggered.connect(text_edit.selectAll)
  912.  
  913.        menu.exec(text_edit.viewport().mapToGlobal(position))
  914.  
  915.    def copy_code_to_clipboard(self, code_widget: CodeDisplayTextEdit):
  916.        clipboard = QApplication.clipboard()
  917.        if clipboard:
  918.            code_text = code_widget.toPlainText()
  919.            clipboard.setText(code_text)
  920.  
  921.            sender_button = self.sender()
  922.            if sender_button:
  923.                original_text = sender_button.text()
  924.                sender_button.setText("Skopiowano!")
  925.                QTimer.singleShot(1500, lambda: sender_button.setText(original_text))
  926.  
  927.    def update_placeholder_text(self, text):
  928.        if self.is_placeholder and hasattr(self, 'placeholder_label'):
  929.            display_text = text.strip()
  930.            if len(display_text) > 200:
  931.                display_text = "..." + display_text[-200:]
  932.            display_text = "⚙️ Przetwarzam... " + display_text
  933.            self.placeholder_label.setText(display_text)
  934.  
  935.    def apply_theme_colors(self, background: QColor, foreground: QColor, bubble_user: QColor, bubble_assistant: QColor):
  936.        bubble_widget = self.findChild(QWidget)
  937.        if bubble_widget and not self.is_placeholder:
  938.            bubble_style = f"""
  939.                 QWidget {{
  940.                     background-color: {'{bubble_user.name()}' if self.role == 'user' else '{bubble_assistant.name()}'};
  941.                     border-radius: 15px;
  942.                     padding: 0px;
  943.                     border: 1px solid #e0e0e0;
  944.                 }}
  945.             """
  946.            bubble_widget.setStyleSheet(bubble_style)
  947.            bubble_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
  948.  
  949.        for segment in self.segments:
  950.            if isinstance(segment, QTextEdit) and not isinstance(segment, CodeDisplayTextEdit):
  951.                segment.setStyleSheet(f"""
  952.                     QTextEdit {{
  953.                         background-color: transparent;
  954.                         border: none;
  955.                         padding: 0;
  956.                         font-size: 10pt;
  957.                         color: {'{foreground.name()}' if self.role == 'assistant' else '#333333'};
  958.                     }}
  959.                 """)
  960. # --- End of Custom Widgets ---
  961.  
  962.  
  963. # --- Code Editor Widget ---
  964. # (CodeEditor - copied and slightly modified for theme colors and line numbers)
  965. # ... (Paste your CodeEditor class here) ...
  966. class CodeEditor(QTextEdit):
  967.    def __init__(self, parent=None):
  968.        super().__init__(parent)
  969.        self.setFont(QFont("Consolas", DEFAULT_FONT_SIZE))
  970.        self.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
  971.        self.highlighter = PythonHighlighter(self.document()) # Default highlighter
  972.  
  973.        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
  974.        self.customContextMenuRequested.connect(self.show_context_menu)
  975.  
  976.        self.line_number_area = QWidget(self)
  977.        self.line_number_area.setFixedWidth(40)
  978.        self.line_number_area.setStyleSheet("background-color: #252526; color: #858585;")
  979.        self.update_line_number_area_width()
  980.  
  981.        self.document().blockCountChanged.connect(self.update_line_number_area_width)
  982.        self.verticalScrollBar().valueChanged.connect(lambda: self.line_number_area.update())
  983.        self.textChanged.connect(lambda: self.line_number_area.update())
  984.        self.cursorPositionChanged.connect(lambda: self.update_line_number_area(self.viewport().rect(), 0))
  985.        self.cursorPositionChanged.connect(self.highlight_current_line)
  986.  
  987.        self.setTabStopDistance(QFontMetrics(self.font()).horizontalAdvance(' ') * 4)
  988.  
  989.        self.current_line_format = QTextCharFormat()
  990.        self.current_line_format.setBackground(QColor("#2d2d2d"))
  991.  
  992.    def delete(self):
  993.        cursor = self.textCursor()
  994.        if cursor.hasSelection():
  995.            cursor.removeSelectedText()
  996.        else:
  997.            cursor.deleteChar()
  998.        self.setTextCursor(cursor)
  999.  
  1000.    def show_context_menu(self, position):
  1001.        # This context menu is now set up by the main window's setup_editor_context_menu
  1002.        # which provides more actions. This local one is potentially redundant or simplified.
  1003.        # For now, let's keep the main window's setup, and this method could be empty or removed.
  1004.        # Or, it could call the main window's method. Let's update the main window setup.
  1005.        pass # The main window will handle this connection instead
  1006.  
  1007.  
  1008.    def update_line_number_area_width(self):
  1009.        digits = len(str(max(1, self.document().blockCount())))
  1010.        space = 10 + self.fontMetrics().horizontalAdvance('9') * digits
  1011.        self.line_number_area.setFixedWidth(space)
  1012.        self.setViewportMargins(self.line_number_area.width(), 0, 0, 0)
  1013.  
  1014.    def update_line_number_area(self, rect, dy):
  1015.        if dy != 0:
  1016.            self.line_number_area.scroll(0, dy)
  1017.        else:
  1018.            self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
  1019.  
  1020.        if rect.contains(self.viewport().rect()):
  1021.            self.update_line_number_area_width()
  1022.  
  1023.    def resizeEvent(self, event):
  1024.        super().resizeEvent(event)
  1025.        cr = self.contentsRect()
  1026.        self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area.width(), cr.height()))
  1027.  
  1028.    def line_number_area_paint_event(self, event):
  1029.        painter = QPainter(self.line_number_area)
  1030.        if not painter.isActive():
  1031.            painter.begin(self.line_number_area)
  1032.  
  1033.        bg_color = self.palette().color(QPalette.ColorRole.Base)
  1034.        painter.fillRect(event.rect(), bg_color)
  1035.  
  1036.        doc = self.document()
  1037.        block = doc.begin()
  1038.        block_number = 0
  1039.        scroll_offset = self.verticalScrollBar().value()
  1040.  
  1041.        top = block.layout().boundingRect().top() + scroll_offset
  1042.        bottom = top + block.layout().boundingRect().height()
  1043.  
  1044.        while block.isValid() and top <= event.rect().bottom():
  1045.            if block.isVisible() and bottom >= event.rect().top():
  1046.                number = str(block_number + 1)
  1047.                # Use line number area's stylesheet color for text
  1048.                # Accessing stylesheet color directly is tricky, fallback to palette or fixed color
  1049.                # Let's use the foreground color set in set_theme_colors
  1050.                painter.setPen(self.line_number_area.palette().color(QPalette.ColorRole.WindowText))
  1051.  
  1052.  
  1053.                painter.drawText(0, int(top), self.line_number_area.width() - 5, self.fontMetrics().height(),
  1054.                                 Qt.AlignmentFlag.AlignRight, number)
  1055.  
  1056.            block = block.next()
  1057.            if block.isValid():
  1058.                top = bottom
  1059.                # Recalculate block height each time
  1060.                bottom = top + self.blockBoundingRect(block).height() # Use blockBoundingRect for accurate height
  1061.                block_number += 1
  1062.            else:
  1063.                break
  1064.  
  1065.    def highlight_current_line(self):
  1066.        extra_selections = []
  1067.  
  1068.        if not self.isReadOnly():
  1069.            selection = QTextEdit.ExtraSelection()
  1070.            selection.format = self.current_line_format
  1071.            selection.format.setProperty(QTextFormat.Property.FullWidthSelection, True)
  1072.            selection.cursor = self.textCursor()
  1073.            selection.cursor.clearSelection()
  1074.            extra_selections.append(selection)
  1075.  
  1076.        self.setExtraSelections(extra_selections)
  1077.  
  1078.    def set_font_size(self, size: int):
  1079.        font = self.font()
  1080.        font.setPointSize(size)
  1081.        self.setFont(font)
  1082.        self.setTabStopDistance(QFontMetrics(self.font()).horizontalAdvance(' ') * 4)
  1083.        self.update_line_number_area_width()
  1084.        self.line_number_area.update()
  1085.  
  1086.    def set_theme_colors(self, background: QColor, foreground: QColor, line_number_bg: QColor, line_number_fg: QColor, current_line_bg: QColor):
  1087.        palette = self.palette()
  1088.        palette.setColor(QPalette.ColorRole.Base, background)
  1089.        palette.setColor(QPalette.ColorRole.Text, foreground)
  1090.        self.setPalette(palette)
  1091.  
  1092.        # Update line number area palette and stylesheet for immediate effect
  1093.        linenum_palette = self.line_number_area.palette()
  1094.        linenum_palette.setColor(QPalette.ColorRole.Window, line_number_bg) # Window role for background
  1095.        linenum_palette.setColor(QPalette.ColorRole.WindowText, line_number_fg) # WindowText for foreground
  1096.        self.line_number_area.setPalette(linenum_palette)
  1097.        self.line_number_area.setStyleSheet(f"QWidget {{ background-color: {line_number_bg.name()}; color: {line_number_fg.name()}; }}")
  1098.  
  1099.  
  1100.        self.current_line_format.setBackground(current_line_bg)
  1101.        self.highlight_current_line()
  1102.  
  1103.    def paintEvent(self, event):
  1104.        # Custom paint event to draw the line number area *before* the main editor content
  1105.        # Note: QWidget's paintEvent is not automatically called by QTextEdit's paintEvent
  1106.        # We need to manually trigger the line number area repaint or rely on its own update signals.
  1107.        # The current setup relies on signals connected in __init__.
  1108.        # We can skip the manual paintEvent call here and let the signals handle it.
  1109.        super().paintEvent(event)
  1110. # --- End of Code Editor Widget ---
  1111.  
  1112.  
  1113. # --- Settings Dialog ---
  1114.  
  1115. class SettingsDialog(QDialog):
  1116.    def __init__(self, active_models_config: list, current_settings: dict, parent=None):
  1117.        super().__init__(parent)
  1118.        self.setWindowTitle("Ustawienia")
  1119.        self.setMinimumWidth(400)
  1120.        self.setModal(True)
  1121.  
  1122.        self.active_models_config = active_models_config
  1123.        self.current_settings = current_settings
  1124.  
  1125.        self.layout = QVBoxLayout(self)
  1126.  
  1127.        # Model selection
  1128.        model_label = QLabel("Model AI:")
  1129.        self.layout.addWidget(model_label)
  1130.  
  1131.        self.model_combo = QComboBox()
  1132.        # Populate with display names, but store API type and identifier as user data
  1133.        for api_type, identifier, display_name in self.active_models_config:
  1134.            self.model_combo.addItem(display_name, userData=(api_type, identifier))
  1135.  
  1136.        # Set the current model in the combobox
  1137.        try:
  1138.            current_api_type = self.current_settings.get("api_type")
  1139.            current_identifier = self.current_settings.get("model_identifier")
  1140.            # Find the index for the current model config
  1141.            for i in range(self.model_combo.count()):
  1142.                 api_type, identifier = self.model_combo.itemData(i)
  1143.                 if api_type == current_api_type and identifier == current_identifier:
  1144.                      self.model_combo.setCurrentIndex(i)
  1145.                      break
  1146.            else: # If current model not found, select the first available
  1147.                if self.model_combo.count() > 0:
  1148.                    self.model_combo.setCurrentIndex(0)
  1149.        except Exception as e:
  1150.            print(f"Error setting initial model in settings dialog: {e}")
  1151.            if self.model_combo.count() > 0: self.model_combo.setCurrentIndex(0)
  1152.  
  1153.  
  1154.        self.layout.addWidget(self.model_combo)
  1155.  
  1156.        # API Key Inputs (conditional based on available APIs)
  1157.        if HAS_GEMINI:
  1158.            # Gemini key is usually from file, but could add an input here too if needed
  1159.            pass # Keep Gemini key from file for now
  1160.  
  1161.        if HAS_MISTRAL:
  1162.            mistral_key_label = QLabel("Klucz API Mistral:")
  1163.            self.layout.addWidget(mistral_key_label)
  1164.            self.mistral_key_input = QLineEdit()
  1165.            self.mistral_key_input.setPlaceholderText("Wprowadź klucz API Mistral")
  1166.            self.mistral_key_input.setText(self.current_settings.get("mistral_api_key", ""))
  1167.            self.layout.addWidget(self.mistral_key_input)
  1168.  
  1169.  
  1170.        # Theme selection
  1171.        theme_label = QLabel("Motyw:")
  1172.        self.layout.addWidget(theme_label)
  1173.  
  1174.        self.theme_combo = QComboBox()
  1175.        self.theme_combo.addItems(["ciemny", "jasny"])
  1176.        self.theme_combo.setCurrentText("ciemny" if self.current_settings.get("theme", DEFAULT_THEME) == "dark" else "jasny")
  1177.        self.layout.addWidget(self.theme_combo)
  1178.  
  1179.        # Font size
  1180.        font_label = QLabel("Rozmiar czcionki:")
  1181.        self.layout.addWidget(font_label)
  1182.  
  1183.        self.font_spin = QSpinBox()
  1184.        self.font_spin.setRange(8, 24)
  1185.        self.font_spin.setValue(self.current_settings.get("font_size", DEFAULT_FONT_SIZE))
  1186.        self.layout.addWidget(self.font_spin)
  1187.  
  1188.        # UI elements visibility
  1189.        self.sidebar_check = QCheckBox("Pokaż pasek boczny")
  1190.        self.sidebar_check.setChecked(self.current_settings.get("show_sidebar", True))
  1191.        self.layout.addWidget(self.sidebar_check)
  1192.  
  1193.        self.toolbar_check = QCheckBox("Pokaż pasek narzędzi")
  1194.        self.toolbar_check.setChecked(self.current_settings.get("show_toolbar", True))
  1195.        self.layout.addWidget(self.toolbar_check)
  1196.  
  1197.        self.statusbar_check = QCheckBox("Pokaż pasek stanu")
  1198.        self.statusbar_check.setChecked(self.current_settings.get("show_statusbar", True))
  1199.        self.layout.addWidget(self.statusbar_check)
  1200.  
  1201.        self.layout.addStretch(1)
  1202.  
  1203.        # Standard OK/Cancel buttons
  1204.        self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  1205.        ok_button = self.button_box.button(QDialogButtonBox.StandardButton.Ok)
  1206.        if ok_button: ok_button.setText("Zapisz")
  1207.        cancel_button = self.button_box.button(QDialogButtonBox.StandardButton.Cancel)
  1208.        if cancel_button: cancel_button.setText("Anuluj")
  1209.        self.button_box.accepted.connect(self.accept)
  1210.        self.button_box.rejected.connect(self.reject)
  1211.        self.layout.addWidget(self.button_box)
  1212.  
  1213.    def get_selected_model_config(self) -> tuple:
  1214.        """Returns (api_type, model_identifier) of the selected model."""
  1215.        return self.model_combo.currentData()
  1216.  
  1217.    def get_mistral_api_key(self) -> str:
  1218.        """Returns the Mistral API key from the input field, or None if Mistral is not supported."""
  1219.        if HAS_MISTRAL:
  1220.             return self.mistral_key_input.text().strip() or None
  1221.        return None
  1222.  
  1223.  
  1224.    def get_selected_theme(self) -> str:
  1225.        return "dark" if self.theme_combo.currentText() == "ciemny" else "light"
  1226.  
  1227.    def get_font_size(self) -> int:
  1228.        return self.font_spin.value()
  1229.  
  1230.    def get_ui_visibility(self) -> dict:
  1231.        return {
  1232.            "show_sidebar": self.sidebar_check.isChecked(),
  1233.            "show_toolbar": self.toolbar_check.isChecked(),
  1234.            "show_statusbar": self.statusbar_check.isChecked()
  1235.        }
  1236.  
  1237. # --- File Explorer ---
  1238.  
  1239. class FileExplorer(QTreeView):
  1240.    # Signal emitted when one or more files are selected for opening (e.g., by double-click or context menu)
  1241.    # Emits a list of file paths (only files, not directories)
  1242.    openFilesRequested = pyqtSignal(list)
  1243.    # Signal emitted when one or more items are selected for deletion
  1244.    # Emits a list of file/directory paths
  1245.    deleteItemsRequested = pyqtSignal(list)
  1246.  
  1247.    def __init__(self, parent=None):
  1248.        super().__init__(parent)
  1249.        self.model = QFileSystemModel() # Store model as instance variable
  1250.        self.setModel(self.model)
  1251.  
  1252.        home_dir = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.HomeLocation)[0]
  1253.        self.model.setRootPath(home_dir)
  1254.        self.setRootIndex(self.model.index(home_dir))
  1255.  
  1256.        self.setAnimated(False)
  1257.        self.setIndentation(15)
  1258.        self.setSortingEnabled(True)
  1259.  
  1260.        # Set default sorting: Folders first, then by name, case-insensitive is often preferred
  1261.        self.model.setFilter(QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot | QDir.Filter.Hidden) # Hide . and .., also hidden files/folders
  1262.        # self.model.setSortingFlags(QDir.SortFlag.DirsFirst | QDir.SortFlag.Name | QDir.SortFlag.IgnoreCase | QDir.SortFlag.LocaleAware)
  1263.        # The above line caused the error. QFileSystemModel handles DirsFirst internally when sorting by name.
  1264.        # We can rely on the default sorting behavior combined with sortByColumn.
  1265.        self.sortByColumn(0, Qt.SortOrder.AscendingOrder) # Sort by Name column (0) ascending
  1266.  
  1267.        # Hide columns we don't need
  1268.        for i in range(1, self.model.columnCount()):
  1269.            self.hideColumn(i)
  1270.  
  1271.        # Selection mode: Allows multi-selection with Ctrl/Shift
  1272.        self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
  1273.        # Selection behavior: Select entire rows
  1274.        self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
  1275.  
  1276.        # Connect double-click
  1277.        # Disconnect the default activated behavior that changes root
  1278.        try:
  1279.             self.activated.disconnect(self.on_item_activated)
  1280.        except TypeError: # Handle case where it's not connected yet or connected elsewhere
  1281.             pass
  1282.        # Connect double-click to toggle expansion for directories and open for files
  1283.        self.doubleClicked.connect(self.on_item_double_clicked)
  1284.  
  1285.  
  1286.        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
  1287.        self.customContextMenuRequested.connect(self.show_context_menu)
  1288.  
  1289.        # Internal clipboard for copy/cut operations
  1290.        self._clipboard_paths = []
  1291.        self._is_cut_operation = False
  1292.  
  1293.    def on_item_activated(self, index: QModelIndex):
  1294.        """Handles item activation (e.g., single-click or Enter key)."""
  1295.        # Keep single-click behavior minimal or just selection
  1296.        if not index.isValid():
  1297.             return
  1298.        # Default single-click is usually just selection, which is fine.
  1299.        # The original on_item_activated changed the root on double-click, which is now handled by on_item_double_clicked.
  1300.        # We can leave this method empty or connect it to something else if needed.
  1301.        pass
  1302.  
  1303.    def on_item_double_clicked(self, index: QModelIndex):
  1304.        """Handles item double-click."""
  1305.        if not index.isValid():
  1306.            return
  1307.  
  1308.        file_path = self.model.filePath(index)
  1309.        file_info = QFileInfo(file_path)
  1310.  
  1311.        if file_info.isDir():
  1312.            # Toggle directory expansion instead of changing root
  1313.            self.setExpanded(index, not self.isExpanded(index))
  1314.        else:
  1315.            # If it's a file, emit signal to main window to open it
  1316.            self.openFilesRequested.emit([file_path]) # Emit a list even for a single file
  1317.  
  1318.  
  1319.    def get_selected_paths(self) -> list:
  1320.        """Returns a list of unique file/directory paths for all selected items."""
  1321.        paths = set() # Use a set to ensure uniqueness
  1322.        # Iterate through selected indexes, but only take the first column's index for each row
  1323.        # to avoid duplicates if multiple columns were visible
  1324.        for index in self.selectedIndexes():
  1325.             if index.column() == 0: # Only process the name column index
  1326.                 paths.add(self.model.filePath(index))
  1327.        return list(paths)
  1328.  
  1329.    def show_context_menu(self, position):
  1330.        menu = QMenu()
  1331.        index = self.indexAt(position) # Get index at click position
  1332.        clipboard = QApplication.clipboard() # Get global clipboard
  1333.  
  1334.        # --- Actions based on the clicked item ---
  1335.        if index.isValid():
  1336.            file_path = self.model.filePath(index)
  1337.            file_info = QFileInfo(file_path)
  1338.            selected_paths = self.get_selected_paths() # Get all selected items
  1339.  
  1340.            # --- Actions for the item at the click position ---
  1341.  
  1342.            # New File/Folder actions (only if clicked item is a directory)
  1343.            if file_info.isDir():
  1344.                new_file_action = menu.addAction(QIcon.fromTheme("document-new"), "Nowy plik w tym folderze")
  1345.                new_file_action.triggered.connect(lambda: self.create_new_file(file_path))
  1346.  
  1347.                new_folder_action = menu.addAction(QIcon.fromTheme("folder-new"), "Nowy folder w tym folderze")
  1348.                new_folder_action.triggered.connect(lambda: self.create_new_folder(file_path))
  1349.  
  1350.                menu.addSeparator()
  1351.  
  1352.            # Open action (for both files and directories)
  1353.            open_action = menu.addAction(QIcon.fromTheme("document-open"), "Otwórz")
  1354.            if file_info.isDir():
  1355.                 # For directories, this could either expand/collapse or change root.
  1356.                 # Let's make it change root via context menu for explicit navigation.
  1357.                 open_action.triggered.connect(lambda: self.setRootIndex(index))
  1358.                 # Alternative: open_action.triggered.connect(lambda: self.setExpanded(index, not self.isExpanded(index)))
  1359.            else:
  1360.                 # For files, emit the open signal to the main window
  1361.                 open_action.triggered.connect(lambda: self.openFilesRequested.emit([file_path]))
  1362.  
  1363.            # Copy/Cut actions for the clicked item
  1364.            copy_action = menu.addAction(QIcon.fromTheme("edit-copy"), "Kopiuj")
  1365.            copy_action.triggered.connect(lambda: self.copy_items([file_path]))
  1366.  
  1367.            cut_action = menu.addAction(QIcon.fromTheme("edit-cut"), "Wytnij")
  1368.            cut_action.triggered.connect(lambda: self.cut_items([file_path]))
  1369.  
  1370.  
  1371.            # Paste actions (conditional based on clipboard and clicked item type)
  1372.            if self._clipboard_paths: # Only show paste options if clipboard is not empty
  1373.                 if file_info.isDir():
  1374.                      # Paste into the clicked directory
  1375.                      paste_into_action = menu.addAction(QIcon.fromTheme("edit-paste"), "Wklej do folderu")
  1376.                      paste_into_action.triggered.connect(lambda: self.paste_items(file_path)) # Paste into this folder
  1377.                      # Paste alongside the clicked directory (in its parent)
  1378.                      parent_dir = self.model.filePath(index.parent())
  1379.                      if parent_dir: # Cannot paste alongside the root of the model
  1380.                         paste_alongside_action = menu.addAction(QIcon.fromTheme("edit-paste"), "Wklej obok")
  1381.                         paste_alongside_action.triggered.connect(lambda: self.paste_items(parent_dir)) # Paste into parent folder
  1382.                 else: # Clicked item is a file
  1383.                      # Paste alongside the clicked file (in its parent)
  1384.                      parent_dir = self.model.filePath(index.parent())
  1385.                      if parent_dir: # Cannot paste alongside the root of the model
  1386.                         paste_alongside_action = menu.addAction(QIcon.fromTheme("edit-paste"), "Wklej obok")
  1387.                         paste_alongside_action.triggered.connect(lambda: self.paste_items(parent_dir)) # Paste into parent folder
  1388.  
  1389.  
  1390.            # Rename action for the clicked item
  1391.            rename_action = menu.addAction(QIcon.fromTheme("edit-rename"), "Zmień nazwę")
  1392.            rename_action.triggered.connect(lambda: self.edit(index)) # QTreeView.edit starts renaming
  1393.  
  1394.            # Delete action for the clicked item
  1395.            delete_action = menu.addAction(QIcon.fromTheme("edit-delete"), "Usuń")
  1396.            delete_action.triggered.connect(lambda: self.deleteItemsRequested.emit([file_path])) # Emit list for consistency
  1397.  
  1398.            menu.addSeparator()
  1399.  
  1400.            show_in_explorer_action = menu.addAction(QIcon.fromTheme("system-file-manager"), "Pokaż w menedżerze plików")
  1401.            show_in_explorer_action.triggered.connect(lambda: self.show_in_explorer(file_path))
  1402.  
  1403.        else:
  1404.            # --- Actions for empty space ---
  1405.            root_path = self.model.filePath(self.rootIndex()) # Target actions to the current root directory
  1406.  
  1407.            new_file_action = menu.addAction(QIcon.fromTheme("document-new"), "Nowy plik")
  1408.            new_file_action.triggered.connect(lambda: self.create_new_file(root_path))
  1409.  
  1410.            new_folder_action = menu.addAction(QIcon.fromTheme("folder-new"), "Nowy folder")
  1411.            new_folder_action.triggered.connect(lambda: self.create_new_folder(root_path))
  1412.  
  1413.            menu.addSeparator()
  1414.  
  1415.            # Paste action for empty space (paste into the current root directory)
  1416.            if self._clipboard_paths:
  1417.                 paste_action = menu.addAction(QIcon.fromTheme("edit-paste"), f"Wklej elementy ({len(self._clipboard_paths)})")
  1418.                 paste_action.triggered.connect(lambda: self.paste_items(root_path))
  1419.  
  1420.  
  1421.            select_all_action = menu.addAction(QIcon.fromTheme("edit-select-all"), "Zaznacz wszystko")
  1422.            select_all_action.triggered.connect(self.selectAll)
  1423.  
  1424.  
  1425.        # --- Actions for multiple selected items (if applicable, add them regardless of clicked item if multi-selected) ---
  1426.        # Check if *multiple* items are selected (excluding the single item already handled above)
  1427.        all_selected_paths = self.get_selected_paths()
  1428.        if len(all_selected_paths) > 1:
  1429.             # Avoid adding separator if one was just added
  1430.             if not menu.actions()[-1].isSeparator():
  1431.                  menu.addSeparator()
  1432.  
  1433.             # Filter out directories for "Open Selected Files"
  1434.             selected_files = [p for p in all_selected_paths if QFileInfo(p).isFile()]
  1435.             if selected_files:
  1436.                  open_selected_action = menu.addAction(QIcon.fromTheme("document-open-folder"), f"Otwórz zaznaczone pliki ({len(selected_files)})")
  1437.                  open_selected_action.triggered.connect(lambda: self.openFilesRequested.emit(selected_files))
  1438.  
  1439.             # Copy/Cut for multiple selected items
  1440.             copy_selected_action = menu.addAction(QIcon.fromTheme("edit-copy"), f"Kopiuj zaznaczone elementy ({len(all_selected_paths)})")
  1441.             copy_selected_action.triggered.connect(lambda: self.copy_items(all_selected_paths))
  1442.  
  1443.             cut_selected_action = menu.addAction(QIcon.fromTheme("edit-cut"), f"Wytnij zaznaczone elementy ({len(all_selected_paths)})")
  1444.             cut_selected_action.triggered.connect(lambda: self.cut_items(all_selected_paths))
  1445.  
  1446.             # Delete action for all selected items (files and folders)
  1447.             delete_selected_action = menu.addAction(QIcon.fromTheme("edit-delete"), f"Usuń zaznaczone elementy ({len(all_selected_paths)})")
  1448.             delete_selected_action.triggered.connect(lambda: self.deleteItemsRequested.emit(all_selected_paths)) # Emit list for consistency
  1449.  
  1450.  
  1451.        menu.exec(self.viewport().mapToGlobal(position))
  1452.  
  1453.    def create_new_file(self, dir_path):
  1454.        name, ok = QInputDialog.getText(self, "Nowy plik", "Nazwa pliku:", QLineEdit.EchoMode.Normal, "nowy_plik.txt")
  1455.        if ok and name:
  1456.            file_path = os.path.join(dir_path, name)
  1457.            try:
  1458.                if os.path.exists(file_path):
  1459.                    QMessageBox.warning(self, "Błąd", f"Plik już istnieje: {file_path}")
  1460.                    return
  1461.                # Create the file manually
  1462.                with open(file_path, 'w', encoding='utf-8') as f:
  1463.                    f.write('') # Create an empty file
  1464.                print(f"Utworzono plik: {file_path}")
  1465.                # Refresh the model to show the new file
  1466.                self.model.refresh(self.model.index(dir_path))
  1467.                # Optional: Select and start renaming the new file
  1468.                new_index = self.model.index(file_path)
  1469.                if new_index.isValid():
  1470.                    self.setCurrentIndex(new_index)
  1471.                    self.edit(new_index)
  1472.                self.parent().update_status_bar_message(f"Utworzono nowy plik: {os.path.basename(file_path)}")
  1473.  
  1474.            except Exception as e:
  1475.                QMessageBox.warning(self, "Błąd", f"Nie można utworzyć pliku '{name}':\n{e}")
  1476.                self.parent().update_status_bar_message(f"Błąd tworzenia pliku: {e}")
  1477.  
  1478.  
  1479.    def create_new_folder(self, dir_path):
  1480.        name, ok = QInputDialog.getText(self, "Nowy folder", "Nazwa folderu:", QLineEdit.EchoMode.Normal, "Nowy folder")
  1481.        if ok and name:
  1482.            folder_path = os.path.join(dir_path, name)
  1483.            try:
  1484.                if os.path.exists(folder_path):
  1485.                    QMessageBox.warning(self, "Błąd", f"Folder już istnieje: {folder_path}")
  1486.                    return
  1487.                # Use the model's method which handles refreshing and selection/editing
  1488.                index = self.model.index(dir_path)
  1489.                if index.isValid():
  1490.                    new_index = self.model.mkdir(index, name)
  1491.                    if new_index.isValid():
  1492.                        # Optional: Expand parent and select/rename new folder
  1493.                        self.setExpanded(index, True)
  1494.                        self.setCurrentIndex(new_index)
  1495.                        self.edit(new_index)
  1496.                        if self.parent() and hasattr(self.parent(), 'update_status_bar_message'):
  1497.                            self.parent().update_status_bar_message(f"Utworzono nowy folder: {os.path.basename(folder_path)}")
  1498.                        else:
  1499.                            print(f"Folder {folder_path} utworzony, ale brak metody 'update_status_bar_message'!")
  1500.                    else:
  1501.                        QMessageBox.warning(self, "Błąd", f"Nie można utworzyć folderu '{name}'. Sprawdź uprawnienia.")
  1502.                        if self.parent() and hasattr(self.parent(), 'update_status_bar_message'):
  1503.                            self.parent().update_status_bar_message(f"Błąd tworzenia folderu: {name}")
  1504.                else:
  1505.                    # Fallback if dir_path cannot be found in the model (less likely for valid paths)
  1506.                    os.mkdir(folder_path)
  1507.                    self.model.refresh(self.model.index(dir_path))  # Manual refresh
  1508.                    new_index = self.model.index(folder_path)
  1509.                    if new_index.isValid():
  1510.                        self.setExpanded(self.model.index(dir_path), True)
  1511.                        self.setCurrentIndex(new_index)
  1512.                        self.edit(new_index)
  1513.                        if self.parent() and hasattr(self.parent(), 'update_status_bar_message'):
  1514.                            self.parent().update_status_bar_message(f"Utworzono nowy folder: {os.path.basename(folder_path)}")
  1515.                    else:
  1516.                        QMessageBox.warning(self, "Błąd", f"Nie można utworzyć folderu '{name}'. Sprawdź uprawnienia.")
  1517.                        if self.parent() and hasattr(self.parent(), 'update_status_bar_message'):
  1518.                            self.parent().update_status_bar_message(f"Błąd tworzenia folderu: {name}")
  1519.            except Exception as e:
  1520.                QMessageBox.warning(self, "Błąd", f"Nie można utworzyć folderu '{name}':\n{e}")
  1521.                if self.parent() and hasattr(self.parent(), 'update_status_bar_message'):
  1522.                    self.parent().update_status_bar_message(f"Błąd tworzenia folderu: {e}")
  1523.  
  1524.  
  1525.  
  1526.    def delete_items(self, file_paths: list):
  1527.        """Initiates deletion of a list of files/directories."""
  1528.        if not file_paths:
  1529.            return
  1530.  
  1531.        # Get confirmation for multiple items
  1532.        if len(file_paths) > 1:
  1533.            items_list = "\n".join([os.path.basename(p) for p in file_paths])
  1534.            reply = QMessageBox.question(self, "Usuń zaznaczone",
  1535.                                         f"Czy na pewno chcesz usunąć następujące elementy?\n\n{items_list}\n\nTa operacja jest nieodwracalna.",
  1536.                                         QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  1537.        else:
  1538.            # Confirmation for single item (reusing logic from show_context_menu)
  1539.            item_name = os.path.basename(file_paths[0])
  1540.            reply = QMessageBox.question(self, "Usuń", f"Czy na pewno chcesz usunąć '{item_name}'?",
  1541.                                         QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  1542.  
  1543.        if reply == QMessageBox.StandardButton.Yes:
  1544.            deleted_count = 0
  1545.            error_messages = []
  1546.            parent_dirs_to_refresh = set()
  1547.  
  1548.            for file_path in file_paths:
  1549.                parent_dirs_to_refresh.add(os.path.dirname(file_path))
  1550.                try:
  1551.                    index = self.model.index(file_path)
  1552.                    if index.isValid():
  1553.                        # QFileSystemModel.remove handles both files and non-empty directories recursively
  1554.                        # on supported platforms (like Windows, macOS). On Linux, it might be just rmdir for empty dirs.
  1555.                        # Let's prioritize the model's method first as it might be more integrated.
  1556.                        # For robustness, we can keep the shutil fallback.
  1557.                        # NOTE: model.remove returns True on success, False on failure, doesn't raise exceptions.
  1558.                        if self.model.remove(index.row(), 1, index.parent()):
  1559.                             print(f"Usunięto element (model): {file_path}")
  1560.                             deleted_count += 1
  1561.                        else:
  1562.                             # model.remove failed, try recursive deletion with shutil/os
  1563.                             print(f"Model nie usunął '{file_path}', próbuję shutil/os...")
  1564.                             if os.path.isdir(file_path):
  1565.                                  shutil.rmtree(file_path)
  1566.                                  print(f"Usunięto katalog (shutil): {file_path}")
  1567.                                  deleted_count += 1
  1568.                             elif os.path.exists(file_path):
  1569.                                  os.remove(file_path)
  1570.                                  print(f"Usunięto plik (os.remove): {file_path}")
  1571.                                  deleted_count += 1
  1572.                             else:
  1573.                                  # Should not happen if index was valid initially
  1574.                                  error_messages.append(f"Nie znaleziono: {file_path}")
  1575.                    else:
  1576.                        error_messages.append(f"Nieprawidłowa ścieżka lub element niedostępny: {file_path}")
  1577.                except Exception as e:
  1578.                    error_messages.append(f"Nie można usunąć '{os.path.basename(file_path)}': {e}")
  1579.                    print(f"Błąd usuwania '{file_path}': {traceback.format_exc()}") # Log error
  1580.  
  1581.            # Refresh parent directories that were affected
  1582.            for parent_dir in parent_dirs_to_refresh:
  1583.                 if os.path.exists(parent_dir): # Ensure parent still exists
  1584.                     self.model.refresh(self.model.index(parent_dir))
  1585.  
  1586.            if error_messages:
  1587.                 QMessageBox.warning(self, "Błąd usuwania", "Wystąpiły błędy podczas usuwania niektórych elementów:\n\n" + "\n".join(error_messages))
  1588.                 self.parent().update_status_bar_message(f"Wystąpiły błędy podczas usuwania ({len(error_messages)} błędów)")
  1589.            elif deleted_count > 0:
  1590.                 self.parent().update_status_bar_message(f"Pomyślnie usunięto {deleted_count} elementów.")
  1591.  
  1592.  
  1593.    def copy_items(self, paths_to_copy: list):
  1594.         """Stores paths for copy operation in the internal clipboard."""
  1595.         if not paths_to_copy:
  1596.              return
  1597.         self._clipboard_paths = paths_to_copy
  1598.         self._is_cut_operation = False
  1599.         self.parent().update_status_bar_message(f"Skopiowano {len(paths_to_copy)} elementów.")
  1600.         print(f"Skopiowano: {self._clipboard_paths}") # Debug print
  1601.  
  1602.  
  1603.    def cut_items(self, paths_to_cut: list):
  1604.         """Stores paths for cut operation in the internal clipboard."""
  1605.         if not paths_to_cut:
  1606.              return
  1607.         self._clipboard_paths = paths_to_cut
  1608.         self._is_cut_operation = True
  1609.         self.parent().update_status_bar_message(f"Wycięto {len(paths_to_cut)} elementów.")
  1610.         print(f"Wycięto: {self._clipboard_paths}") # Debug print
  1611.  
  1612.  
  1613.    def paste_items(self, destination_dir: str):
  1614.         """Pastes items from the internal clipboard into the destination directory."""
  1615.         if not self._clipboard_paths:
  1616.              self.parent().update_status_bar_message("Schowek jest pusty.")
  1617.              return
  1618.  
  1619.         if not os.path.isdir(destination_dir):
  1620.              QMessageBox.warning(self, "Błąd wklejania", f"Docelowa ścieżka nie jest katalogiem: {destination_dir}")
  1621.              self.parent().update_status_bar_message(f"Błąd wklejania: {destination_dir} nie jest katalogiem.")
  1622.              return
  1623.  
  1624.         if not os.access(destination_dir, os.W_OK):
  1625.              QMessageBox.warning(self, "Błąd wklejania", f"Brak uprawnień zapisu w katalogu docelowym: {destination_dir}")
  1626.              self.parent().update_status_bar_message(f"Błąd wklejania: Brak uprawnień w {destination_dir}.")
  1627.              return
  1628.  
  1629.         operation = "Przenoszenie" if self._is_cut_operation else "Kopiowanie"
  1630.         self.parent().update_status_bar_message(f"{operation} {len(self._clipboard_paths)} elementów do '{os.path.basename(destination_dir)}'...")
  1631.  
  1632.         success_count = 0
  1633.         error_messages = []
  1634.         parent_dirs_to_refresh = {destination_dir} # Always refresh destination
  1635.  
  1636.         for src_path in self._clipboard_paths:
  1637.              if not os.path.exists(src_path):
  1638.                   error_messages.append(f"Źródło nie istnieje: {os.path.basename(src_path)}")
  1639.                   continue
  1640.  
  1641.              item_name = os.path.basename(src_path)
  1642.              dest_path = os.path.join(destination_dir, item_name)
  1643.  
  1644.              # Prevent pasting an item into itself or its sub-directory during a move
  1645.              if self._is_cut_operation and src_path == dest_path:
  1646.                   error_messages.append(f"Nie można przenieść '{item_name}' w to samo miejsce.")
  1647.                   continue
  1648.              if self._is_cut_operation and os.path.commonpath([src_path, dest_path]) == src_path and os.path.isdir(src_path):
  1649.                  error_messages.append(f"Nie można przenieść '{item_name}' do jego podkatalogu.")
  1650.                  continue
  1651.  
  1652.              # Handle potential overwrite (simple overwrite for now)
  1653.              if os.path.exists(dest_path):
  1654.                   # Ask for confirmation? For simplicity, let's overwrite or skip for now.
  1655.                   # A more complex dialog could be added here.
  1656.                   # For this example, let's just overwrite.
  1657.                   if os.path.isdir(dest_path):
  1658.                       try: shutil.rmtree(dest_path)
  1659.                       except Exception as e: error_messages.append(f"Nie można nadpisać katalogu '{item_name}': {e}"); continue
  1660.                   else:
  1661.                        try: os.remove(dest_path)
  1662.                        except Exception as e: error_messages.append(f"Nie można nadpisać pliku '{item_name}': {e}"); continue
  1663.  
  1664.  
  1665.              try:
  1666.                   if self._is_cut_operation:
  1667.                        # Move the item
  1668.                        shutil.move(src_path, dest_path)
  1669.                        success_count += 1
  1670.                        parent_dirs_to_refresh.add(os.path.dirname(src_path)) # Also refresh source's parent on move
  1671.                   else:
  1672.                        # Copy the item (recursive for directories)
  1673.                        if os.path.isdir(src_path):
  1674.                             shutil.copytree(src_path, dest_path)
  1675.                        else:
  1676.                             shutil.copy2(src_path, dest_path) # copy2 preserves metadata
  1677.                        success_count += 1
  1678.  
  1679.              except Exception as e:
  1680.                   error_messages.append(f"Błąd {operation.lower()} '{item_name}': {e}")
  1681.                   print(f"Błąd {operation.lower()} '{src_path}' do '{dest_path}': {traceback.format_exc()}") # Log error
  1682.  
  1683.  
  1684.         # Refresh affected directories
  1685.         for refresh_dir in parent_dirs_to_refresh:
  1686.              if os.path.exists(refresh_dir):
  1687.                   self.model.refresh(self.model.index(refresh_dir))
  1688.  
  1689.         if self._is_cut_operation and success_count > 0:
  1690.              # Clear clipboard only if it was a cut operation and at least one item was successfully moved
  1691.              self._clipboard_paths = []
  1692.              self._is_cut_operation = False
  1693.  
  1694.         if error_messages:
  1695.              QMessageBox.warning(self, f"Błąd {operation.lower()}enia", f"Wystąpiły błędy podczas {operation.lower()}enia:\n\n" + "\n".join(error_messages))
  1696.              self.parent().update_status_bar_message(f"Wystąpiły błędy podczas {operation.lower()}enia ({len(error_messages)} błędów)")
  1697.         elif success_count > 0:
  1698.              self.parent().update_status_bar_message(f"Pomyślnie {operation.lower()}ono {success_count} elementów.")
  1699.         else:
  1700.              # This case happens if clipboard was empty or all items failed
  1701.              if not self._clipboard_paths: # If clipboard was empty initially
  1702.                 pass # Message already handled at the start
  1703.              else: # All items failed
  1704.                 self.parent().update_status_bar_message(f"Nie udało się {operation.lower()}ić żadnych elementów.")
  1705.  
  1706.    def show_in_explorer(self, file_path):
  1707.        """Opens the file or folder in the native file explorer."""
  1708.        if sys.platform == "win32":
  1709.            try:
  1710.                 # Use explorer.exe /select, to select the file/folder
  1711.                 subprocess.Popen(['explorer.exe', '/select,', os.path.normpath(file_path)])
  1712.                 self.parent().update_status_bar_message(f"Otworzono w eksploratorze: {os.path.basename(file_path)}")
  1713.            except FileNotFoundError:
  1714.                 QMessageBox.warning(self, "Błąd", "Nie znaleziono 'explorer.exe'.")
  1715.                 self.parent().update_status_bar_message("Błąd: Nie znaleziono explorer.exe.")
  1716.            except Exception as e:
  1717.                 QMessageBox.warning(self, "Błąd", f"Nie można otworzyć menedżera plików:\n{e}")
  1718.                 self.parent().update_status_bar_message(f"Błąd otwarcia w menedżerze: {e}")
  1719.        elif sys.platform == "darwin":  # macOS
  1720.            try:
  1721.                 # Use 'open -R' to reveal file in Finder, or 'open' for folder
  1722.                 subprocess.Popen(['open', '-R', file_path])
  1723.                 self.parent().update_status_bar_message(f"Otworzono w Finderze: {os.path.basename(file_path)}")
  1724.            except FileNotFoundError:
  1725.                 QMessageBox.warning(self, "Błąd", "Nie znaleziono 'open'.")
  1726.                 self.parent().update_status_bar_message("Błąd: Nie znaleziono open.")
  1727.            except Exception as e:
  1728.                 QMessageBox.warning(self, "Błąd", f"Nie można otworzyć Findera:\n{e}")
  1729.                 self.parent().update_status_bar_message(f"Błąd otwarcia w Finderze: {e}")
  1730.        else:  # Linux
  1731.            try:
  1732.                # Use xdg-open which should open the containing folder
  1733.                # For a file, xdg-open opens the file. To open the folder containing the file:
  1734.                target_path = os.path.dirname(file_path) if os.path.isfile(file_path) else file_path
  1735.                subprocess.Popen(['xdg-open', target_path])
  1736.                self.parent().update_status_bar_message(f"Otworzono w menedżerze plików: {os.path.basename(target_path)}")
  1737.            except FileNotFoundError:
  1738.                QMessageBox.warning(self, "Błąd", "Nie znaleziono 'xdg-open'. Nie można otworzyć lokalizacji pliku.")
  1739.                self.parent().update_status_bar_message("Błąd: Nie znaleziono xdg-open.")
  1740.            except Exception as e:
  1741.                 QMessageBox.warning(self, "Błąd", f"Nie można otworzyć lokalizacji pliku:\n{e}")
  1742.                 self.parent().update_status_bar_message(f"Błąd otwarcia w menedżerze: {e}")
  1743.  
  1744.    # Open file logic is now handled by the main window via signal
  1745.    # The file explorer's open_file method is effectively replaced by on_item_activated
  1746.    # which emits openFilesRequested.
  1747.  
  1748.  
  1749. # --- Main Application Window ---
  1750.  
  1751. class CodeEditorWindow(QMainWindow):
  1752.    def __init__(self, parent=None):
  1753.        super().__init__(parent)
  1754.        self.setWindowTitle("Edytor Kodu AI")
  1755.        self.setGeometry(100, 100, 1200, 800)
  1756.  
  1757.        # Load settings
  1758.        self.settings = load_settings()
  1759.        self.current_api_type = self.settings.get("api_type", DEFAULT_MODEL_CONFIG[0])
  1760.        self.current_model_identifier = self.settings.get("model_identifier", DEFAULT_MODEL_CONFIG[1])
  1761.        self.mistral_api_key = self.settings.get("mistral_api_key") # Load Mistral key
  1762.        # Gemini key is loaded globally GEMINI_API_KEY_GLOBAL
  1763.  
  1764.        self.recent_files = self.settings["recent_files"]
  1765.        self.font_size = self.settings["font_size"]
  1766.        self.theme = self.settings["theme"]
  1767.        self.workspace = self.settings["workspace"]
  1768.        self.show_sidebar = self.settings["show_sidebar"]
  1769.        self.show_toolbar = self.settings["show_toolbar"]
  1770.        self.show_statusbar = self.settings["show_statusbar"]
  1771.  
  1772.        # Initialize UI
  1773.        self.init_ui()
  1774.  
  1775.        # Store references to menu actions for toggling visibility checks (Fix AttributeError)
  1776.        self.action_toggle_sidebar = self.findChild(QAction, "Przełącz Pasek Boczny")
  1777.        self.action_toggle_toolbar = self.findChild(QAction, "Przełącz Pasek Narzędzi")
  1778.        self.action_toggle_statusbar = self.findChild(QAction, "Przełącz Pasek Stanu")
  1779.  
  1780.  
  1781.        # Chat History State
  1782.        # Stored as list of (role, content, metadata) tuples
  1783.        self.chat_history = []
  1784.  
  1785.        # Threading Setup for API calls
  1786.        self.worker = None
  1787.        self.worker_thread = None
  1788.        self._is_processing = False
  1789.  
  1790.        # State for streaming response
  1791.        self.current_placeholder_widget = None
  1792.        self.current_response_content = ""
  1793.  
  1794.        # Setup status bar message timer
  1795.        self._status_timer = QTimer(self)
  1796.        self._status_timer.setSingleShot(True)
  1797.        self._status_timer.timeout.connect(self.clear_status_bar_message)
  1798.  
  1799.        # Open workspace if set and exists
  1800.        if self.workspace and os.path.exists(self.workspace) and os.path.isdir(self.workspace):
  1801.            self.file_explorer.setRootIndex(self.file_explorer.model.index(self.workspace))
  1802.            self.update_status_bar_message(f"Obszar roboczy: {self.workspace}")
  1803.        else:
  1804.            # If workspace is not set or invalid, set root to home directory
  1805.            home_dir = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.HomeLocation)[0]
  1806.            self.file_explorer.model.setRootPath(home_dir)
  1807.            self.file_explorer.setRootIndex(self.file_explorer.model.index(home_dir))
  1808.            self.workspace = home_dir  # Update settings to reflect actual root
  1809.            self.settings["workspace"] = self.workspace
  1810.            save_settings(self.settings)  # Save updated workspace
  1811.            self.update_status_bar_message(f"Ustawiono domyślny obszar roboczy: {self.workspace}")
  1812.  
  1813.  
  1814.        # Add welcome message
  1815.        # Find the display name for the initial model
  1816.        initial_model_name = next((name for api_type, identifier, name in ACTIVE_MODELS_CONFIG if api_type == self.current_api_type and identifier == self.current_model_identifier), self.current_model_identifier)
  1817.  
  1818.        self.add_message("assistant", f"Witaj w edytorze kodu AI! Aktualnie działam na modelu '{initial_model_name}'. Jak mogę Ci dziś pomóc?")
  1819.  
  1820.    def init_ui(self):
  1821.        central_widget = QWidget()
  1822.        self.setCentralWidget(central_widget)
  1823.  
  1824.        self.main_splitter = QSplitter(Qt.Orientation.Horizontal)
  1825.  
  1826.        self.sidebar = QWidget()
  1827.        sidebar_layout = QVBoxLayout(self.sidebar)
  1828.        sidebar_layout.setContentsMargins(0, 0, 0, 0)
  1829.        sidebar_layout.setSpacing(0)
  1830.  
  1831.        # FileExplorer initialization (this is where the error occurred)
  1832.        # The fix is inside the FileExplorer class itself
  1833.        self.file_explorer = FileExplorer(self)
  1834.        sidebar_layout.addWidget(self.file_explorer)
  1835.        # Connect signals from FileExplorer
  1836.        self.file_explorer.openFilesRequested.connect(self.open_files)
  1837.        self.file_explorer.deleteItemsRequested.connect(self.file_explorer.delete_items) # Connect to file_explorer's delete method
  1838.  
  1839.  
  1840.        self.right_panel = QSplitter(Qt.Orientation.Vertical)
  1841.  
  1842.        self.tabs = QTabWidget()
  1843.        self.tabs.setTabsClosable(True)
  1844.        self.tabs.tabCloseRequested.connect(self.close_tab)
  1845.        self.tabs.currentChanged.connect(self.update_status_bar)
  1846.  
  1847.        self.chat_container = QWidget()
  1848.        chat_layout = QVBoxLayout(self.chat_container)
  1849.        chat_layout.setContentsMargins(0, 0, 0, 0)
  1850.        chat_layout.setSpacing(0)
  1851.  
  1852.        self.chat_scroll = QScrollArea()
  1853.        self.chat_scroll.setWidgetResizable(True)
  1854.        self.chat_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
  1855.  
  1856.        self.chat_widget = QWidget()
  1857.        self.chat_widget.setObjectName("chat_widget")
  1858.        self.chat_layout = QVBoxLayout(self.chat_widget)
  1859.        self.chat_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter)
  1860.        self.chat_layout.setSpacing(10)
  1861.        self.chat_layout.addStretch(1)
  1862.  
  1863.        self.chat_scroll.setWidget(self.chat_widget)
  1864.        chat_layout.addWidget(self.chat_scroll)
  1865.  
  1866.        self.chat_input = QLineEdit()
  1867.        self.chat_input.setPlaceholderText("Wpisz wiadomość tutaj...")
  1868.        self.chat_input.returnPressed.connect(self.send_message)
  1869.  
  1870.        self.send_button = QPushButton("Wyślij")
  1871.        self.send_button.clicked.connect(self.send_message)
  1872.  
  1873.        input_layout = QHBoxLayout()
  1874.        input_layout.addWidget(self.chat_input, 1)
  1875.        input_layout.addWidget(self.send_button)
  1876.        chat_layout.addLayout(input_layout)
  1877.  
  1878.        self.main_splitter.addWidget(self.sidebar)
  1879.        self.right_panel.addWidget(self.tabs)
  1880.        self.right_panel.addWidget(self.chat_container)
  1881.        self.right_panel.setStretchFactor(0, 3)
  1882.        self.right_panel.setStretchFactor(1, 1)
  1883.        self.main_splitter.addWidget(self.right_panel)
  1884.  
  1885.        main_layout = QVBoxLayout(central_widget)
  1886.        main_layout.addWidget(self.main_splitter)
  1887.  
  1888.        self.create_menu_bar()
  1889.        self.create_tool_bar()
  1890.        self.status_bar = QStatusBar()
  1891.        self.setStatusBar(self.status_bar)
  1892.        self.update_status_bar()
  1893.  
  1894.        self.apply_font_size(self.font_size)
  1895.        self.apply_theme(self.theme)
  1896.  
  1897.        self.sidebar.setVisible(self.show_sidebar)
  1898.        self.toolbar.setVisible(self.show_toolbar)
  1899.        self.status_bar.setVisible(self.show_statusbar)
  1900.  
  1901.        self.main_splitter.setSizes([200, 800])
  1902.        self.right_panel.setSizes([600, 200])
  1903.  
  1904.    def create_menu_bar(self):
  1905.        menubar = self.menuBar()
  1906.  
  1907.        # File menu
  1908.        file_menu = menubar.addMenu("📄 Plik")
  1909.  
  1910.        new_action = QAction(QIcon.fromTheme("document-new"), "Nowy", self)
  1911.        new_action.setShortcut("Ctrl+N")
  1912.        new_action.setShortcutContext(Qt.ShortcutContext.WindowShortcut)
  1913.        new_action.triggered.connect(self.new_file)
  1914.        file_menu.addAction(new_action)
  1915.  
  1916.        open_action = QAction(QIcon.fromTheme("document-open"), "Otwórz...", self)
  1917.        open_action.setShortcut("Ctrl+O")
  1918.        open_action.setShortcutContext(Qt.ShortcutContext.WindowShortcut)
  1919.        open_action.triggered.connect(self.open_file_dialog)
  1920.        file_menu.addAction(open_action)
  1921.  
  1922.        save_action = QAction(QIcon.fromTheme("document-save"), "Zapisz", self)
  1923.        save_action.setObjectName("action_save") # Add object name for potential lookup
  1924.        save_action.setShortcut("Ctrl+S")
  1925.        save_action.setShortcutContext(Qt.ShortcutContext.WindowShortcut) # Ensure Ctrl+S works even when editor has focus
  1926.        save_action.triggered.connect(self.save_file)
  1927.        file_menu.addAction(save_action)
  1928.  
  1929.        save_as_action = QAction("Zapisz jako...", self)
  1930.        save_as_action.setShortcut("Ctrl+Shift+S")
  1931.        save_as_action.setShortcutContext(Qt.ShortcutContext.WindowShortcut)
  1932.        save_as_action.triggered.connect(self.save_file_as)
  1933.        file_menu.addAction(save_as_action)
  1934.  
  1935.        file_menu.addSeparator()
  1936.  
  1937.        open_workspace_action = QAction(QIcon.fromTheme("folder-open"), "Otwórz Obszar Roboczy...", self)
  1938.        open_workspace_action.triggered.connect(self.open_workspace)
  1939.        file_menu.addAction(open_workspace_action)
  1940.  
  1941.        self.recent_files_menu = file_menu.addMenu("Ostatnie pliki")
  1942.        self.update_recent_files_menu()
  1943.  
  1944.        file_menu.addSeparator()
  1945.  
  1946.        exit_action = QAction("Wyjście", self)
  1947.        exit_action.setShortcut("Ctrl+Q")
  1948.        exit_action.setShortcutContext(Qt.ShortcutContext.WindowShortcut)
  1949.        exit_action.triggered.connect(self.close)
  1950.        file_menu.addAction(exit_action)
  1951.  
  1952.        # Edit menu
  1953.        edit_menu = menubar.addMenu("✏️ Edycja")
  1954.  
  1955.        undo_action = QAction(QIcon.fromTheme("edit-undo"), "Cofnij", self)
  1956.        undo_action.setShortcut("Ctrl+Z")
  1957.        undo_action.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) # Standard editor shortcut
  1958.        undo_action.triggered.connect(self.undo)
  1959.        edit_menu.addAction(undo_action)
  1960.  
  1961.        redo_action = QAction(QIcon.fromTheme("edit-redo"), "Ponów", self)
  1962.        redo_action.setShortcut("Ctrl+Y")
  1963.        redo_action.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) # Standard editor shortcut
  1964.        redo_action.triggered.connect(self.redo)
  1965.        edit_menu.addAction(redo_action)
  1966.  
  1967.        edit_menu.addSeparator()
  1968.  
  1969.        cut_action = QAction(QIcon.fromTheme("edit-cut"), "Wytnij", self)
  1970.        cut_action.setShortcut("Ctrl+X")
  1971.        cut_action.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) # Standard editor shortcut
  1972.        cut_action.triggered.connect(self.cut)
  1973.        edit_menu.addAction(cut_action)
  1974.  
  1975.        copy_action = QAction(QIcon.fromTheme("edit-copy"), "Kopiuj", self)
  1976.        copy_action.setShortcut("Ctrl+C")
  1977.        copy_action.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) # Standard editor shortcut
  1978.        copy_action.triggered.connect(self.copy)
  1979.        edit_menu.addAction(copy_action)
  1980.  
  1981.        paste_action = QAction(QIcon.fromTheme("edit-paste"), "Wklej", self)
  1982.        paste_action.setShortcut("Ctrl+V")
  1983.        paste_action.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) # Standard editor shortcut
  1984.        paste_action.triggered.connect(self.paste)
  1985.        edit_menu.addAction(paste_action)
  1986.  
  1987.        edit_menu.addSeparator()
  1988.  
  1989.        find_action = QAction(QIcon.fromTheme("edit-find"), "Znajdź...", self)
  1990.        find_action.setShortcut("Ctrl+F")
  1991.        find_action.setShortcutContext(Qt.ShortcutContext.WindowShortcut) # Find can often be window-wide
  1992.        find_action.triggered.connect(self.find)
  1993.        edit_menu.addAction(find_action)
  1994.  
  1995.        replace_action = QAction(QIcon.fromTheme("edit-find-replace"), "Zamień...", self)
  1996.        replace_action.setShortcut("Ctrl+H")
  1997.        replace_action.setShortcutContext(Qt.ShortcutContext.WindowShortcut)
  1998.        replace_action.triggered.connect(self.replace)
  1999.        edit_menu.addAction(replace_action)
  2000.  
  2001.        # View menu
  2002.        view_menu = menubar.addMenu("🖼️ Widok")
  2003.  
  2004.        # Store references to toggle actions (Fix AttributeError)
  2005.        self.action_toggle_sidebar = QAction("Przełącz Pasek Boczny", self)
  2006.        self.action_toggle_sidebar.setObjectName("Przełącz Pasek Boczny") # Set object name for findChild if needed elsewhere
  2007.        self.action_toggle_sidebar.setShortcut("Ctrl+B")
  2008.        self.action_toggle_sidebar.setCheckable(True)
  2009.        self.action_toggle_sidebar.setChecked(self.show_sidebar)
  2010.        self.action_toggle_sidebar.triggered.connect(self.toggle_sidebar)
  2011.        view_menu.addAction(self.action_toggle_sidebar)
  2012.  
  2013.        self.action_toggle_toolbar = QAction("Przełącz Pasek Narzędzi", self)
  2014.        self.action_toggle_toolbar.setObjectName("Przełącz Pasek Narzędzi")
  2015.        self.action_toggle_toolbar.setCheckable(True)
  2016.        self.action_toggle_toolbar.setChecked(self.show_toolbar)
  2017.        self.action_toggle_toolbar.triggered.connect(self.toggle_toolbar)
  2018.        view_menu.addAction(self.action_toggle_toolbar)
  2019.  
  2020.        self.action_toggle_statusbar = QAction("Przełącz Pasek Stanu", self)
  2021.        self.action_toggle_statusbar.setObjectName("Przełącz Pasek Stanu")
  2022.        self.action_toggle_statusbar.setCheckable(True)
  2023.        self.action_toggle_statusbar.setChecked(self.show_statusbar)
  2024.        self.action_toggle_statusbar.triggered.connect(self.toggle_statusbar)
  2025.        view_menu.addAction(self.action_toggle_statusbar)
  2026.  
  2027.        view_menu.addSeparator()
  2028.  
  2029.        dark_theme_action = QAction("Ciemny Motyw", self)
  2030.        dark_theme_action.triggered.connect(lambda: self.apply_theme("dark"))
  2031.        view_menu.addAction(dark_theme_action)
  2032.  
  2033.        light_theme_action = QAction("Jasny Motyw", self)
  2034.        light_theme_action.triggered.connect(lambda: self.apply_theme("light"))
  2035.        view_menu.addAction(light_theme_action)
  2036.  
  2037.        # Tools menu
  2038.        tools_menu = menubar.addMenu("🛠️ Narzędzia")
  2039.  
  2040.        run_code_action = QAction(QIcon.fromTheme("system-run"), "Uruchom kod", self)
  2041.        run_code_action.setShortcut("Ctrl+R")
  2042.        run_code_action.setShortcutContext(Qt.ShortcutContext.WindowShortcut) # Run code is window-wide
  2043.        run_code_action.triggered.connect(self.run_code)
  2044.        tools_menu.addAction(run_code_action)
  2045.  
  2046.        settings_action = QAction(QIcon.fromTheme("preferences-system"), "Ustawienia...", self)
  2047.        settings_action.triggered.connect(self.show_settings_dialog)
  2048.        tools_menu.addAction(settings_action)
  2049.  
  2050.        # Help menu
  2051.        help_menu = menubar.addMenu("❓ Pomoc")
  2052.  
  2053.        about_action = QAction("O programie", self)
  2054.        about_action.triggered.connect(self.show_about)
  2055.        help_menu.addAction(about_action)
  2056.  
  2057.  
  2058.    def create_tool_bar(self):
  2059.        self.toolbar = QToolBar("Główny Pasek Narzędzi")
  2060.        self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.toolbar)
  2061.        self.toolbar.setObjectName("main_toolbar") # Add object name for styling
  2062.  
  2063.        # Add actions (use the same actions created in the menu bar if possible, or recreate)
  2064.        # Recreating ensures they have icons regardless of theme availability
  2065.        self.toolbar.addAction(QAction(QIcon.fromTheme("document-new"), "Nowy", self, triggered=self.new_file))
  2066.        self.toolbar.addAction(QAction(QIcon.fromTheme("document-open"), "Otwórz", self, triggered=self.open_file_dialog))
  2067.        # Connect the toolbar save action to the same slot and set shortcut context
  2068.        save_toolbar_action = QAction(QIcon.fromTheme("document-save"), "Zapisz", self, triggered=self.save_file)
  2069.        save_toolbar_action.setShortcut("Ctrl+S") # Redundant but good practice
  2070.        save_toolbar_action.setShortcutContext(Qt.ShortcutContext.WindowShortcut)
  2071.        self.toolbar.addAction(save_toolbar_action)
  2072.  
  2073.        self.toolbar.addSeparator()
  2074.  
  2075.        undo_action = QAction(QIcon.fromTheme("edit-undo"), "Cofnij", self, triggered=self.undo)
  2076.        undo_action.setShortcut("Ctrl+Z")
  2077.        undo_action.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
  2078.        self.toolbar.addAction(undo_action)
  2079.  
  2080.        redo_action = QAction(QIcon.fromTheme("edit-redo"), "Ponów", self, triggered=self.redo)
  2081.        redo_action.setShortcut("Ctrl+Y")
  2082.        redo_action.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
  2083.        self.toolbar.addAction(redo_action)
  2084.  
  2085.        self.toolbar.addSeparator()
  2086.  
  2087.        cut_action = QAction(QIcon.fromTheme("edit-cut"), "Wytnij", self, triggered=self.cut)
  2088.        cut_action.setShortcut("Ctrl+X")
  2089.        cut_action.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
  2090.        self.toolbar.addAction(cut_action)
  2091.  
  2092.        copy_action = QAction(QIcon.fromTheme("edit-copy"), "Kopiuj", self, triggered=self.copy)
  2093.        copy_action.setShortcut("Ctrl+C")
  2094.        copy_action.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
  2095.        self.toolbar.addAction(copy_action)
  2096.  
  2097.        paste_action = QAction(QIcon.fromTheme("edit-paste"), "Wklej", self, triggered=self.paste)
  2098.        paste_action.setShortcut("Ctrl+V")
  2099.        paste_action.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
  2100.        self.toolbar.addAction(paste_action)
  2101.  
  2102.        self.toolbar.addSeparator()
  2103.  
  2104.        find_action = QAction(QIcon.fromTheme("edit-find"), "Znajdź", self, triggered=self.find)
  2105.        find_action.setShortcut("Ctrl+F")
  2106.        find_action.setShortcutContext(Qt.ShortcutContext.WindowShortcut)
  2107.        self.toolbar.addAction(find_action)
  2108.  
  2109.        self.toolbar.addSeparator()
  2110.  
  2111.        run_code_action = QAction(QIcon.fromTheme("system-run"), "➡️ Uruchom kod", self, triggered=self.run_code)
  2112.        run_code_action.setShortcut("Ctrl+R")
  2113.        run_code_action.setShortcutContext(Qt.ShortcutContext.WindowShortcut)
  2114.        self.toolbar.addAction(run_code_action)
  2115.  
  2116.    def apply_theme(self, theme_name):
  2117.        self.theme = theme_name
  2118.        self.settings["theme"] = theme_name
  2119.        save_settings(self.settings)
  2120.  
  2121.        if theme_name == "dark":
  2122.            main_bg = QColor("#252526")
  2123.            main_fg = QColor("#ffffff")
  2124.            menu_bg = QColor("#252526")
  2125.            menu_fg = QColor("#ffffff")
  2126.            menu_selected_bg = QColor("#2d2d30")
  2127.            menu_border = QColor("#454545")
  2128.            tab_pane_border = QColor("#454545")
  2129.            tab_pane_bg = QColor("#1e1e1e")
  2130.            tab_bg = QColor("#2d2d2d")
  2131.            tab_fg = QColor("#ffffff")
  2132.            tab_selected_bg = QColor("#1e1e1e")
  2133.            statusbar_bg = QColor("#252526")
  2134.            statusbar_fg = QColor("#ffffff")
  2135.            toolbar_bg = QColor("#252526")
  2136.            toolbar_fg = QColor("#ffffff")
  2137.            splitter_handle_bg = QColor("#252526")
  2138.            lineedit_bg = QColor("#333333")
  2139.            lineedit_fg = QColor("#ffffff")
  2140.            lineedit_border = QColor("#454545")
  2141.            button_bg = QColor("#3c3c3c")
  2142.            button_fg = QColor("#ffffff")
  2143.            button_border = QColor("#5a5a5a")
  2144.            button_hover_bg = QColor("#4a4a4a")
  2145.            button_pressed_bg = QColor("#2a2a2a")
  2146.            editor_bg = QColor("#1e1e1e")
  2147.            editor_fg = QColor("#d4d4d4")
  2148.            linenum_area_bg = QColor("#252526")
  2149.            linenum_fg = QColor("#858585")
  2150.            current_line_bg = QColor("#2d2d2d")
  2151.            chat_bg = QColor("#1e1e1e")
  2152.            chat_input_bg = QColor("#333333")
  2153.            chat_input_fg = QColor("#ffffff")
  2154.            bubble_user = QColor("#3a3a3a")
  2155.            bubble_assistant = QColor("#2d2d2d")
  2156.            bubble_border = QColor("#454545")
  2157.  
  2158.        else: # light theme
  2159.            main_bg = QColor("#f5f5f5")
  2160.            main_fg = QColor("#333333")
  2161.            menu_bg = QColor("#f5f5f5")
  2162.            menu_fg = QColor("#333333")
  2163.            menu_selected_bg = QColor("#e5e5e5")
  2164.            menu_border = QColor("#cccccc")
  2165.            tab_pane_border = QColor("#cccccc")
  2166.            tab_pane_bg = QColor("#ffffff")
  2167.            tab_bg = QColor("#e5e5e5")
  2168.            tab_fg = QColor("#333333")
  2169.            tab_selected_bg = QColor("#ffffff")
  2170.            statusbar_bg = QColor("#f5f5f5")
  2171.            statusbar_fg = QColor("#333333")
  2172.            toolbar_bg = QColor("#f5f5f5")
  2173.            toolbar_fg = QColor("#333333")
  2174.            splitter_handle_bg = QColor("#f5f5f5")
  2175.            lineedit_bg = QColor("#ffffff")
  2176.            lineedit_fg = QColor("#000000")
  2177.            lineedit_border = QColor("#cccccc")
  2178.            button_bg = QColor("#e1e1e1")
  2179.            button_fg = QColor("#000000")
  2180.            button_border = QColor("#cccccc")
  2181.            button_hover_bg = QColor("#d1d1d1")
  2182.            button_pressed_bg = QColor("#c1c1c1")
  2183.            editor_bg = QColor("#ffffff")
  2184.            editor_fg = QColor("#000000")
  2185.            linenum_area_bg = QColor("#eeeeee")
  2186.            linenum_fg = QColor("#666666")
  2187.            current_line_bg = QColor("#f0f0f0")
  2188.            chat_bg = QColor("#ffffff")
  2189.            chat_input_bg = QColor("#ffffff")
  2190.            chat_input_fg = QColor("#000000")
  2191.            bubble_user = QColor("#dcf8c6")
  2192.            bubble_assistant = QColor("#ffffff")
  2193.            bubble_border = QColor("#e0e0e0")
  2194.  
  2195.        palette = QPalette()
  2196.        palette.setColor(QPalette.ColorRole.Window, main_bg)
  2197.        palette.setColor(QPalette.ColorRole.WindowText, main_fg)
  2198.        palette.setColor(QPalette.ColorRole.Base, editor_bg) # Used by QTextEdit background
  2199.        palette.setColor(QPalette.ColorRole.Text, editor_fg) # Used by QTextEdit text color
  2200.        palette.setColor(QPalette.ColorRole.Button, button_bg)
  2201.        palette.setColor(QPalette.ColorRole.ButtonText, button_fg)
  2202.        palette.setColor(QPalette.ColorRole.Highlight, QColor("#0078d4"))
  2203.        palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
  2204.        palette.setColor(QPalette.ColorRole.ToolTipBase, QColor("#ffffe1")) # Tooltip background
  2205.        palette.setColor(QPalette.ColorRole.ToolTipText, QColor("#000000")) # Tooltip text
  2206.  
  2207.  
  2208.        # Set palette for the application
  2209.        QApplication.setPalette(palette)
  2210.  
  2211.        # Apply specific stylesheets
  2212.        self.setStyleSheet(f"""
  2213.             QMainWindow {{
  2214.                 background-color: {main_bg.name()};
  2215.                 color: {main_fg.name()};
  2216.             }}
  2217.             QMenuBar {{
  2218.                 background-color: {menu_bg.name()};
  2219.                 color: {menu_fg.name()};
  2220.             }}
  2221.             QMenuBar::item {{
  2222.                 background-color: transparent;
  2223.                 padding: 5px 10px;
  2224.                 color: {menu_fg.name()};
  2225.             }}
  2226.             QMenuBar::item:selected {{
  2227.                 background-color: {menu_selected_bg.name()};
  2228.             }}
  2229.             QMenu {{
  2230.                 background-color: {menu_bg.name()};
  2231.                 border: 1px solid {menu_border.name()};
  2232.                 color: {menu_fg.name()};
  2233.             }}
  2234.             QMenu::item:selected {{
  2235.                 background-color: {menu_selected_bg.name()};
  2236.             }}
  2237.             QTabWidget::pane {{
  2238.                 border: 1px solid {tab_pane_border.name()};
  2239.                 background: {tab_pane_bg.name()};
  2240.             }}
  2241.             QTabBar::tab {{
  2242.                 background: {tab_bg.name()};
  2243.                 color: {tab_fg.name()};
  2244.                 padding: 5px;
  2245.                 border: 1px solid {tab_pane_border.name()};
  2246.                 border-bottom: none;
  2247.                 min-width: 80px;
  2248.             }}
  2249.              QTabBar::tab:top, QTabBar::tab:bottom {{
  2250.                 border-top-left-radius: 4px;
  2251.                 border-top-right-radius: 4px;
  2252.              }}
  2253.              QTabBar::tab:left, QTabBar::tab:right {{
  2254.                 border-top-left-radius: 4px;
  2255.                 border-bottom-left-radius: 4px;
  2256.              }}
  2257.             QTabBar::tab:hover {{
  2258.                 background: {tab_selected_bg.name()};
  2259.             }}
  2260.             QTabBar::tab:selected {{
  2261.                 background: {tab_selected_bg.name()};
  2262.                 border-bottom: 1px solid {tab_selected_bg.name()};
  2263.             }}
  2264.             QStatusBar {{
  2265.                 background: {statusbar_bg.name()};
  2266.                 color: {statusbar_fg.name()};
  2267.                 border-top: 1px solid {menu_border.name()};
  2268.             }}
  2269.             QToolBar {{
  2270.                 background: {toolbar_bg.name()};
  2271.                 border: none;
  2272.                 padding: 2px;
  2273.                 spacing: 5px;
  2274.             }}
  2275.              QToolButton {{
  2276.                 padding: 4px;
  2277.                 border: 1px solid transparent; /* subtle border for hover */
  2278.                 border-radius: 3px;
  2279.              }}
  2280.              QToolButton:hover {{
  2281.                 background-color: {button_hover_bg.name()};
  2282.                 border-color: {button_border.name()};
  2283.              }}
  2284.              QToolButton:pressed {{
  2285.                 background-color: {button_pressed_bg.name()};
  2286.                 border-color: {button_border.darker(150).name()};
  2287.              }}
  2288.             QSplitter::handle {{
  2289.                 background: {splitter_handle_bg.name()};
  2290.             }}
  2291.             QSplitter::handle:hover {{
  2292.                 background: {button_hover_bg.name()};
  2293.             }}
  2294.             QLineEdit {{
  2295.                 background-color: {lineedit_bg.name()};
  2296.                 color: {lineedit_fg.name()};
  2297.                 border: 1px solid {lineedit_border.name()};
  2298.                 padding: 4px;
  2299.                 border-radius: 4px;
  2300.             }}
  2301.             QPushButton {{
  2302.                 background-color: {button_bg.name()};
  2303.                 color: {button_fg.name()};
  2304.                 border: 1px solid {button_border.name()};
  2305.                 border-radius: 4px;
  2306.                 padding: 5px 10px;
  2307.             }}
  2308.             QPushButton:hover {{
  2309.                 background-color: {button_hover_bg.name()};
  2310.                 border-color: {button_border.darker(120).name()};
  2311.             }}
  2312.             QPushButton:pressed {{
  2313.                 background-color: {button_pressed_bg.name()};
  2314.                 border-color: {button_border.darker(150).name()};
  2315.             }}
  2316.             QScrollArea {{
  2317.                 border: none;
  2318.             }}
  2319.             #chat_widget {{
  2320.                 background-color: {chat_bg.name()};
  2321.             }}
  2322.              QTreeView {{
  2323.                 background-color: {main_bg.name()};
  2324.                 color: {main_fg.name()};
  2325.                 border: 1px solid {tab_pane_border.name()}; /* Add border for separation */
  2326.                 selection-background-color: {palette.color(QPalette.ColorRole.Highlight).name()};
  2327.                 selection-color: {palette.color(QPalette.ColorRole.HighlightedText).name()};
  2328.             }}
  2329.             QTreeView::item:hover {{
  2330.                 background-color: {menu_selected_bg.name()}; /* Subtle hover effect */
  2331.             }}
  2332.  
  2333.         """)
  2334.  
  2335.        # Apply theme colors to CodeEditor instances
  2336.        for i in range(self.tabs.count()):
  2337.            editor = self.tabs.widget(i)
  2338.            if isinstance(editor, CodeEditor):
  2339.                editor.set_theme_colors(editor_bg, editor_fg, linenum_area_bg, linenum_fg, current_line_bg)
  2340.  
  2341.        # Apply theme colors to MessageWidget instances
  2342.        for i in range(self.chat_layout.count()):
  2343.            item = self.chat_layout.itemAt(i)
  2344.            if item and item.widget() and isinstance(item.widget(), MessageWidget):
  2345.                message_widget = item.widget()
  2346.                message_widget.apply_theme_colors(chat_bg, main_fg, bubble_user, bubble_assistant)
  2347.  
  2348.        self.apply_font_size(self.font_size)
  2349.  
  2350.    def update_status_bar_message(self, message: str, timeout_ms: int = 3000):
  2351.        """Displays a temporary message in the status bar."""
  2352.        if self.statusBar() and self.show_statusbar:
  2353.            self.statusBar().showMessage(message, timeout_ms)
  2354.            # The timeout handling by showMessage is often sufficient, but a dedicated timer
  2355.            # can be used for more complex clearing logic if needed.
  2356.            # self._status_timer.stop()
  2357.            # self._status_timer.start(timeout_ms)
  2358.  
  2359.    def clear_status_bar_message(self):
  2360.         """Clears the temporary status bar message."""
  2361.         if self.statusBar() and self.show_statusbar:
  2362.              self.statusBar().clearMessage()
  2363.              self.update_status_bar() # Restore default status message (line/col)
  2364.  
  2365.  
  2366.    def apply_font_size(self, size: int):
  2367.        self.font_size = size
  2368.        self.settings["font_size"] = size
  2369.        save_settings(self.settings)
  2370.  
  2371.        for i in range(self.tabs.count()):
  2372.            editor = self.tabs.widget(i)
  2373.            if isinstance(editor, CodeEditor):
  2374.                editor.set_font_size(size)
  2375.  
  2376.        font = self.chat_input.font()
  2377.        font.setPointSize(size)
  2378.        self.chat_input.setFont(font)
  2379.        # Note: MessageWidget text font size is largely controlled by internal stylesheets (10pt, 9pt).
  2380.  
  2381.    def update_status_bar(self):
  2382.        # Ensure status bar object exists before trying to use it
  2383.        if self.statusBar() and self.show_statusbar:
  2384.            # If there's a temporary message, don't overwrite it immediately
  2385.            if not self.statusBar().currentMessage():
  2386.                editor = self.get_current_editor()
  2387.                if editor:
  2388.                    cursor = editor.textCursor()
  2389.                    line = cursor.blockNumber() + 1
  2390.                    col = cursor.columnNumber() + 1
  2391.                    modified_status = "*" if editor.document().isModified() else ""
  2392.                    file_name = os.path.basename(getattr(editor, 'file_path', 'Bez tytułu'))
  2393.                    self.statusBar().showMessage(f"Plik: {file_name}{modified_status} | Linia: {line}, Kolumna: {col}")
  2394.                else:
  2395.                    current_tab_index = self.tabs.currentIndex()
  2396.                    if current_tab_index != -1:
  2397.                        tab_title = self.tabs.tabText(current_tab_index)
  2398.                        self.statusBar().showMessage(f"Gotowy - {tab_title}")
  2399.                    else:
  2400.                        self.statusBar().showMessage("Gotowy")
  2401.        elif self.statusBar(): # Status bar exists but is hidden
  2402.             self.statusBar().clearMessage() # Clear any lingering message
  2403.  
  2404.  
  2405.    def get_current_editor(self):
  2406.        current_widget = self.tabs.currentWidget()
  2407.        if current_widget and isinstance(current_widget, CodeEditor):
  2408.            return current_widget
  2409.        return None
  2410.  
  2411.    def setup_editor_context_menu(self, editor):
  2412.        """Sets up a custom context menu for the CodeEditor instance."""
  2413.        # Disconnect any default context menu connection first if it existed
  2414.        try:
  2415.             editor.customContextMenuRequested.disconnect()
  2416.        except:
  2417.             pass # Ignore if not connected
  2418.  
  2419.        editor.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
  2420.        editor.customContextMenuRequested.connect(lambda pos: self.show_editor_context_menu(pos, editor)) # Pass editor explicitly
  2421.  
  2422.  
  2423.    def show_editor_context_menu(self, position, editor):
  2424.        """Shows a custom context menu for the CodeEditor."""
  2425.        menu = QMenu(editor)
  2426.  
  2427.        undo_action = menu.addAction("Cofnij")
  2428.        undo_action.setIcon(QIcon.fromTheme("edit-undo"))
  2429.        undo_action.setShortcut("Ctrl+Z")
  2430.        undo_action.triggered.connect(editor.undo)
  2431.        undo_action.setEnabled(editor.document().isUndoAvailable())
  2432.  
  2433.        redo_action = menu.addAction("Ponów")
  2434.        redo_action.setIcon(QIcon.fromTheme("edit-redo"))
  2435.        redo_action.setShortcut("Ctrl+Y")
  2436.        redo_action.triggered.connect(editor.redo)
  2437.        redo_action.setEnabled(editor.document().isRedoAvailable())
  2438.  
  2439.        menu.addSeparator()
  2440.  
  2441.        cut_action = menu.addAction("Wytnij")
  2442.        cut_action.setIcon(QIcon.fromTheme("edit-cut"))
  2443.        cut_action.setShortcut("Ctrl+X")
  2444.        cut_action.triggered.connect(editor.cut)
  2445.        cut_action.setEnabled(editor.textCursor().hasSelection())
  2446.  
  2447.        copy_action = menu.addAction("Kopiuj")
  2448.        copy_action.setIcon(QIcon.fromTheme("edit-copy"))
  2449.        copy_action.setShortcut("Ctrl+C")
  2450.        copy_action.triggered.connect(editor.copy)
  2451.        copy_action.setEnabled(editor.textCursor().hasSelection())
  2452.  
  2453.        paste_action = menu.addAction("Wklej")
  2454.        paste_action.setIcon(QIcon.fromTheme("edit-paste"))
  2455.        paste_action.setShortcut("Ctrl+V")
  2456.        paste_action.triggered.connect(editor.paste)
  2457.        clipboard = QApplication.clipboard()
  2458.        paste_action.setEnabled(bool(clipboard.text()))
  2459.  
  2460.        delete_action = menu.addAction("Usuń")
  2461.        delete_action.setIcon(QIcon.fromTheme("edit-delete"))
  2462.        delete_action.triggered.connect(lambda: editor.textCursor().removeSelectedText())
  2463.        delete_action.setEnabled(editor.textCursor().hasSelection())
  2464.  
  2465.        menu.addSeparator()
  2466.  
  2467.        select_all_action = menu.addAction("Zaznacz wszystko")
  2468.        select_all_action.setIcon(QIcon.fromTheme("edit-select-all"))
  2469.        select_all_action.setShortcut("Ctrl+A")
  2470.        select_all_action.triggered.connect(editor.selectAll)
  2471.  
  2472.        menu.exec(editor.viewport().mapToGlobal(position))
  2473.  
  2474.    def new_file(self):
  2475.        editor = CodeEditor()
  2476.        editor.document().contentsChanged.connect(self.update_status_bar)
  2477.        editor.cursorPositionChanged.connect(self.update_status_bar)
  2478.        editor.document().setModified(False) # New file starts as unmodified
  2479.  
  2480.        self.setup_editor_context_menu(editor) # Setup the context menu
  2481.  
  2482.        tab_title = "Bez tytułu"
  2483.        # Store file_path as None initially for unsaved files
  2484.        editor.file_path = None
  2485.        editor.setObjectName("editor_tab") # Add object name for styling
  2486.  
  2487.        self.tabs.addTab(editor, tab_title)
  2488.        self.tabs.setCurrentWidget(editor)
  2489.  
  2490.        self.apply_font_size(self.font_size)
  2491.        # Re-apply theme to ensure new editor gets correct colors
  2492.        self.apply_theme(self.theme)
  2493.  
  2494.        self.update_recent_files(None) # Add placeholder for untitled file (or just update menu)
  2495.        self.update_status_bar()
  2496.        self.update_status_bar_message("Utworzono nowy plik 'Bez tytułu'.")
  2497.  
  2498.  
  2499.    def open_file_dialog(self):
  2500.        start_dir = self.workspace if self.workspace and os.path.exists(self.workspace) else QStandardPaths.standardLocations(QStandardPaths.StandardLocation.HomeLocation)[0]
  2501.  
  2502.        file_path, _ = QFileDialog.getOpenFileName(self, "Otwórz plik", start_dir,
  2503.            "Wszystkie pliki (*);;"
  2504.            "Pliki Pythona (*.py);;"
  2505.            "Pliki tekstowe (*.txt);;"
  2506.            "Pliki CSS (*.css);;"
  2507.            "Pliki HTML (*.html *.htm);;"
  2508.            "Pliki JavaScript (*.js);;"
  2509.            "Pliki GML (*.gml);;"
  2510.            "Pliki JSON (*.json);;"
  2511.            "Pliki Markdown (*.md);;"
  2512.            "Pliki konfiguracyjne (*.ini *.cfg)")
  2513.        if file_path:
  2514.            self.open_file(file_path)
  2515.  
  2516.    def open_file(self, file_path):
  2517.        """Opens a single file in a new tab."""
  2518.        if not file_path or not os.path.exists(file_path):
  2519.             QMessageBox.warning(self, "Błąd", f"Plik nie znaleziono:\n{file_path}")
  2520.             self.update_status_bar_message(f"Błąd: Plik nie znaleziono ({os.path.basename(file_path)})")
  2521.             return False
  2522.  
  2523.        # Check if the file is already open in a tab
  2524.        for i in range(self.tabs.count()):
  2525.            editor = self.tabs.widget(i)
  2526.            if isinstance(editor, CodeEditor) and hasattr(editor, 'file_path') and editor.file_path == file_path:
  2527.                self.tabs.setCurrentIndex(i)
  2528.                self.update_status_bar_message(f"Przełączono na plik: {os.path.basename(file_path)}")
  2529.                return True
  2530.  
  2531.        # If not already open, open the file
  2532.        try:
  2533.            with open(file_path, 'r', encoding='utf-8') as f:
  2534.                content = f.read()
  2535.  
  2536.            editor = CodeEditor()
  2537.            editor.setPlainText(content)
  2538.  
  2539.            editor.document().contentsChanged.connect(self.update_status_bar)
  2540.            editor.cursorPositionChanged.connect(self.update_status_bar)
  2541.            editor.document().setModified(False) # Newly opened file is not modified
  2542.  
  2543.            self.setup_editor_context_menu(editor)
  2544.  
  2545.            file_name = os.path.basename(file_path)
  2546.            tab_title = file_name
  2547.  
  2548.            # Set syntax highlighting based on file extension
  2549.            # Get the actual CodeEditor highlighter object
  2550.            highlighter = None
  2551.            file_extension = os.path.splitext(file_path)[1].lower()
  2552.            if file_extension == '.py':
  2553.                highlighter = PythonHighlighter(editor.document())
  2554.            elif file_extension == '.css':
  2555.                highlighter = CSSHighlighter(editor.document())
  2556.            elif file_extension in ['.html', '.htm']:
  2557.                highlighter = HTMLHighlighter(editor.document())
  2558.            elif file_extension == '.js':
  2559.                highlighter = JSHighlighter(editor.document())
  2560.            elif file_extension == '.gml':
  2561.                highlighter = GMLHighlighter(editor.document())
  2562.            # Add more extensions/highlighters as needed
  2563.  
  2564.            editor.highlighter = highlighter # Assign the created highlighter (can be None)
  2565.            if highlighter:
  2566.                highlighter.rehighlight()
  2567.  
  2568.  
  2569.            editor.file_path = file_path
  2570.            editor.setObjectName("editor_tab") # Add object name for styling
  2571.  
  2572.            self.tabs.addTab(editor, tab_title)
  2573.            self.tabs.setCurrentWidget(editor)
  2574.  
  2575.            self.update_recent_files(file_path) # Update recent files list
  2576.  
  2577.            self.apply_font_size(self.font_size)
  2578.            self.apply_theme(self.theme) # Re-apply theme to ensure new editor has correct colors
  2579.  
  2580.            self.update_status_bar()
  2581.            self.update_status_bar_message(f"Otworzono plik: {os.path.basename(file_path)}")
  2582.            return True
  2583.        except Exception as e:
  2584.            QMessageBox.warning(self, "Błąd", f"Nie można otworzyć pliku '{file_path}':\n{e}")
  2585.            self.update_status_bar_message(f"Błąd otwierania pliku: {e}")
  2586.            return False
  2587.  
  2588.    def open_files(self, file_paths: list):
  2589.        """Opens a list of files."""
  2590.        if not file_paths:
  2591.             return
  2592.  
  2593.        for file_path in file_paths:
  2594.            # Call the single open_file method for each file in the list
  2595.            self.open_file(file_path)
  2596.  
  2597.  
  2598.    def save_file(self):
  2599.        editor = self.get_current_editor()
  2600.        if not editor:
  2601.            self.update_status_bar_message("Brak aktywnego edytora do zapisania.")
  2602.            return False # No active editor
  2603.  
  2604.        # If editor has a file_path, save to it
  2605.        if hasattr(editor, 'file_path') and editor.file_path and os.path.exists(os.path.dirname(editor.file_path) if os.path.dirname(editor.file_path) else '.'):
  2606.             # Check if the directory exists, or if it's a new file in the current directory
  2607.             file_path = editor.file_path
  2608.        else:
  2609.            # If no file_path (new file) or path is invalid, use Save As
  2610.            return self.save_file_as()
  2611.  
  2612.        # Perform the save operation
  2613.        try:
  2614.            with open(file_path, 'w', encoding='utf-8') as f:
  2615.                f.write(editor.toPlainText())
  2616.  
  2617.            editor.document().setModified(False)
  2618.            self.update_status_bar()
  2619.            self.update_recent_files(file_path)
  2620.            self.update_status_bar_message(f"Zapisano plik: {os.path.basename(file_path)}")
  2621.            return True
  2622.        except Exception as e:
  2623.            QMessageBox.warning(self, "Błąd", f"Nie można zapisać pliku '{file_path}':\n{e}")
  2624.            self.update_status_bar_message(f"Błąd zapisywania pliku: {e}")
  2625.            return False
  2626.  
  2627.    def save_file_as(self):
  2628.        editor = self.get_current_editor()
  2629.        if not editor:
  2630.            self.update_status_bar_message("Brak aktywnego edytora do zapisania.")
  2631.            return False
  2632.  
  2633.        initial_dir = self.workspace if self.workspace and os.path.exists(self.workspace) else QStandardPaths.standardLocations(QStandardPaths.StandardLocation.HomeLocation)[0]
  2634.        if hasattr(editor, 'file_path') and editor.file_path and os.path.dirname(editor.file_path):
  2635.            initial_dir = os.path.dirname(editor.file_path)
  2636.  
  2637.        file_path, _ = QFileDialog.getSaveFileName(self, "Zapisz plik jako", initial_dir, "All Files (*);;Python Files (*.py);;Text Files (*.txt)")
  2638.  
  2639.        if not file_path:
  2640.            self.update_status_bar_message("Zapisywanie anulowane.")
  2641.            return False
  2642.  
  2643.        try:
  2644.            with open(file_path, 'w', encoding='utf-8') as f:
  2645.                f.write(editor.toPlainText())
  2646.  
  2647.            file_name = os.path.basename(file_path)
  2648.            index = self.tabs.indexOf(editor)
  2649.            self.tabs.setTabText(index, file_name)
  2650.  
  2651.            editor.file_path = file_path
  2652.            editor.document().setModified(False)
  2653.            self.update_status_bar()
  2654.            self.update_recent_files(file_path)
  2655.  
  2656.            # Update syntax highlighting if extension changed
  2657.            highlighter = None
  2658.            file_extension = os.path.splitext(file_path)[1].lower()
  2659.            if file_extension == '.py':
  2660.                highlighter = PythonHighlighter(editor.document())
  2661.            elif file_extension == '.css':
  2662.                highlighter = CSSHighlighter(editor.document())
  2663.            elif file_extension in ['.html', '.htm']:
  2664.                highlighter = HTMLHighlighter(editor.document())
  2665.            elif file_extension == '.js':
  2666.                highlighter = JSHighlighter(editor.document())
  2667.            elif file_extension == '.gml':
  2668.                highlighter = GMLHighlighter(editor.document())
  2669.            # Add more extensions/highlighters as needed
  2670.            editor.highlighter = highlighter # Assign the new highlighter
  2671.            editor.document().clearFormats() # Clear old highlighting before applying new one
  2672.            if highlighter:
  2673.                 highlighter.rehighlight()
  2674.  
  2675.  
  2676.            self.update_status_bar_message(f"Zapisano plik jako: {os.path.basename(file_path)}")
  2677.            return True
  2678.        except Exception as e:
  2679.            QMessageBox.warning(self, "Błąd", f"Nie można zapisać pliku '{file_path}':\n{e}")
  2680.            self.update_status_bar_message(f"Błąd zapisywania pliku jako: {e}")
  2681.            return False
  2682.  
  2683.    def close_tab(self, index):
  2684.        editor = self.tabs.widget(index)
  2685.        if editor:
  2686.            if isinstance(editor, CodeEditor) and editor.document().isModified():
  2687.                file_name = os.path.basename(getattr(editor, 'file_path', 'Bez tytułu'))
  2688.                reply = QMessageBox.question(self, "Zapisz zmiany", f"Czy chcesz zapisać zmiany w '{file_name}' przed zamknięciem?",
  2689.                                             QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel)
  2690.                if reply == QMessageBox.StandardButton.Yes:
  2691.                    # Need to save the tab before closing. If save fails/cancelled, stop closing.
  2692.                    # Temporarily set this tab as current to make save_file work on it.
  2693.                    original_index = self.tabs.currentIndex()
  2694.                    self.tabs.setCurrentIndex(index)
  2695.                    save_success = self.save_file()
  2696.                    self.tabs.setCurrentIndex(original_index) # Restore original index
  2697.                    if not save_success:
  2698.                        return # Stop closing if save failed/cancelled
  2699.                elif reply == QMessageBox.StandardButton.Cancel:
  2700.                    return # User cancelled closing
  2701.                # If reply is No or Yes (and save succeeded), continue closing
  2702.  
  2703.            tab_name = self.tabs.tabText(index) # Get name *before* removing tab
  2704.            self.tabs.removeTab(index)
  2705.            editor.deleteLater()
  2706.            self.update_status_bar() # Update status bar as current tab might change
  2707.            self.update_status_bar_message(f"Zamknięto zakładkę: {tab_name}")
  2708.  
  2709.    def update_recent_files(self, file_path):
  2710.        """Updates the list of recent files and the menu."""
  2711.        # Note: None is passed for untitled files, which shouldn't be added to recent.
  2712.        if file_path and isinstance(file_path, str) and os.path.exists(file_path):
  2713.            # Normalize path for consistency
  2714.            file_path = os.path.normpath(file_path)
  2715.            # Remove if already exists to move it to the top
  2716.            if file_path in self.recent_files:
  2717.                self.recent_files.remove(file_path)
  2718.  
  2719.            # Add to the beginning
  2720.            self.recent_files.insert(0, file_path)
  2721.  
  2722.            # Trim the list if it exceeds max size
  2723.            if len(self.recent_files) > RECENT_FILES_MAX:
  2724.                self.recent_files = self.recent_files[:RECENT_FILES_MAX]
  2725.  
  2726.            # Save the updated list
  2727.            self.settings["recent_files"] = self.recent_files
  2728.            save_settings(self.settings)
  2729.  
  2730.        # Always update the menu after potentially changing the list
  2731.        self.update_recent_files_menu()
  2732.  
  2733.  
  2734.    def update_recent_files_menu(self, menu: QMenu = None):
  2735.        """Updates the 'Ostatnie pliki' menu."""
  2736.        # Find the menu if not passed
  2737.        if menu is None:
  2738.             # Iterate through the menu bar actions to find the "Plik" menu
  2739.             for file_action in self.menuBar().actions():
  2740.                 if file_action.text() == "📄 Plik" and file_action.menu():
  2741.                     # Iterate through the "Plik" menu actions to find the "Ostatnie pliki" submenu
  2742.                     for sub_action in file_action.menu().actions():
  2743.                         # Use object name or text for lookup
  2744.                         # Ensure the action has a menu before accessing it
  2745.                         if sub_action.text() == "Ostatnie pliki" and sub_action.menu():
  2746.                             menu = sub_action.menu()
  2747.                             break
  2748.                 if menu: break # Stop searching once found
  2749.  
  2750.        if not menu: # Menu not found, cannot update
  2751.            print("Warning: Recent files menu not found.")
  2752.            return
  2753.  
  2754.        menu.clear()
  2755.        # Filter out non-existent files from the stored list before updating the menu
  2756.        valid_recent_files = [f for f in self.recent_files if os.path.exists(f)]
  2757.  
  2758.        if not valid_recent_files:
  2759.            menu.setEnabled(False)
  2760.            # Add a dummy action
  2761.            disabled_action = QAction("Brak ostatnio otwieranych plików", self)
  2762.            disabled_action.setEnabled(False)
  2763.            menu.addAction(disabled_action)
  2764.        else:
  2765.            menu.setEnabled(True)
  2766.            # Use the filtered list to populate the menu
  2767.            for file_path in valid_recent_files:
  2768.                 action = QAction(os.path.basename(file_path), self)
  2769.                 # Store the file path in the action's data
  2770.                 action.setData(file_path)
  2771.                 action.triggered.connect(self.open_recent_file)
  2772.                 menu.addAction(action)
  2773.  
  2774.            # Update settings with the cleaned list
  2775.            if valid_recent_files != self.recent_files:
  2776.                 self.recent_files = valid_recent_files
  2777.                 self.settings["recent_files"] = self.recent_files
  2778.                 save_settings(self.settings)
  2779.  
  2780.  
  2781.    def open_recent_file(self):
  2782.        action = self.sender()
  2783.        if action:
  2784.            file_path = action.data()
  2785.            if file_path and isinstance(file_path, str) and os.path.exists(file_path):
  2786.                self.open_file(file_path)
  2787.            else:
  2788.                QMessageBox.warning(self, "Błąd", f"Ostatni plik nie znaleziono:\n{file_path}")
  2789.                self.update_status_bar_message(f"Błąd: Ostatni plik nie znaleziono ({os.path.basename(file_path)})")
  2790.                # Remove invalid file from recent list
  2791.                if file_path in self.recent_files:
  2792.                    self.recent_files.remove(file_path)
  2793.                    self.settings["recent_files"] = self.recent_files
  2794.                    save_settings(self.settings)
  2795.                    self.update_recent_files_menu()
  2796.  
  2797.  
  2798.    def open_workspace(self):
  2799.        start_dir = self.workspace if self.workspace and os.path.exists(self.workspace) else QStandardPaths.standardLocations(QStandardPaths.StandardLocation.HomeLocation)[0]
  2800.  
  2801.        dir_path = QFileDialog.getExistingDirectory(self, "Otwórz Obszar Roboczy", start_dir)
  2802.        if dir_path:
  2803.            self.workspace = dir_path
  2804.            self.file_explorer.model.setRootPath(dir_path)
  2805.            self.file_explorer.setRootIndex(self.file_explorer.model.index(dir_path))
  2806.            self.settings["workspace"] = dir_path
  2807.            save_settings(self.settings)
  2808.            self.update_status_bar_message(f"Zmieniono obszar roboczy na: {dir_path}")
  2809.  
  2810.    # Standard editor actions (delegated to current editor)
  2811.    def undo(self):
  2812.        editor = self.get_current_editor()
  2813.        if editor:
  2814.             editor.undo()
  2815.             self.update_status_bar_message("Cofnięto ostatnią operację.")
  2816.  
  2817.  
  2818.    def redo(self):
  2819.        editor = self.get_current_editor()
  2820.        if editor:
  2821.             editor.redo()
  2822.             self.update_status_bar_message("Ponowiono ostatnią operację.")
  2823.  
  2824.  
  2825.    def cut(self):
  2826.        editor = self.get_current_editor()
  2827.        if editor:
  2828.             editor.cut()
  2829.             self.update_status_bar_message("Wycięto zaznaczenie.")
  2830.  
  2831.  
  2832.    def copy(self):
  2833.        editor = self.get_current_editor()
  2834.        if editor:
  2835.             editor.copy()
  2836.             self.update_status_bar_message("Skopiowano zaznaczenie.")
  2837.  
  2838.  
  2839.    def paste(self):
  2840.        editor = self.get_current_editor()
  2841.        if editor:
  2842.             editor.paste()
  2843.             self.update_status_bar_message("Wklejono zawartość schowka.")
  2844.  
  2845.  
  2846.    # Basic find/replace (delegated)
  2847.    def find(self):
  2848.        editor = self.get_current_editor()
  2849.        if editor:
  2850.            text, ok = QInputDialog.getText(self, "Znajdź", "Szukaj:")
  2851.            if ok and text:
  2852.                cursor = editor.textCursor()
  2853.                # Find from current position first
  2854.                if not editor.find(text):
  2855.                     # If not found from current position, try from the beginning
  2856.                     cursor.movePosition(QTextCursor.MoveOperation.Start)
  2857.                     editor.setTextCursor(cursor)
  2858.                     if not editor.find(text):
  2859.                         QMessageBox.information(self, "Znajdź", f"'{text}' nie znaleziono.")
  2860.                         self.update_status_bar_message(f"Nie znaleziono '{text}'.")
  2861.                     else:
  2862.                         self.update_status_bar_message(f"Znaleziono pierwsze wystąpienie '{text}'.")
  2863.                else:
  2864.                     self.update_status_bar_message(f"Znaleziono następne wystąpienie '{text}'.")
  2865.  
  2866.  
  2867.    def replace(self):
  2868.        editor = self.get_current_editor()
  2869.        if editor:
  2870.            find_text, ok1 = QInputDialog.getText(self, "Zamień", "Szukaj:")
  2871.            if ok1 and find_text:
  2872.                replace_text, ok2 = QInputDialog.getText(self, "Zamień", "Zamień na:")
  2873.                if ok2:
  2874.                    # Simple text replacement (replaces all occurrences)
  2875.                    text = editor.toPlainText()
  2876.                    # Count occurrences before replacing
  2877.                    occurrences = text.count(find_text)
  2878.                    if occurrences > 0:
  2879.                        new_text = text.replace(find_text, replace_text)
  2880.                        editor.setPlainText(new_text)
  2881.                        editor.document().setModified(True)
  2882.                        self.update_status_bar()
  2883.                        self.update_status_bar_message(f"Zamieniono {occurrences} wystąpień '{find_text}'.")
  2884.                    else:
  2885.                         QMessageBox.information(self, "Zamień", f"'{find_text}' nie znaleziono.")
  2886.                         self.update_status_bar_message(f"Nie znaleziono '{find_text}' do zamiany.")
  2887.  
  2888.  
  2889.    # Code execution
  2890.    def run_code(self):
  2891.        editor = self.get_current_editor()
  2892.        if editor:
  2893.            code = editor.toPlainText()
  2894.            if not code.strip():
  2895.                self.add_message("assistant", "Brak kodu do uruchomienia.")
  2896.                self.update_status_bar_message("Brak kodu do uruchomienia.")
  2897.                return
  2898.  
  2899.            # Add user message to chat history
  2900.            self.add_message("user", f"Proszę uruchomić ten kod:\n```\n{code}\n```")
  2901.  
  2902.            try:
  2903.                # Use a temporary file with a .py extension to allow python interpreter to identify it
  2904.                # Ensure the temp directory exists and has write permissions
  2905.                temp_dir = tempfile.gettempdir()
  2906.                if not os.access(temp_dir, os.W_OK):
  2907.                    QMessageBox.warning(self, "Błąd", f"Brak uprawnień zapisu w katalogu tymczasowym: {temp_dir}")
  2908.                    self.add_message("assistant", f"Błąd: Brak uprawnień zapisu w katalogu tymczasowym.")
  2909.                    self.update_status_bar_message("Błąd: Brak uprawnień zapisu w katalogu tymczasowym.")
  2910.                    return
  2911.  
  2912.                # Ensure the temp file has a .py extension for the interpreter
  2913.                temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8', dir=temp_dir)
  2914.                temp_file_path = temp_file.name
  2915.                temp_file.write(code)
  2916.                temp_file.close()
  2917.  
  2918.  
  2919.                self.add_message("assistant", "⚙️ Uruchamiam kod...", {"type": "placeholder"})
  2920.                self.update_status_bar_message(f"Uruchamiam kod ({os.path.basename(getattr(editor, 'file_path', 'Bez tytułu'))})")
  2921.  
  2922.  
  2923.                # Run the code in a separate process
  2924.                # Using sys.executable ensures we use the same Python interpreter running the app
  2925.                process = subprocess.Popen([sys.executable, temp_file_path],
  2926.                                          stdout=subprocess.PIPE,
  2927.                                          stderr=subprocess.PIPE,
  2928.                                          text=True, # Decode output as text
  2929.                                          encoding='utf-8',
  2930.                                          cwd=os.path.dirname(temp_file_path)) # Set working directory
  2931.  
  2932.  
  2933.                stdout = ""
  2934.                stderr = ""
  2935.                try:
  2936.                    # Use a slightly longer timeout, maybe 30 seconds?
  2937.                    # Or make it configurable. Let's stick to 10 for now.
  2938.                    timeout_seconds = 10
  2939.                    stdout, stderr = process.communicate(timeout=timeout_seconds)
  2940.                    process.wait() # Ensure process is truly finished
  2941.                except subprocess.TimeoutExpired:
  2942.                    process.kill() # Kill the process if it times out
  2943.                    process.wait() # Wait for it to be killed
  2944.                    stderr = f"Czas wykonania kodu minął po {timeout_seconds} sekundach. Proces został przerwany.\n{stderr}"
  2945.                    self.update_status_bar_message(f"Wykonanie kodu przekroczyło limit czasu ({timeout_seconds}s).")
  2946.  
  2947.                except Exception as proc_err:
  2948.                     stderr = f"Błąd wewnętrzny podczas uruchamiania procesu: {proc_err}\n{stderr}"
  2949.                     self.update_status_bar_message(f"Błąd wewnętrzny uruchamiania kodu: {proc_err}")
  2950.  
  2951.  
  2952.                # Clean up the temporary file
  2953.                try:
  2954.                    os.unlink(temp_file_path)
  2955.                except OSError as e:
  2956.                    print(f"Błąd usuwania pliku tymczasowego {temp_file_path}: {e}")
  2957.                    # Decide if this should be a user-visible error, probably not critical
  2958.  
  2959.                # Remove the placeholder message
  2960.                self.remove_last_message_widget()
  2961.  
  2962.                # Display the output and errors in the chat
  2963.                output_message = ""
  2964.                if stdout:
  2965.                    output_message += f"Wyjście:\n```text\n{stdout.strip()}\n```\n" # Use 'text' for plain output highlighting
  2966.                if stderr:
  2967.                    output_message += f"Błędy:\n```text\n{stderr.strip()}\n```\n"
  2968.  
  2969.                if output_message:
  2970.                    self.add_message("assistant", f"Wykonanie kodu zakończone:\n{output_message}")
  2971.                    self.update_status_bar_message("Wykonanie kodu zakończone.")
  2972.                else:
  2973.                    self.add_message("assistant", "Kod wykonano bez wyjścia i błędów.")
  2974.                    self.update_status_bar_message("Kod wykonano bez wyjścia/błędów.")
  2975.  
  2976.            except FileNotFoundError:
  2977.                self.remove_last_message_widget()
  2978.                self.add_message("assistant", f"Błąd: Interpreter Pythona '{sys.executable}' nie znaleziono.")
  2979.                self.update_status_bar_message(f"Błąd: Interpreter Pythona nie znaleziono.")
  2980.            except Exception as e:
  2981.                self.remove_last_message_widget()
  2982.                self.add_message("assistant", f"Błąd wykonania kodu: {str(e)}")
  2983.                print(f"Błąd uruchamiania kodu: {traceback.format_exc()}")
  2984.                self.update_status_bar_message(f"Błąd wykonania kodu: {e}")
  2985.  
  2986.    # Visibility toggles (Fixed AttributeErrors by using stored action references)
  2987.    def toggle_sidebar(self):
  2988.        self.show_sidebar = not self.show_sidebar
  2989.        self.sidebar.setVisible(self.show_sidebar)
  2990.        self.settings["show_sidebar"] = self.show_sidebar
  2991.        save_settings(self.settings)
  2992.        if self.action_toggle_sidebar: # Check if reference exists
  2993.            self.action_toggle_sidebar.setChecked(self.show_sidebar)
  2994.        self.update_status_bar_message(f"Pasek boczny: {'widoczny' if self.show_sidebar else 'ukryty'}")
  2995.  
  2996.    def toggle_toolbar(self):
  2997.        self.show_toolbar = not self.show_toolbar
  2998.        self.toolbar.setVisible(self.show_toolbar)
  2999.        self.settings["show_toolbar"] = self.show_toolbar
  3000.        save_settings(self.settings)
  3001.        if self.action_toggle_toolbar: # Check if reference exists
  3002.            self.action_toggle_toolbar.setChecked(self.show_toolbar)
  3003.        self.update_status_bar_message(f"Pasek narzędzi: {'widoczny' if self.show_toolbar else 'ukryty'}")
  3004.  
  3005.    def toggle_statusbar(self):
  3006.        self.show_statusbar = not self.show_statusbar
  3007.        if self.statusBar():
  3008.            self.statusBar().setVisible(self.show_statusbar)
  3009.        self.settings["show_statusbar"] = self.show_statusbar
  3010.        save_settings(self.settings)
  3011.        if self.action_toggle_statusbar: # Check if reference exists
  3012.            self.action_toggle_statusbar.setChecked(self.show_statusbar)
  3013.        # Status bar message won't appear if status bar is now hidden
  3014.        if self.show_statusbar:
  3015.            self.update_status_bar_message(f"Pasek stanu: {'widoczny' if self.show_statusbar else 'ukryty'}")
  3016.  
  3017.  
  3018.    def show_settings_dialog(self):
  3019.        # Pass active model configurations and current settings
  3020.        dialog = SettingsDialog(ACTIVE_MODELS_CONFIG, self.settings, self)
  3021.        if dialog.exec() == QDialog.DialogCode.Accepted:
  3022.            # Update model and API type
  3023.            selected_api_type, selected_identifier = dialog.get_selected_model_config()
  3024.  
  3025.            model_changed = (selected_api_type != self.current_api_type or selected_identifier != self.current_model_identifier)
  3026.  
  3027.            self.current_api_type = selected_api_type
  3028.            self.current_model_identifier = selected_identifier
  3029.            self.settings["api_type"] = self.current_api_type
  3030.            self.settings["model_identifier"] = self.current_model_identifier
  3031.  
  3032.            # Update Mistral key
  3033.            if HAS_MISTRAL:
  3034.                new_mistral_key = dialog.get_mistral_api_key()
  3035.                key_changed = (new_mistral_key != self.mistral_api_key)
  3036.                self.mistral_api_key = new_mistral_key
  3037.                self.settings["mistral_api_key"] = self.mistral_api_key
  3038.            else:
  3039.                 key_changed = False # Key couldn't change if Mistral isn't supported
  3040.  
  3041.            save_settings(self.settings)
  3042.  
  3043.            # Inform user about settings changes
  3044.            status_messages = []
  3045.            if model_changed:
  3046.                display_name = next((name for api_type, identifier, name in ACTIVE_MODELS_CONFIG if api_type == self.current_api_type and identifier == self.current_model_identifier), self.current_model_identifier)
  3047.                status_messages.append(f"Model AI zmieniono na '{display_name}'.")
  3048.            if key_changed:
  3049.                 status_messages.append(f"Ustawienia klucza API Mistral zaktualizowane.")
  3050.  
  3051.            new_theme = dialog.get_selected_theme()
  3052.            if new_theme != self.theme:
  3053.                self.apply_theme(new_theme)
  3054.                status_messages.append(f"Motyw zmieniono na '{new_theme}'.")
  3055.  
  3056.  
  3057.            new_font_size = dialog.get_font_size()
  3058.            if new_font_size != self.font_size:
  3059.                self.apply_font_size(new_font_size)
  3060.                status_messages.append(f"Rozmiar czcionki zmieniono na {new_font_size}.")
  3061.  
  3062.  
  3063.            ui_visibility = dialog.get_ui_visibility()
  3064.            if ui_visibility["show_sidebar"] != self.show_sidebar:
  3065.                self.toggle_sidebar() # This call updates settings and status bar message internally
  3066.            if ui_visibility["show_toolbar"] != self.show_toolbar:
  3067.                self.toggle_toolbar() # This call updates settings and status bar message internally
  3068.            if ui_visibility["show_statusbar"] != self.show_statusbar:
  3069.                self.toggle_statusbar() # This call updates settings and status bar message internally
  3070.  
  3071.  
  3072.            if status_messages:
  3073.                 self.update_status_bar_message("Ustawienia zaktualizowane: " + "; ".join(status_messages), 5000)
  3074.            else:
  3075.                 self.update_status_bar_message("Ustawienia zapisano.")
  3076.  
  3077.  
  3078.    def show_about(self):
  3079.        QMessageBox.about(self, "Informacje o Edytorze Kodu AI",
  3080.                          "<h2>Edytor Kodu AI</h2>"
  3081.                          "<p>Prosty edytor kodu z integracją czatu AI.</p>"
  3082.                          "<p>Wykorzystuje API Google Gemini i Mistral do pomocy AI.</p>"
  3083.                          "<p>Wersja 1.1</p>"
  3084.                          "<p>Stworzony przy użyciu PyQt6, google-generativeai i mistralai</p>")
  3085.        self.update_status_bar_message("Wyświetlono informacje o programie.")
  3086.  
  3087.  
  3088.    # --- Chat Message Handling ---
  3089.    def add_message(self, role: str, content: str, metadata: dict = None):
  3090.        # Add message to internal history (excluding placeholders, errors, empty)
  3091.        if metadata is None or metadata.get("type") not in ["placeholder", "error", "empty_response"]:
  3092.            # Store clean history for API calls
  3093.            self.chat_history.append((role, content, metadata))
  3094.            # Limit history size
  3095.            HISTORY_LIMIT = 20 # Keep a reasonable history size
  3096.            if len(self.chat_history) > HISTORY_LIMIT:
  3097.                self.chat_history = self.chat_history[len(self.chat_history) - HISTORY_LIMIT:]
  3098.  
  3099.        message_widget = MessageWidget(role, content, metadata=metadata, parent=self.chat_widget)
  3100.  
  3101.        # Apply current theme colors
  3102.        if self.theme == "dark":
  3103.            bubble_user_color = QColor("#3a3a3a")
  3104.            bubble_assistant_color = QColor("#2d2d2d")
  3105.            main_fg_color = QColor("#ffffff")
  3106.        else: # light theme
  3107.            bubble_user_color = QColor("#dcf8c6")
  3108.            bubble_assistant_color = QColor("#ffffff")
  3109.            main_fg_color = QColor("#333333")
  3110.  
  3111.        message_widget.apply_theme_colors(self.chat_widget.palette().color(QPalette.ColorRole.Window), main_fg_color, bubble_user_color, bubble_assistant_color)
  3112.  
  3113.        # Add the widget to the chat layout, keeping the stretch item at the end
  3114.        # Find the stretch item
  3115.        stretch_item = None
  3116.        if self.chat_layout.count() > 0:
  3117.             last_item = self.chat_layout.itemAt(self.chat_layout.count() - 1)
  3118.             if last_item and last_item.spacerItem():
  3119.                  stretch_item = self.chat_layout.takeAt(self.chat_layout.count() - 1)
  3120.  
  3121.        self.chat_layout.addWidget(message_widget)
  3122.  
  3123.        # Re-add the stretch item if found
  3124.        if stretch_item:
  3125.             self.chat_layout.addItem(stretch_item)
  3126.        elif self.chat_layout.count() == 1: # If this is the very first message and no stretch was added yet
  3127.             self.chat_layout.addStretch(1)
  3128.  
  3129.  
  3130.        if message_widget.is_placeholder:
  3131.            self.current_placeholder_widget = message_widget
  3132.  
  3133.        QTimer.singleShot(50, self.scroll_chat_to_bottom)
  3134.  
  3135.    def remove_last_message_widget(self):
  3136.        if self.chat_layout.count() > 1: # Need at least 1 widget + 1 stretch
  3137.            widget_to_remove = None
  3138.            # Find the last widget item before the stretch
  3139.            for i in reversed(range(self.chat_layout.count())):
  3140.                item = self.chat_layout.itemAt(i)
  3141.                if item and item.widget():
  3142.                    widget_to_remove = item.widget()
  3143.                    break
  3144.  
  3145.            if widget_to_remove:
  3146.                self.chat_layout.removeWidget(widget_to_remove)
  3147.                widget_to_remove.deleteLater()
  3148.  
  3149.            self.current_placeholder_widget = None
  3150.  
  3151.    def scroll_chat_to_bottom(self):
  3152.        self.chat_scroll.verticalScrollBar().setValue(self.chat_scroll.verticalScrollBar().maximum())
  3153.  
  3154.    def send_message(self):
  3155.        if self._is_processing:
  3156.            return
  3157.  
  3158.        msg = self.chat_input.text().strip()
  3159.        if not msg:
  3160.            return
  3161.  
  3162.        self._is_processing = True
  3163.        self.add_message("user", msg, None)
  3164.  
  3165.        self.chat_input.clear()
  3166.        self.chat_input.setPlaceholderText("Czekam na odpowiedź...")
  3167.        self.send_button.setEnabled(False)
  3168.        self.chat_input.setEnabled(False)
  3169.        self.update_status_bar_message("Wysyłam zapytanie do modelu AI...")
  3170.  
  3171.  
  3172.        # Stop any running worker thread
  3173.        if self.worker_thread and self.worker_thread.isRunning():
  3174.            print("Stopping existing worker thread...")
  3175.            self.worker.stop()
  3176.            if not self.worker_thread.wait(1000): # Wait up to 1 second
  3177.                print("Worker thread did not stop cleanly, terminating.")
  3178.                self.worker_thread.terminate()
  3179.                self.worker_thread.wait()
  3180.            print("Worker thread stopped.")
  3181.  
  3182.        # Determine which worker to use based on selected API type
  3183.        api_type = self.current_api_type
  3184.        model_identifier = self.current_model_identifier
  3185.        worker = None
  3186.  
  3187.        if api_type == "gemini" and HAS_GEMINI:
  3188.            api_key = GEMINI_API_KEY_GLOBAL # Use the globally loaded Gemini key
  3189.            if not api_key:
  3190.                 self.handle_error("Klucz API Google Gemini nie znaleziono. Ustaw go w pliku .api_key lub w ustawieniach.")
  3191.                 self._is_processing = False # Reset state
  3192.                 self.send_button.setEnabled(True)
  3193.                 self.chat_input.setEnabled(True)
  3194.                 self.chat_input.setPlaceholderText("Wpisz wiadomość tutaj...")
  3195.                 return
  3196.            worker = GeminiWorker(api_key, msg, list(self.chat_history), model_identifier)
  3197.  
  3198.        elif api_type == "mistral" and HAS_MISTRAL:
  3199.            api_key = self.mistral_api_key # Use the key from settings
  3200.            if not api_key:
  3201.                 self.handle_error("Klucz API Mistral nie ustawiono w ustawieniach.")
  3202.                 self._is_processing = False # Reset state
  3203.                 self.send_button.setEnabled(True)
  3204.                 self.chat_input.setEnabled(True)
  3205.                 self.chat_input.setPlaceholderText("Wpisz wiadomość tutaj...")
  3206.                 return
  3207.            worker = MistralWorker(api_key, msg, list(self.chat_history), model_identifier)
  3208.        else:
  3209.            # Check if the selected model type is "none" (fallback when no APIs are installed)
  3210.            if api_type == "none":
  3211.                 self.handle_error("Brak dostępnych modeli AI. Proszę zainstalować obsługiwane biblioteki API.")
  3212.            else:
  3213.                 self.handle_error(f"Wybrany model '{model_identifier}' (API '{api_type}') nie jest obsługiwany lub brakuje zainstalowanych bibliotek.")
  3214.  
  3215.            self._is_processing = False # Reset state
  3216.            self.send_button.setEnabled(True)
  3217.            self.chat_input.setEnabled(True)
  3218.            self.chat_input.setPlaceholderText("Wpisz wiadomość tutaj...")
  3219.            return
  3220.  
  3221.  
  3222.        self.worker = worker # Store the current worker
  3223.        self.worker_thread = QThread()
  3224.        self.worker.moveToThread(self.worker_thread)
  3225.  
  3226.        self.worker.response_chunk.connect(self.handle_response_chunk)
  3227.        self.worker.response_complete.connect(self.handle_response_complete)
  3228.        self.worker.error.connect(self.handle_error)
  3229.  
  3230.        self.worker_thread.started.connect(self.worker.run)
  3231.        self.worker.finished.connect(self.worker_thread.quit)
  3232.        self.worker.finished.connect(self.worker.deleteLater)
  3233.        self.worker_thread.finished.connect(self.worker_thread.deleteLater)
  3234.  
  3235.        self.current_response_content = ""
  3236.        display_name = next((name for t, i, name in ACTIVE_MODELS_CONFIG if t == api_type and i == model_identifier), model_identifier)
  3237.        self.add_message("assistant", f"⚙️ Przetwarzam z użyciem '{display_name}'...", {"type": "placeholder"})
  3238.  
  3239.        self.worker_thread.start()
  3240.  
  3241.    def handle_response_chunk(self, chunk: str):
  3242.        self.current_response_content += chunk
  3243.        if self.current_placeholder_widget:
  3244.            self.current_placeholder_widget.update_placeholder_text(self.current_response_content)
  3245.        self.scroll_chat_to_bottom()
  3246.  
  3247.    def handle_response_complete(self):
  3248.        if self.current_placeholder_widget:
  3249.            self.remove_last_message_widget()
  3250.  
  3251.        final_content = self.current_response_content.strip()
  3252.        if final_content:
  3253.            self.add_message("assistant", self.current_response_content, None)
  3254.        else:
  3255.            self.add_message("assistant", "Otrzymano pustą odpowiedź od modelu.", {"type": "empty_response"})
  3256.  
  3257.        self.current_response_content = ""
  3258.        self.send_button.setEnabled(True)
  3259.        self.chat_input.setEnabled(True)
  3260.        self.chat_input.setPlaceholderText("Wpisz wiadomość tutaj...")
  3261.        self.chat_input.setFocus()
  3262.        self._is_processing = False
  3263.        self.scroll_chat_to_bottom()
  3264.        self.update_status_bar_message("Odpowiedź AI zakończona.")
  3265.  
  3266.  
  3267.    def handle_error(self, error_message: str):
  3268.        if self.current_placeholder_widget:
  3269.            self.remove_last_message_widget()
  3270.  
  3271.        error_styled_message = f"<span style='color: #cc0000; font-weight: bold;'>Błąd API:</span> {error_message}"
  3272.        self.add_message("assistant", error_styled_message, {"type": "error"})
  3273.  
  3274.        self.send_button.setEnabled(True)
  3275.        self.chat_input.setEnabled(True)
  3276.        self.chat_input.setPlaceholderText("Wpisz wiadomość tutaj...")
  3277.        self.chat_input.setFocus()
  3278.        self._is_processing = False
  3279.        self.current_response_content = ""
  3280.        self.scroll_chat_to_bottom()
  3281.        self.update_status_bar_message(f"Błąd API: {error_message[:50]}...") # Truncate message for status bar
  3282.  
  3283.    def closeEvent(self, event):
  3284.        # Stop the worker thread
  3285.        if self.worker_thread and self.worker_thread.isRunning():
  3286.            self.worker.stop()
  3287.            if not self.worker_thread.wait(3000): # Wait up to 3 seconds
  3288.                print("Worker thread did not finish after stop signal, terminating.")
  3289.                self.worker_thread.terminate()
  3290.                self.worker_thread.wait()
  3291.  
  3292.        # Check for unsaved files
  3293.        unsaved_tabs = []
  3294.        for i in range(self.tabs.count()):
  3295.            editor = self.tabs.widget(i)
  3296.            if isinstance(editor, CodeEditor) and editor.document().isModified():
  3297.                 unsaved_tabs.append(i)
  3298.  
  3299.        if unsaved_tabs:
  3300.            # Ask about saving all unsaved tabs
  3301.            reply = QMessageBox.question(self, "Zapisz zmiany", f"Masz niezapisane zmiany w {len(unsaved_tabs)} plikach.\nCzy chcesz zapisać zmiany przed wyjściem?",
  3302.                                         QMessageBox.StandardButton.SaveAll | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
  3303.  
  3304.            if reply == QMessageBox.StandardButton.Cancel:
  3305.                event.ignore() # Stop closing
  3306.                self.update_status_bar_message("Zamykanie anulowane.")
  3307.                return
  3308.            elif reply == QMessageBox.StandardButton.SaveAll:
  3309.                save_success = True
  3310.                # Iterate over unsaved tabs and try to save each one
  3311.                for index in unsaved_tabs:
  3312.                     editor = self.tabs.widget(index) # Get the editor again, index might change if tabs are closed during save
  3313.                     if editor and isinstance(editor, CodeEditor) and editor.document().isModified():
  3314.                          # Temporarily switch to the tab to make save_file work correctly
  3315.                          original_index = self.tabs.currentIndex()
  3316.                          self.tabs.setCurrentIndex(index)
  3317.                          current_save_success = self.save_file() # This updates status bar
  3318.                          self.tabs.setCurrentIndex(original_index) # Restore original index
  3319.  
  3320.                          if not current_save_success:
  3321.                               save_success = False
  3322.                               # If any save fails/cancelled, stop the whole process
  3323.                               event.ignore()
  3324.                               self.update_status_bar_message("Zamykanie przerwane z powodu błędu zapisu.")
  3325.                               return # Stop the loop and closing process
  3326.  
  3327.                if save_success:
  3328.                     event.accept() # Continue closing if all saves succeeded
  3329.                else:
  3330.                     # This path should ideally not be reached due to the 'return' above,
  3331.                     # but as a safeguard:
  3332.                     event.ignore()
  3333.                     self.update_status_bar_message("Zamykanie przerwane z powodu błędu zapisu.") # Redundant but safe
  3334.                     return
  3335.  
  3336.            elif reply == QMessageBox.StandardButton.Discard:
  3337.                # Discard changes and close all tabs
  3338.                # We need to close tabs in reverse order to avoid index issues
  3339.                for i in reversed(unsaved_tabs):
  3340.                    self.tabs.removeTab(i) # Remove tab without saving check
  3341.  
  3342.                event.accept() # Continue closing
  3343.                self.update_status_bar_message("Zamknięto pliki bez zapisywania zmian.")
  3344.  
  3345.  
  3346.        else:
  3347.             # No unsaved tabs, just accept the close event
  3348.             event.accept()
  3349.  
  3350.  
  3351. # --- Main Application Entry Point ---
  3352.  
  3353. if __name__ == "__main__":
  3354.    # Enable High DPI scaling
  3355.    QGuiApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
  3356.  
  3357.    app = QApplication(sys.argv)
  3358.    app.setApplicationName("Edytor Kodu AI")
  3359.    app.setOrganizationName("YourOrganization")
  3360.  
  3361.    # Initialize icon theme if available
  3362.    # QIcon.setThemeName("breeze-dark") # Example theme, requires icon theme installed
  3363.  
  3364.    try:
  3365.        main_window = CodeEditorWindow()
  3366.        main_window.show()
  3367.        sys.exit(app.exec())
  3368.    except Exception as app_error:
  3369.        print(f"Wystąpił nieoczekiwany błąd podczas uruchamiania aplikacji:\n{app_error}")
  3370.        traceback.print_exc()
  3371.        # Ensure message box is shown even if app failed to initialize fully
  3372.        try:
  3373.            QMessageBox.critical(None, "Błąd uruchomienia aplikacji", f"Wystąpił nieoczekiwany błąd podczas uruchomienia aplikacji:\n{app_error}\n\nSprawdź konsolę, aby uzyskać szczegóły.")
  3374.        except Exception as mb_error:
  3375.            print(f"Could not show error message box: {mb_error}")
  3376.        sys.exit(1)
Advertisement
Comments
  • dutmdh
    5 days
    # text 0.29 KB | 0 0
    1. Leon West Accidental Goblin King Audiobooks 1-4
    2.  
    3. magnet:?xt=urn:btih:49d386821d7a4093ac6209084242fbdf979b0ac1
    4. magnet:?xt=urn:btih:9f49b631081256fdab2d7b13927ce27bd44cf683
    5. magnet:?xt=urn:btih:6ef04c5cd32428d63afbca8a5b754688082059d3
    6. magnet:?xt=urn:btih:feae4390a335f0743bc8852d70183ace64240e1a
Add Comment
Please, Sign In to add comment
Advertisement