Advertisement
PaffcioStudio

Untitled

May 18th, 2025
556
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 127.82 KB | None | 0 0
  1. import sys
  2. import os
  3. import json
  4. import subprocess
  5. import re
  6. import platform
  7. import shutil # Do kopiowania plików
  8. import shlex # Do bezpiecznego formatowania komend
  9. from PyQt6.QtWidgets import (
  10.     QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
  11.     QSplitter, QTreeView, QTabWidget, QPlainTextEdit,
  12.     QPushButton, QLineEdit, QFileDialog, QMenuBar, QToolBar, QStatusBar,
  13.     QMessageBox, QMenu, QStyleFactory, QDialog, QFormLayout,
  14.     QLabel, QDialogButtonBox, QComboBox, QToolButton,
  15.     QInputDialog, QSpinBox, QSizePolicy, QAbstractItemView,
  16.     QFrame # Dodano do okna ustawień
  17. )
  18. from PyQt6.QtGui import (
  19.     QIcon, QAction, QKeySequence, QTextCharFormat, QFont,
  20.     QSyntaxHighlighter, QTextDocument, QColor, QFileSystemModel,
  21.     QDesktopServices, # Do otwierania plików w domyślnych aplikacjach
  22.     QPalette # Do motywów
  23. )
  24. from PyQt6.QtCore import (
  25.     QDir, Qt, QProcess, QSettings, QFileInfo, QThread, pyqtSignal, QTimer, QSize,
  26.     QStandardPaths, QUrl, QLocale, QCoreApplication, QProcessEnvironment
  27. )
  28. try:
  29.     import qtawesome as qta
  30. except ImportError:
  31.     qta = None
  32.     print("Zainstaluj qtawesome ('pip install qtawesome') dla lepszych ikon.", file=sys.stderr)
  33. APP_DIR = os.path.dirname(os.path.abspath(__file__))
  34. DATA_DIR = os.path.join(APP_DIR, 'data')
  35. PROJECTS_DIR = os.path.join(APP_DIR, 'projects')
  36. SETTINGS_FILE = os.path.join(DATA_DIR, 'settings.json')
  37. RECENTS_FILE = os.path.join(DATA_DIR, 'recents.json')
  38. os.makedirs(DATA_DIR, exist_ok=True)
  39. os.makedirs(PROJECTS_DIR, exist_ok=True)
  40. FORMAT_DEFAULT = QTextCharFormat()
  41. FORMAT_KEYWORD = QTextCharFormat()
  42. FORMAT_KEYWORD.setForeground(QColor("#000080")) # Navy
  43. FORMAT_STRING = QTextCharFormat()
  44. FORMAT_STRING.setForeground(QColor("#008000")) # Green
  45. FORMAT_COMMENT = QTextCharFormat()
  46. FORMAT_COMMENT.setForeground(QColor("#808080")) # Gray
  47. FORMAT_COMMENT.setFontItalic(True)
  48. FORMAT_FUNCTION = QTextCharFormat()
  49. FORMAT_FUNCTION.setForeground(QColor("#0000FF")) # Blue
  50. FORMAT_CLASS = QTextCharFormat()
  51. FORMAT_CLASS.setForeground(QColor("#A52A2A")) # Brown
  52. FORMAT_CLASS.setFontWeight(QFont.Weight.Bold)
  53. FORMAT_NUMBERS = QTextCharFormat()
  54. FORMAT_NUMBERS.setForeground(QColor("#FF0000")) # Red
  55. FORMAT_OPERATOR = QTextCharFormat()
  56. FORMAT_OPERATOR.setForeground(QColor("#A62929")) # Dark Red
  57. FORMAT_BUILTIN = QTextCharFormat()
  58. FORMAT_BUILTIN.setForeground(QColor("#008080")) # Teal
  59. FORMAT_SECTION = QTextCharFormat() # Dla sekcji w INI
  60. FORMAT_SECTION.setForeground(QColor("#800080")) # Purple
  61. FORMAT_SECTION.setFontWeight(QFont.Weight.Bold)
  62. FORMAT_PROPERTY = QTextCharFormat() # Dla kluczy/właściwości w INI/JSON
  63. FORMAT_PROPERTY.setForeground(QColor("#B8860B")) # DarkGoldenrod
  64. HIGHLIGHTING_RULES = {
  65.     'python': {
  66.         'keywords': ['and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else',
  67.                      'except', 'False', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'None',
  68.                      'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield'],
  69.         'builtins': ['print', 'len', 'range', 'list', 'dict', 'tuple', 'set', 'str', 'int', 'float', 'bool', 'open', 'isinstance'],
  70.         'patterns': [
  71.             (r'\b[A-Za-z_][A-Za-z0-9_]*\s*\(', FORMAT_FUNCTION), # Funkcje (proste wykrycie, litera/podkreślnik na początku)
  72.             (r'\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b', FORMAT_CLASS), # Klasy
  73.             (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS), # Liczby
  74.             (r'[+\-*/=<>!&|]', FORMAT_OPERATOR), # Operatory
  75.             (r'".*?"', FORMAT_STRING), # Stringi w cudzysłowach podwójnych
  76.             (r"'.*?'", FORMAT_STRING), # Stringi w cudzysłowach pojedynczych
  77.             (r'#.*', FORMAT_COMMENT), # Komentarze liniowe
  78.         ]
  79.     },
  80.     'javascript': {
  81.         'keywords': ['abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue',
  82.                      'debugger', 'default', 'delete', 'do', 'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final',
  83.                      'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', 'int', 'interface',
  84.                      'let', 'long', 'native', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static',
  85.                      'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 'typeof', 'var', 'void',
  86.                      'volatile', 'while', 'with', 'yield'],
  87.          'builtins': ['console', 'log', 'warn', 'error', 'info', 'Math', 'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'RegExp', 'JSON', 'Promise', 'setTimeout', 'setInterval'], # Przykładowe
  88.         'patterns': [
  89.             (r'\b[A-Za-z_][A-Za-z0-9_]*\s*\(', FORMAT_FUNCTION), # Funkcje (proste wykrycie)
  90.              (r'\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b', FORMAT_CLASS), # Klasy
  91.             (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS), # Liczby
  92.             (r'[+\-*/=<>!&|]', FORMAT_OPERATOR), # Operatory
  93.             (r'".*?"', FORMAT_STRING), # Stringi w cudzysłowach podwójnych
  94.             (r"'.*?'", FORMAT_STRING), # Stringi w cudzysłowach pojedynczych
  95.             (r'//.*', FORMAT_COMMENT), # Komentarze liniowe
  96.         ]
  97.     },
  98.      'html': {
  99.         'keywords': [], # HTML nie ma tradycyjnych słów kluczowych w ten sposób
  100.         'builtins': [], # Encje HTML można potraktować jako builtins
  101.         'patterns': [
  102.             (r'<[^>]+>', FORMAT_KEYWORD), # Tagi HTML (uproszczone, bez atrybutów)
  103.             (r'[a-zA-Z0-9_-]+\s*=', FORMAT_OPERATOR), # Znaki '=' w atrybutach
  104.             (r'".*?"', FORMAT_STRING), # Wartości atrybutów
  105.             (r"'.*?'", FORMAT_STRING), # Wartości atrybutów
  106.              (r'&[a-zA-Z0-9]+;', FORMAT_BUILTIN), # Encje HTML
  107.             (r'<!--.*?-->', FORMAT_COMMENT, re.DOTALL), # Komentarze (z re.DOTALL, aby objęły wiele linii)
  108.         ]
  109.     },
  110.     'css': {
  111.         'keywords': [],
  112.         'builtins': [], # Selektory ID
  113.         'patterns': [
  114.             (r'\.[a-zA-Z0-9_-]+', FORMAT_CLASS), # Selektory klas
  115.             (r'#[a-zA-Z0-9_-]+', FORMAT_BUILTIN), # Selektory ID
  116.             (r'[a-zA-Z0-9_-]+\s*:', FORMAT_KEYWORD), # Właściwości CSS
  117.             (r';', FORMAT_OPERATOR), # Średniki
  118.             (r'\{|\}', FORMAT_OPERATOR), # Nawiasy klamrowe
  119.              (r'\(|\)', FORMAT_OPERATOR), # Nawiasy okrągłe (np. w rgb())
  120.             (r'\b\d+(\.\d*)?(px|em|%|vh|vw|rem|pt|cm|mm)?\b', FORMAT_NUMBERS), # Liczby z jednostkami
  121.              (r'#[0-9a-fA-F]{3,6}', FORMAT_NUMBERS), # Kolory HEX
  122.             (r'".*?"', FORMAT_STRING), # Wartości stringów
  123.             (r"'.*?'", FORMAT_STRING), # Wartości stringów
  124.         ]
  125.     },
  126.     'c++': {
  127.          'keywords': ['alignas', 'alignof', 'and', 'and_eq', 'asm', 'atomic_cancel', 'atomic_commit', 'atomic_noexcept', 'auto',
  128.                      'bitand', 'bitor', 'bool', 'break', 'case', 'catch', 'char', 'char8_t', 'char16_t', 'char32_t', 'class',
  129.                      'compl', 'concept', 'const', 'consteval', 'constexpr', 'constinit', 'const_cast', 'continue', 'co_await',
  130.                      'co_return', 'decltype', 'default', 'delete', 'do', 'double', 'dynamic_cast', 'else', 'enum',
  131.                      'explicit', 'export', 'extern', 'false', 'float', 'for', 'friend', 'goto', 'if', 'inline', 'int', 'long',
  132.                      'mutable', 'namespace', 'new', 'noexcept', 'not', 'not_eq', 'nullptr', 'operator', 'or', 'or_eq', 'private',
  133.                      'protected', 'public', 'reflexpr', 'register', 'reinterpret_cast', 'requires', 'return', 'short', 'signed',
  134.                      'sizeof', 'static', 'static_assert', 'static_cast', 'struct', 'switch', 'synchronized', 'template',
  135.                      'this', 'thread_local', 'throw', 'true', 'try', 'typedef', 'typeid', 'typename', 'union', 'unsigned',
  136.                      'using', 'virtual', 'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq'],
  137.          'builtins': ['cout', 'cin', 'endl', 'string', 'vector', 'map', 'set', 'array', 'queue', 'stack', 'pair', 'algorithm', 'iostream', 'fstream', 'sstream', 'cmath', 'cstdlib', 'cstdio', 'ctime'], # Przykładowe popularne
  138.          'patterns': [
  139.              (r'\b[A-Za-z_][A-Za-z0-9_]*\s*\(', FORMAT_FUNCTION), # Funkcje (proste wykrycie)
  140.              (r'\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b', FORMAT_CLASS), # Klasy
  141.              (r'\bstruct\s+([A-Za-z_][A-Za-z0-9_]*)\b', FORMAT_CLASS), # Struktury
  142.              (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS), # Liczby
  143.              (r'[+\-*/=<>!&|%^~?:]', FORMAT_OPERATOR), # Operatory
  144.              (r'".*?"', FORMAT_STRING), # Stringi w cudzysłowach podwójnych
  145.              (r"'.*?'", FORMAT_STRING), # Stringi w cudzysłowach pojedynczych (pojedyncze znaki)
  146.              (r'//.*', FORMAT_COMMENT), # Komentarze liniowe
  147.          ]
  148.     },
  149.     'ini': { # Nowe reguły dla INI
  150.         'keywords': [], # Brak tradycyjnych słów kluczowych
  151.         'builtins': [], # Brak typowych builtins
  152.         'patterns': [
  153.             (r'^\[.*?\]', FORMAT_SECTION), # Sekcje [section]
  154.             (r'^[a-zA-Z0-9_-]+\s*=', FORMAT_PROPERTY), # Klucze property=
  155.             (r';.*', FORMAT_COMMENT), # Komentarze po średniku
  156.             (r'#.*', FORMAT_COMMENT), # Komentarze po krzyżyku
  157.             (r'[+\-*/=<>!&|]', FORMAT_OPERATOR), # Operatory (np. =)
  158.              (r'=\s*".*?"', FORMAT_STRING), # "value"
  159.              (r"=\s*'.*?'", FORMAT_STRING), # 'value'
  160.              (r'=\s*[^;#"\'].*', FORMAT_STRING), # value without quotes or comments/sections
  161.         ]
  162.     },
  163.     'json': { # Nowe reguły dla JSON
  164.         'keywords': ['true', 'false', 'null'], # Literały JSON
  165.         'builtins': [], # Brak typowych builtins
  166.         'patterns': [
  167.             (r'"(?:[^"\\]|\\.)*"\s*:', FORMAT_PROPERTY), # Klucze w cudzysłowach z następującym ':'
  168.             (r'".*?"', FORMAT_STRING), # Wartości stringów (muszą być po kluczach, żeby nie nadpisać klucza)
  169.             (r'\b-?\d+(\.\d+)?([eE][+-]?\d+)?\b', FORMAT_NUMBERS), # Liczby
  170.             (r'\{|\}|\[|\]|:|,', FORMAT_OPERATOR), # Nawiasy, dwukropek, przecinek
  171.         ]
  172.     }
  173. }
  174. class CodeSyntaxHighlighter(QSyntaxHighlighter):
  175.     def __init__(self, parent: QTextDocument, language: str):
  176.         super().__init__(parent)
  177.         self._language = language.lower()
  178.         self._rules = []
  179.         lang_config = HIGHLIGHTING_RULES.get(self._language, {})
  180.         keywords = lang_config.get('keywords', [])
  181.         builtins = lang_config.get('builtins', [])
  182.         patterns = lang_config.get('patterns', [])
  183.         keyword_format = FORMAT_KEYWORD
  184.         for keyword in keywords:
  185.             pattern = r'\b' + re.escape(keyword) + r'\b' # Użyj re.escape dla słów kluczowych
  186.             self._rules.append((re.compile(pattern), keyword_format))
  187.         builtin_format = FORMAT_BUILTIN
  188.         for builtin in builtins:
  189.             pattern = r'\b' + re.escape(builtin) + r'\b'
  190.             self._rules.append((re.compile(pattern), builtin_format))
  191.         for pattern_str, format, *flags in patterns: # Opcjonalne flagi regex np. re.DOTALL
  192.              try:
  193.                  pattern = re.compile(pattern_str, *flags)
  194.                  self._rules.append((pattern, format))
  195.              except re.error as e:
  196.                  print(f"Błąd kompilacji regex '{pattern_str}' dla języka {self._language}: {e}", file=sys.stderr)
  197.     def highlightBlock(self, text: str):
  198.         """Główna metoda kolorująca blok tekstu (linię)."""
  199.         self.setFormat(0, len(text), FORMAT_DEFAULT)
  200.         self.setCurrentBlockState(0) # Domyślny stan dla tego bloku
  201.         block_comment_delimiters = []
  202.         if self._language in ['javascript', 'css', 'c++']:
  203.              block_comment_delimiters.append(("/*", "*/", FORMAT_COMMENT))
  204.         if self._language == 'html':
  205.              pass # Rely on regex pattern for HTML comments
  206.         comment_start_in_prev_block = (self.previousBlockState() == 1) # State 1 means inside /* ... */
  207.         if comment_start_in_prev_block:
  208.              end_delimiter_index = text.find("*/")
  209.              if end_delimiter_index >= 0:
  210.                   self.setFormat(0, end_delimiter_index + 2, FORMAT_COMMENT)
  211.                   self.setCurrentBlockState(0) # Reset state
  212.                   start_pos = end_delimiter_index + 2
  213.              else:
  214.                   self.setFormat(0, len(text), FORMAT_COMMENT)
  215.                   self.setCurrentBlockState(1) # Keep state as inside comment
  216.                   return # Entire line is a comment, no need to parse further
  217.         else:
  218.              start_pos = 0
  219.         start_delimiter = "/*"
  220.         end_delimiter = "*/"
  221.         startIndex = text.find(start_delimiter, start_pos)
  222.         while startIndex >= 0:
  223.             endIndex = text.find(end_delimiter, startIndex)
  224.             if endIndex >= 0:
  225.                 length = endIndex - startIndex + len(end_delimiter)
  226.                 self.setFormat(startIndex, startIndex + length, FORMAT_COMMENT)
  227.                 startIndex = text.find(start_delimiter, startIndex + length)
  228.             else:
  229.                 self.setFormat(startIndex, len(text) - startIndex, FORMAT_COMMENT)
  230.                 self.setCurrentBlockState(1) # Set state to inside block comment
  231.                 break # No more pairs starting in this line
  232.         for pattern, format in self._rules:
  233.              if format == FORMAT_COMMENT and (pattern.pattern.startswith(re.escape('/*')) or pattern.pattern.startswith(re.escape('<!--'))):
  234.                   continue
  235.              if format == FORMAT_COMMENT and pattern.pattern.startswith('//') and self.currentBlockState() == 1:
  236.                   continue
  237.              for match in pattern.finditer(text):
  238.                 start, end = match.span()
  239.                 self.setFormat(start, end, format)
  240. class CustomFileSystemModel(QFileSystemModel):
  241.     def __init__(self, parent=None):
  242.         super().__init__(parent)
  243.         self.icon_map = {
  244.             '.py': 'fa5s.file-code',
  245.             '.js': 'fa5s.file-code',
  246.             '.json': 'fa5s.file-code',  # JSON też jako plik kodu
  247.             '.html': 'fa5s.file-code',
  248.             '.css': 'fa5s.file-code',
  249.             '.ini': 'fa5s.file-alt',  # Plik konfiguracji
  250.             '.txt': 'fa5s.file-alt',
  251.             '.md': 'fa5s.file-alt',
  252.             '.c': 'fa5s.file-code',
  253.             '.cpp': 'fa5s.file-code',
  254.             '.h': 'fa5s.file-code',
  255.             '.hpp': 'fa5s.file-code',
  256.         }
  257.         self.folder_icon_name = 'fa5s.folder'
  258.         self.default_file_icon_name = 'fa5s.file'
  259.         self._has_qtawesome = qta is not None
  260.     def rename(self, index, new_name):
  261.         """Zmienia nazwę pliku lub folderu reprezentowanego przez podany index."""
  262.         if not index.isValid():
  263.             return False
  264.         old_path = self.filePath(index)
  265.         new_path = os.path.join(os.path.dirname(old_path), new_name)
  266.         try:
  267.             os.rename(old_path, new_path)
  268.             self.refresh()  # Możliwe, że trzeba wymusić odświeżenie modelu
  269.             return True
  270.         except Exception as e:
  271.             print(f"Błąd podczas zmiany nazwy: {e}")
  272.             return False
  273.     def data(self, index, role=Qt.ItemDataRole.DisplayRole):
  274.         if not index.isValid():
  275.             return None
  276.         if role == Qt.ItemDataRole.DecorationRole:
  277.             file_info = self.fileInfo(index)
  278.             if file_info.isDir():
  279.                 if self._has_qtawesome:
  280.                     return qta.icon(self.folder_icon_name)
  281.                 else:
  282.                     return super().data(index, role)
  283.             elif file_info.isFile():
  284.                 extension = file_info.suffix().lower()
  285.                 dotted_extension = '.' + extension
  286.                 if dotted_extension in self.icon_map and self._has_qtawesome:
  287.                     return qta.icon(self.icon_map[dotted_extension])
  288.                 else:
  289.                     if self._has_qtawesome:
  290.                         return qta.icon(self.default_file_icon_name)
  291.                     else:
  292.                         return super().data(index, role)
  293.         return super().data(index, role)
  294.     def refresh(self, *args):
  295.         self.setRootPath(self.rootPath())
  296. class NewProjectDialog(QDialog):
  297.     def __init__(self, projects_dir, parent=None):
  298.         super().__init__(parent)
  299.         self.setWindowTitle("Nowy projekt")
  300.         self.projects_dir = projects_dir
  301.         self.setModal(True)
  302.         layout = QFormLayout(self)
  303.         self.name_edit = QLineEdit()
  304.         layout.addRow("Nazwa projektu:", self.name_edit)
  305.         self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  306.         self.button_box.button(QDialogButtonBox.StandardButton.Ok).setText("Utwórz")  # Zmieniamy tekst przycisku
  307.         self.button_box.accepted.connect(self.accept)
  308.         self.button_box.rejected.connect(self.reject)
  309.         layout.addRow(self.button_box)
  310.         self.name_edit.textChanged.connect(self._validate_name)
  311.         self.name_edit.textChanged.emit(self.name_edit.text())  # Wywołaj walidację od razu
  312.     def _validate_name(self, name):
  313.         """Sprawdza poprawność nazwy projektu."""
  314.         name = name.strip()
  315.         is_empty = not name
  316.         is_valid_chars = re.fullmatch(r'[a-zA-Z0-9_-]+', name) is not None or name == ""
  317.         full_path = os.path.join(self.projects_dir, name)
  318.         dir_exists = os.path.exists(full_path)
  319.         enable_ok = not is_empty and is_valid_chars and not dir_exists
  320.         self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(enable_ok)
  321.         if is_empty:
  322.             self.name_edit.setToolTip("Nazwa projektu nie może być pusta.")
  323.         elif not is_valid_chars:
  324.             self.name_edit.setToolTip("Nazwa projektu może zawierać tylko litery, cyfry, podkreślenia i myślniki.")
  325.         elif dir_exists:
  326.             self.name_edit.setToolTip(f"Projekt o nazwie '{name}' już istnieje w:\n{self.projects_dir}")
  327.         else:
  328.             self.name_edit.setToolTip(f"Katalog projektu zostanie utworzony w:\n{full_path}")
  329.         if not enable_ok and not is_empty:
  330.             self.name_edit.setStyleSheet("background-color: #ffe0e0;")  # Jasnoczerwony
  331.         else:
  332.             self.name_edit.setStyleSheet("")
  333.     def get_project_name(self):
  334.         return self.name_edit.text().strip()
  335.     def get_project_path(self):
  336.         return os.path.join(self.projects_dir, self.get_project_name())
  337. class NewItemDialog(QDialog):
  338.     def __init__(self, parent_dir, is_folder=False, parent=None):
  339.         super().__init__(parent)
  340.         self.setWindowTitle("Nowy folder" if is_folder else "Nowy plik")
  341.         self.parent_dir = parent_dir
  342.         self.is_folder = is_folder
  343.         self.setModal(True)
  344.         layout = QFormLayout(self)
  345.         self.item_type_label = "Nazwa folderu:" if is_folder else "Nazwa pliku:"
  346.         self.name_edit = QLineEdit()
  347.         layout.addRow(self.item_type_label, self.name_edit)
  348.         self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  349.         self.button_box.accepted.connect(self.accept)
  350.         self.button_box.rejected.connect(self.reject)
  351.         layout.addRow(self.button_box)
  352.         self.name_edit.textChanged.connect(self._validate_name)
  353.         self.name_edit.textChanged.emit(self.name_edit.text()) # Initial validation
  354.     def _validate_name(self, name):
  355.         """Sprawdza poprawność nazwy pliku/folderu."""
  356.         name = name.strip()
  357.         is_empty = not name
  358.         illegal_chars_pattern = r'[<>:"/\\|?*\x00-\x1F]'
  359.         is_valid_chars = re.search(illegal_chars_pattern, name) is None
  360.         full_path = os.path.join(self.parent_dir, name)
  361.         item_exists = os.path.exists(full_path)
  362.         enable_create = not is_empty and is_valid_chars and not item_exists
  363.         self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(enable_create)
  364.         if is_empty:
  365.              self.name_edit.setToolTip(f"{self.item_type_label} nie może być pusta.")
  366.         elif not is_valid_chars:
  367.              self.name_edit.setToolTip("Nazwa zawiera niedozwolone znaki.")
  368.         elif item_exists:
  369.              self.name_edit.setToolTip(f"Element o nazwie '{name}' już istnieje w:\n{self.parent_dir}")
  370.         else:
  371.              self.name_edit.setToolTip("")
  372.         if not enable_create and not is_empty:
  373.              self.name_edit.setStyleSheet("background-color: #ffe0e0;")
  374.         else:
  375.              self.name_edit.setStyleSheet("")
  376.     def get_item_name(self):
  377.         return self.name_edit.text().strip()
  378. class RenameItemDialog(QDialog):
  379.     def __init__(self, current_path, parent=None):
  380.         super().__init__(parent)
  381.         self.current_path = current_path
  382.         self.is_folder = os.path.isdir(current_path)
  383.         old_name = os.path.basename(current_path)
  384.         self.setWindowTitle("Zmień nazwę")
  385.         layout = QVBoxLayout(self)
  386.         self.label = QLabel(f"Nowa nazwa dla '{old_name}':", self)
  387.         layout.addWidget(self.label)
  388.         self.line_edit = QLineEdit(old_name, self)
  389.         layout.addWidget(self.line_edit)
  390.         self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self)
  391.         layout.addWidget(self.button_box)
  392.         self.button_box.accepted.connect(self.accept)
  393.         self.button_box.rejected.connect(self.reject)
  394.         self.line_edit.textChanged.connect(self._validate_name)
  395.         self._validate_name(self.line_edit.text())  # Od razu sprawdź
  396.     def _validate_name(self, name):
  397.         name = name.strip()
  398.         is_empty = not name
  399.         illegal_chars_pattern = r'[<>:"/\\|?*\x00-\x1F]'
  400.         is_valid_chars = re.search(illegal_chars_pattern, name) is None
  401.         old_name = os.path.basename(self.current_path)
  402.         is_same_name = name == old_name
  403.         parent_dir = os.path.dirname(self.current_path)
  404.         new_full_path = os.path.join(parent_dir, name)
  405.         item_exists_at_new_path = os.path.exists(new_full_path)
  406.         enable_ok = not is_empty and is_valid_chars and (is_same_name or not item_exists_at_new_path)
  407.         self.button_box.button(QDialogButtonBox.Ok).setEnabled(enable_ok)
  408.     def get_new_name(self):
  409.         return self.line_edit.text().strip()
  410. class SettingsDialog(QDialog):
  411.     def __init__(self, settings, parent=None):
  412.         super().__init__(parent)
  413.         self.setWindowTitle("Ustawienia")
  414.         self._settings = settings # Pracujemy na kopii
  415.         self.setModal(True)
  416.         layout = QFormLayout(self)
  417.         self.theme_combo = QComboBox()
  418.         self.theme_combo.addItems(["light", "dark"])
  419.         self.theme_combo.setCurrentText(self._settings.get("theme", "light"))
  420.         layout.addRow("Motyw:", self.theme_combo)
  421.         self.python_path_edit = QLineEdit(self._settings.get("python_path", ""))
  422.         self.python_path_button = QPushButton("Przeglądaj...")
  423.         python_path_layout = QHBoxLayout()
  424.         python_path_layout.addWidget(self.python_path_edit)
  425.         python_path_layout.addWidget(self.python_path_button)
  426.         layout.addRow("Ścieżka Python:", python_path_layout)
  427.         self.python_path_button.clicked.connect(lambda: self._browse_file(self.python_path_edit))
  428.         self.node_path_edit = QLineEdit(self._settings.get("node_path", ""))
  429.         self.node_path_button = QPushButton("Przeglądaj...")
  430.         node_path_layout = QHBoxLayout()
  431.         node_path_layout.addWidget(self.node_path_edit)
  432.         node_path_layout.addWidget(self.node_path_button)
  433.         layout.addRow("Ścieżka Node.js:", node_path_layout)
  434.         self.node_path_button.clicked.connect(lambda: self._browse_file(self.node_path_edit))
  435.         self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  436.         self.button_box.accepted.connect(self.accept)
  437.         self.button_box.rejected.connect(self.reject)
  438.         layout.addRow(self.button_box)
  439.     def _browse_file(self, line_edit):
  440.         """Otwiera dialog wyboru pliku dla pola QLineEdit."""
  441.         start_dir = os.path.dirname(line_edit.text()) if os.path.dirname(line_edit.text()) else os.path.expanduser("~")
  442.         if platform.system() == "Windows":
  443.              filter_str = "Wykonywalne pliki (*.exe *.bat *.cmd);;Wszystkie pliki (*)"
  444.         else:
  445.              filter_str = "Wszystkie pliki (*)" # Na Linux/macOS pliki wykonywalne nie mają konkretnego rozszerzenia
  446.         file_path, _ = QFileDialog.getOpenFileName(self, "Wybierz plik", start_dir, filter_str)
  447.         if file_path:
  448.             line_edit.setText(os.path.normpath(file_path)) # Znormalizuj ścieżkę przed ustawieniem
  449.     def get_settings(self):
  450.         self._settings["theme"] = self.theme_combo.currentText()
  451.         self._settings["python_path"] = self.python_path_edit.text().strip()
  452.         self._settings["node_path"] = self.node_path_edit.text().strip()
  453.         return self._settings
  454.     def layout(self):
  455.         return super().layout()
  456. class IDEWindow(QMainWindow):
  457.     def __init__(self):
  458.         super().__init__()
  459.         self.settings = {} # Słownik na ustawienia aplikacji
  460.         self.recents = {"last_project_dir": None, "open_files": []} # Słownik na historię
  461.         self._load_app_state() # Wczytaj stan aplikacji (ustawienia i historię)
  462.         self.setWindowTitle("Proste IDE - Bez nazwy")
  463.         self.setGeometry(100, 100, 1200, 800)
  464.         if qta:
  465.              self.setWindowIcon(qta.icon('fa5s.code'))
  466.         else:
  467.              self.setWindowIcon(QIcon.fromTheme("applications-development")) # Przykładowa ikona systemowa
  468.         self.current_project_dir = self.recents.get("last_project_dir")
  469.         self.open_files = {} # {ścieżka_pliku: edytor_widget}
  470.         self.base_editor_font = QFont("Courier New", 10) # Ustaw domyślną czcionkę początkową
  471.         self._setup_ui()
  472.         self._setup_menu()
  473.         self._setup_toolbar()
  474.         self._setup_status_bar()
  475.         self._setup_connections()
  476.         self._apply_theme(self.settings.get("theme", "light"))
  477.         self._apply_editor_font_size() # Zastosuj rozmiar czcionki do wszystkich otwartych edytorów (choć na start puste)
  478.         self.process = QProcess(self) # Proces do uruchamiania kodu
  479.         self.process.readyReadStandardOutput.connect(self._handle_stdout)
  480.         self.process.readyReadStandardError.connect(self._handle_stderr)
  481.         self.process.finished.connect(self._handle_process_finished)
  482.         self.node_scripts = {} # Słownik na skrypty z package.json
  483.         QTimer.singleShot(10, self._initial_setup)
  484.     def _setup_ui(self):
  485.         """Konfiguracja głównych elementów interfejsu."""
  486.         central_widget = QWidget()
  487.         main_layout = QVBoxLayout(central_widget)
  488.         main_layout.setContentsMargins(0, 0, 0, 0)
  489.         self.main_splitter = QSplitter(Qt.Orientation.Horizontal)
  490.         main_layout.addWidget(self.main_splitter)
  491.         self.project_model = CustomFileSystemModel()  # Użyj niestandardowego modelu z ikonami
  492.         self.project_model.setFilter(QDir.Filter.AllDirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot)
  493.         self.project_tree = QTreeView()
  494.         self.project_tree.setModel(self.project_model)
  495.         self.project_tree.setHeaderHidden(True)
  496.         self.project_tree.hideColumn(1)
  497.         self.project_tree.hideColumn(2)
  498.         self.project_tree.hideColumn(3)
  499.         self.project_tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
  500.         self.main_splitter.addWidget(self.project_tree)
  501.         self.right_splitter = QSplitter(Qt.Orientation.Vertical)
  502.         self.main_splitter.addWidget(self.right_splitter)
  503.         self.tab_widget = QTabWidget()
  504.         self.tab_widget.setTabsClosable(True)
  505.         self.tab_widget.setMovable(True)
  506.         self.right_splitter.addWidget(self.tab_widget)
  507.         self.console_widget = QWidget()
  508.         self.console_layout = QVBoxLayout(self.console_widget)
  509.         self.console_layout.setContentsMargins(0, 0, 0, 0)
  510.         self.console = QPlainTextEdit()
  511.         self.console.setReadOnly(True)
  512.         self.console.setFont(self.base_editor_font)
  513.         self.console_layout.addWidget(self.console, 1) # Rozciągnij pole konsoli
  514.         self.console_input = QLineEdit()
  515.         self.console_input.setPlaceholderText("Wpisz polecenie...")
  516.         self.console_layout.addWidget(self.console_input, 0) # Nie rozciągaj pola wprowadzania
  517.         self.console_buttons_layout = QHBoxLayout()
  518.         self.console_buttons_layout.setContentsMargins(0, 0, 0, 0)
  519.         self.console_buttons_layout.addStretch(1)
  520.         self.clear_console_button = QPushButton("Wyczyść konsolę")
  521.         self.console_buttons_layout.addWidget(self.clear_console_button)
  522.         self.copy_console_button = QPushButton("Skopiuj")
  523.         self.console_buttons_layout.addWidget(self.copy_console_button)
  524.         self.console_layout.addLayout(self.console_buttons_layout)
  525.         self.right_splitter.addWidget(self.console_widget)
  526.         self.main_splitter.setSizes([200, 800])
  527.         self.right_splitter.setSizes([600, 200])
  528.         self.setCentralWidget(central_widget)
  529.         self.action_toggle_tree = QAction("Pokaż/Ukryj drzewko", self)
  530.         self.action_toggle_tree.setCheckable(True)
  531.         self.action_toggle_tree.setChecked(True)
  532.         self.action_toggle_tree.triggered.connect(self._toggle_tree_panel)
  533.         self.action_toggle_console = QAction("Pokaż/Ukryj konsolę", self)
  534.         self.action_toggle_console.setCheckable(True)
  535.         self.action_toggle_console.setChecked(True)
  536.         self.action_toggle_console.triggered.connect(self._toggle_console_panel)
  537.         self._apply_view_settings()
  538.     def _toggle_tree_panel(self, checked):
  539.         self.main_splitter.widget(0).setVisible(checked)
  540.     def _toggle_console_panel(self, checked):
  541.         self.right_splitter.widget(1).setVisible(checked)
  542.     def _setup_menu(self):
  543.         """Konfiguracja paska menu."""
  544.         menu_bar = self.menuBar()
  545.         file_menu = menu_bar.addMenu("&Plik") # & dodaje skrót klawiszowy Alt+P
  546.         self.action_new_project = QAction(qta.icon('fa5s.folder-plus') if qta else QIcon(), "&Nowy projekt...", self)
  547.         self.action_new_project.triggered.connect(self._new_project)
  548.         file_menu.addAction(self.action_new_project)
  549.         self.action_open_folder = QAction(qta.icon('fa5s.folder-open') if qta else QIcon(), "Otwórz &folder projektu...", self)
  550.         self.action_open_folder.triggered.connect(self._open_project_folder) # Użyj bezpośredniego połączenia
  551.         file_menu.addAction(self.action_open_folder)
  552.         self.action_open_file = QAction(qta.icon('fa5s.file-code') if qta else QIcon(), "Otwórz &plik...", self)
  553.         self.action_open_file.triggered.connect(self._open_file_dialog)
  554.         file_menu.addAction(self.action_open_file)
  555.         file_menu.addSeparator()
  556.         self.recent_files_menu = QMenu("Ostatnio otwierane", self)
  557.         file_menu.addMenu(self.recent_files_menu)
  558.         file_menu.addSeparator()
  559.         self.action_save = QAction(qta.icon('fa5s.save') if qta else QIcon(), "&Zapisz", self)
  560.         self.action_save.setShortcut(QKeySequence.StandardKey.Save)
  561.         self.action_save.triggered.connect(self._save_current_file)
  562.         file_menu.addAction(self.action_save)
  563.         self.action_save_as = QAction(qta.icon('fa5s.file-export') if qta else QIcon(), "Zapisz &jako...", self)
  564.         self.action_save_as.setShortcut(QKeySequence.StandardKey.SaveAs)
  565.         self.action_save_as.triggered.connect(self._save_current_file_as)
  566.         file_menu.addAction(self.action_save_as)
  567.         self.action_save_all = QAction(qta.icon('fa5s.save') if qta else QIcon(), "Zapisz wszys&tko", self)
  568.         self.action_save_all.setShortcut(QKeySequence("Ctrl+Shift+S")) # Standardowy skrót
  569.         self.action_save_all.triggered.connect(self._save_all_files)
  570.         file_menu.addAction(self.action_save_all)
  571.         file_menu.addSeparator()
  572.         self.action_close_file = QAction(qta.icon('fa5s.window-close') if qta else QIcon(), "Zamknij ak&tualny plik", self)
  573.         self.action_close_file.triggered.connect(self._close_current_tab)
  574.         file_menu.addAction(self.action_close_file)
  575.         file_menu.addSeparator()
  576.         self.action_exit = QAction(qta.icon('fa5s.door-open') if qta else QIcon(), "&Zakończ", self)
  577.         self.action_exit.setShortcut(QKeySequence.StandardKey.Quit)
  578.         self.action_exit.triggered.connect(self.close)
  579.         file_menu.addAction(self.action_exit)
  580.         edit_menu = menu_bar.addMenu("&Edycja")
  581.         view_menu = menu_bar.addMenu("&Widok")
  582.         self.action_toggle_tree = QAction(qta.icon('fa5s.sitemap') if qta else QIcon(), "Pokaż &drzewko plików", self)
  583.         self.action_toggle_tree.setCheckable(True)
  584.         self.action_toggle_tree.setChecked(self.settings.get("show_tree", True)) # Ustaw stan z ustawień
  585.         self.action_toggle_tree.triggered.connect(self._toggle_tree_view)
  586.         view_menu.addAction(self.action_toggle_tree)
  587.         self.action_toggle_console = QAction(qta.icon('fa5s.terminal') if qta else QIcon(), "Pokaż &konsolę", self)
  588.         self.action_toggle_console.setCheckable(True)
  589.         self.action_toggle_console.setChecked(self.settings.get("show_console", True)) # Ustaw stan z ustawień
  590.         self.action_toggle_console.triggered.connect(self._toggle_console)
  591.         view_menu.addAction(self.action_toggle_console)
  592.         search_menu = menu_bar.addMenu("&Wyszukaj")
  593.         self.action_find = QAction(qta.icon('fa5s.search') if qta else QIcon(), "&Znajdź...", self)
  594.         self.action_find.setShortcut(QKeySequence.StandardKey.Find)
  595.         self.action_find.triggered.connect(self._show_find_bar)
  596.         search_menu.addAction(self.action_find)
  597.         run_menu = menu_bar.addMenu("&Uruchom")
  598.         self.action_run_file = QAction(qta.icon('fa5s.play') if qta else QIcon(), "&Uruchom aktualny plik", self)
  599.         self.action_run_file.setShortcut(QKeySequence("F5")) # Przykładowy skrót
  600.         self.action_run_file.triggered.connect(self._run_current_file)
  601.         run_menu.addAction(self.action_run_file)
  602.         tools_menu = menu_bar.addMenu("&Narzędzia")
  603.         self.action_settings = QAction(qta.icon('fa5s.cog') if qta else QIcon(), "&Ustawienia...", self)
  604.         self.action_settings.triggered.connect(self._show_settings_dialog)
  605.         tools_menu.addAction(self.action_settings)
  606.         help_menu = menu_bar.addMenu("&Pomoc")
  607.         self.action_about = QAction(qta.icon('fa5s.info-circle') if qta else QIcon(), "&O programie...", self)
  608.         self.action_about.triggered.connect(self._show_about_dialog)
  609.         help_menu.addAction(self.action_about)
  610.     def _setup_toolbar(self):
  611.         """Konfiguracja paska narzędzi."""
  612.         toolbar = self.addToolBar("Główne narzędzia")
  613.         toolbar.setMovable(False) # Nie można go przesuwać
  614.         toolbar.setIconSize(QSize(16, 16)) # Rozmiar ikon
  615.         toolbar.addAction(self.action_new_project) # Nowy projekt
  616.         toolbar.addAction(self.action_open_folder) # Otwórz folder
  617.         toolbar.addAction(self.action_open_file) # Otwórz plik
  618.         toolbar.addSeparator()
  619.         toolbar.addAction(self.action_save) # Zapisz
  620.         toolbar.addAction(self.action_save_all) # Zapisz wszystko
  621.         toolbar.addSeparator()
  622.         self.run_toolbutton = QToolButton(self)
  623.         self.run_toolbutton.setDefaultAction(self.action_run_file)
  624.         self.run_toolbutton.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
  625.         toolbar.addWidget(self.run_toolbutton) # Dodaj QToolButton do toolbara
  626.         toolbar.addSeparator()
  627.         self.search_input = QLineEdit(self)
  628.         self.search_input.setPlaceholderText("Szukaj w pliku...")
  629.         self.search_input.setClearButtonEnabled(True) # Przycisk czyszczenia
  630.         self.search_input.returnPressed.connect(lambda: self._find_text(self.search_input.text(), 'next')) # Szukaj po wciśnięciu Enter
  631.         self.find_next_button = QPushButton("Znajdź dalej")
  632.         self.find_next_button.clicked.connect(lambda: self._find_text(self.search_input.text(), 'next'))
  633.         self.find_prev_button = QPushButton("Znajdź poprzedni")
  634.         self.find_prev_button.clicked.connect(lambda: self._find_text(self.search_input.text(), 'previous'))
  635.         toolbar.addWidget(self.search_input)
  636.         toolbar.addWidget(self.find_next_button)
  637.         toolbar.addWidget(self.find_prev_button)
  638.         self.search_input.setVisible(False)
  639.         self.find_next_button.setVisible(False)
  640.         self.find_prev_button.setVisible(False)
  641.     def _setup_status_bar(self):
  642.         """Konfiguracja paska stanu."""
  643.         self.statusBar().showMessage("Gotowy.")
  644.     def _setup_connections(self):
  645.         """Połączenie sygnałów ze slotami."""
  646.         self.project_tree.doubleClicked.connect(self._handle_tree_item_double_click)
  647.         self.tab_widget.tabCloseRequested.connect(self._close_tab_by_index)
  648.         self.tab_widget.currentChanged.connect(self._handle_tab_change)
  649.         self.project_tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) # Upewnij się, że policy jest ustawione
  650.         self.project_tree.customContextMenuRequested.connect(self._show_project_tree_context_menu)
  651.         self.clear_console_button.clicked.connect(self.console.clear)
  652.         self.copy_console_button.clicked.connect(self._copy_console)
  653.         self.console_input.returnPressed.connect(self._run_console_command)
  654.     def _initial_setup(self):
  655.         """Wstępna konfiguracja po uruchomieniu, w tym ładowanie ostatniego stanu."""
  656.         initial_dir = self.recents.get("last_project_dir")
  657.         if not initial_dir or not os.path.isdir(initial_dir):
  658.              initial_dir = PROJECTS_DIR # Użyj domyślnego katalogu projects
  659.              os.makedirs(PROJECTS_DIR, exist_ok=True)
  660.         if os.path.isdir(initial_dir):
  661.              self._open_project_folder(initial_dir)
  662.         else:
  663.              self.statusBar().showMessage(f"Brak domyślnego katalogu projektu. Otwórz folder ręcznie lub utwórz nowy.")
  664.              self.project_model.setRootPath("") # Brak roota w drzewku
  665.              self.current_project_dir = None # Resetuj current_project_dir
  666.              self._update_run_button_menu()
  667.         recent_files = self.recents.get("open_files", [])
  668.         QTimer.singleShot(200, lambda: self._reopen_files(recent_files)) # Krótsze opóźnienie
  669.         self._update_recent_files_menu() # Uaktualnij menu ostatnio otwieranych
  670.     def _load_app_state(self):
  671.         """Wczytuje ustawienia i historię z plików JSON."""
  672.         try:
  673.             if os.path.exists(SETTINGS_FILE):
  674.                 with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
  675.                     loaded_settings = json.load(f)
  676.                     self.settings = {
  677.                         "theme": loaded_settings.get("theme", "light"),
  678.                         "python_path": loaded_settings.get("python_path", ""),
  679.                         "node_path": loaded_settings.get("node_path", ""),
  680.                         "show_tree": loaded_settings.get("show_tree", True),
  681.                         "show_console": loaded_settings.get("show_console", True),
  682.                         "editor_font_size": loaded_settings.get("editor_font_size", 10)
  683.                     }
  684.             else:
  685.                 self.settings = {
  686.                     "theme": "light",
  687.                     "python_path": "",
  688.                     "node_path": "",
  689.                     "show_tree": True,
  690.                     "show_console": True,
  691.                     "editor_font_size": 10
  692.                 }
  693.             if os.path.exists(RECENTS_FILE):
  694.                  with open(RECENTS_FILE, 'r', encoding='utf-8') as f:
  695.                      loaded_recents = json.load(f)
  696.                      self.recents = {
  697.                          "last_project_dir": loaded_recents.get("last_project_dir"),
  698.                          "open_files": loaded_recents.get("open_files", [])
  699.                      }
  700.             else:
  701.                  self.recents = {"last_project_dir": None, "open_files": []}
  702.         except (json.JSONDecodeError, Exception) as e:
  703.             print(f"Błąd podczas wczytywania stanu aplikacji: {e}", file=sys.stderr)
  704.             self.settings = {
  705.                 "theme": "light",
  706.                 "python_path": "",
  707.                 "node_path": "",
  708.                 "show_tree": True,
  709.                 "show_console": True,
  710.                 "editor_font_size": 10
  711.             }
  712.             self.recents = {"last_project_dir": None, "open_files": []}
  713.     def _save_app_state(self):
  714.         """Zapisuje ustawienia i historię do plików JSON."""
  715.         try:
  716.             self.recents["open_files"] = list(self.open_files.keys())
  717.             if self.current_project_dir and os.path.isdir(self.current_project_dir):
  718.                  self.recents["last_project_dir"] = os.path.normpath(self.current_project_dir) # Znormalizuj przed zapisem
  719.             else:
  720.                  self.recents["last_project_dir"] = None # Nie zapisuj jeśli nie ma folderu
  721.             with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
  722.                 json.dump(self.settings, f, indent=4)
  723.             with open(RECENTS_FILE, 'w', encoding='utf-8') as f:
  724.                  normalized_open_files = [os.path.normpath(p) for p in self.recents["open_files"]]
  725.                  unique_open_files = []
  726.                  for p in normalized_open_files:
  727.                      if p not in unique_open_files:
  728.                          unique_open_files.append(p)
  729.                  self.recents["open_files"] = unique_open_files[:20] # Ostatnie 20 unikalnych
  730.                  json.dump(self.recents, f, indent=4)
  731.         except Exception as e:
  732.             print(f"Błąd podczas zapisu stanu aplikacji: {e}", file=sys.stderr)
  733.     def closeEvent(self, event):
  734.         """Obsługa zdarzenia zamykania okna."""
  735.         unsaved_files = [path for path, editor in self.open_files.items() if editor.document().isModified()]
  736.         if unsaved_files:
  737.             msg_box = QMessageBox(self)
  738.             msg_box.setIcon(QMessageBox.Icon.Warning)
  739.             msg_box.setWindowTitle("Niezapisane zmiany")
  740.             msg_box.setText(f"Masz niezapisane zmiany w {len(unsaved_files)} plikach.\nCzy chcesz zapisać przed zamknięciem?")
  741.             msg_box.setStandardButtons(QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
  742.             msg_box.setDefaultButton(QMessageBox.StandardButton.Save)
  743.             reply = msg_box.exec()
  744.             if reply == QMessageBox.StandardButton.Save:
  745.                 if self._save_all_files():
  746.                      self._save_app_state() # Zapisz stan po pomyślnym zapisie plików
  747.                      event.accept() # Akceptuj zamknięcie
  748.                 else:
  749.                      event.ignore() # Nie zamykaj, jeśli zapis się nie udał
  750.             elif reply == QMessageBox.StandardButton.Discard:
  751.                 for i in range(self.tab_widget.count() - 1, -1, -1):
  752.                      widget = self.tab_widget.widget(i)
  753.                      if hasattr(widget, 'document'):
  754.                           widget.document().setModified(False)
  755.                      self._close_tab_by_index(i) # Ta metoda usunie z open_files i recents
  756.                 self._save_app_state() # Zapisz stan (lista otwartych plików będzie aktualna)
  757.                 event.accept() # Akceptuj zamknięcie
  758.             else:
  759.                 event.ignore() # Ignoruj zamknięcie
  760.         else:
  761.             self._save_app_state() # Zapisz stan, bo nie ma niezapisanych plików
  762.             event.accept() # Akceptuj zamknięcie
  763.     def _new_project(self):
  764.         """Tworzy nowy katalog projektu."""
  765.         dialog = NewProjectDialog(PROJECTS_DIR, self)
  766.         if dialog.exec() == QDialog.DialogCode.Accepted:
  767.             project_name = dialog.get_project_name()
  768.             project_path = dialog.get_project_path()
  769.             try:
  770.                 if os.path.exists(project_path):
  771.                     QMessageBox.warning(self, "Projekt już istnieje", f"Projekt o nazwie '{project_name}' już istnieje.")
  772.                     return
  773.                 os.makedirs(project_path)
  774.                 self.statusBar().showMessage(f"Utworzono nowy projekt: {project_name}")
  775.                 self._open_project_folder(project_path)
  776.             except OSError as e:
  777.                 QMessageBox.critical(self, "Błąd tworzenia projektu", f"Nie można utworzyć katalogu projektu:\n{e}")
  778.                 self.statusBar().showMessage("Błąd tworzenia projektu.")
  779.             except Exception as e:
  780.                  QMessageBox.critical(self, "Nieoczekiwany błąd", f"Wystąpił nieoczekiwany błąd:\n{e}")
  781.                  self.statusBar().showMessage("Nieoczekiwany błąd.")
  782.     def _open_project_folder(self, path=None):
  783.         """Otwiera okno dialogowe wyboru folderu projektu lub otwiera wskazany folder."""
  784.         if path is None:
  785.             start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
  786.             dialog_path = QFileDialog.getExistingDirectory(self, "Otwórz folder projektu", start_path)
  787.             if not dialog_path:
  788.                 return
  789.             path = dialog_path
  790.         path = os.path.normpath(path)
  791.         if not os.path.isdir(path):
  792.             QMessageBox.critical(self, "Błąd", f"Wybrana ścieżka nie jest katalogiem lub nie istnieje:\n{path}")
  793.             self.statusBar().showMessage(f"Błąd: Nie można otworzyć folderu: {path}")
  794.             return
  795.         if self.current_project_dir and self.current_project_dir != path:
  796.              unsaved_files_count = sum(1 for editor in self.open_files.values() if editor.document().isModified())
  797.              if unsaved_files_count > 0:
  798.                   reply = QMessageBox.question(self, "Niezapisane zmiany",
  799.                                                f"Obecny projekt ma {unsaved_files_count} niezapisanych plików.\n"
  800.                                                "Czy chcesz zapisać zmiany przed otwarciem nowego folderu?",
  801.                                                QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
  802.                   if reply == QMessageBox.StandardButton.Cancel:
  803.                        self.statusBar().showMessage("Otwieranie folderu anulowane.")
  804.                        return # Anuluj operację
  805.                   if reply == QMessageBox.StandardButton.Save:
  806.                        if not self._save_all_files(): # Ta metoda obsłuży Save As dla nowych plików
  807.                             self.statusBar().showMessage("Otwieranie folderu anulowane (błąd zapisu).")
  808.                             return # Anuluj operację
  809.              self._close_all_files() # Ta metoda już nie pyta o zapis
  810.         self.current_project_dir = path
  811.         self.project_model.setRootPath(path)
  812.         root_index = self.project_model.index(path)
  813.         if not root_index.isValid():
  814.              QMessageBox.critical(self, "Błąd", f"Nie można ustawić katalogu głównego drzewka dla ścieżki:\n{path}\n"
  815.                                                  "Sprawdź uprawnienia lub czy ścieżka jest dostępna dla systemu plików.")
  816.              self.statusBar().showMessage(f"Błąd ustawienia katalogu głównego: {path}")
  817.              self.project_tree.setRootIndex(self.project_model.index("")) # Wyczyść roota
  818.              self.current_project_dir = None # Resetuj current_project_dir
  819.              self.recents["open_files"] = [p for p in self.recents["open_files"] if not os.path.normpath(p).startswith(os.path.normpath(path) + os.sep)]
  820.              self._update_recent_files_menu()
  821.              self._save_app_state() # Zapisz zaktualizowany stan
  822.              self._update_run_button_menu() # Uaktualnij menu uruchamiania (brak projektu)
  823.              return
  824.         self.project_tree.setRootIndex(root_index)
  825.         self.setWindowTitle(f"Proste IDE - {os.path.basename(path)}")
  826.         self.statusBar().showMessage(f"Otwarto folder: {path}")
  827.         self._check_package_json(path)
  828.         self.recents["last_project_dir"] = path
  829.         self._save_app_state() # Zapisz, żeby zapamiętać ostatni folder
  830.     def _close_all_files(self):
  831.          """Zamyka wszystkie otwarte zakładki edytora bez pytania o zapis."""
  832.          for file_path in list(self.open_files.keys()):
  833.               editor_widget = self.open_files.get(file_path)
  834.               if editor_widget:
  835.                    tab_index = self.tab_widget.indexOf(editor_widget)
  836.                    if tab_index != -1:
  837.                         if hasattr(editor_widget, 'document'):
  838.                             editor_widget.document().setModified(False)
  839.                         self.tab_widget.removeTab(tab_index)
  840.                         if file_path in self.open_files:
  841.                              del self.open_files[file_path]
  842.          self.recents["open_files"] = [] # Wyczyść listę otwartych plików
  843.          self._update_recent_files_menu() # Uaktualnij menu
  844.     def _open_file_dialog(self):
  845.         """Otwiera okno dialogowe wyboru pliku i otwiera go w edytorze."""
  846.         start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
  847.         file_path, _ = QFileDialog.getOpenFileName(self, "Otwórz plik", start_path, "Wszystkie pliki (*);;Pliki Pythona (*.py);;Pliki JavaScript (*.js);;Pliki HTML (*.html);;Pliki CSS (*.css);;Pliki C++ (*.c *.cpp *.h *.hpp);;Pliki INI (*.ini);;Pliki JSON (*.json)")
  848.         if file_path:
  849.             self._open_file(file_path)
  850.     def _open_file(self, file_path):
  851.         """Otwiera wskazany plik w nowej zakładce edytora."""
  852.         file_path = os.path.normpath(file_path)
  853.         if not os.path.exists(file_path) or not os.path.isfile(file_path):
  854.             self.statusBar().showMessage(f"Błąd: Plik nie istnieje lub nie jest plikiem: {file_path}")
  855.             if file_path in self.recents["open_files"]:
  856.                  self.recents["open_files"].remove(file_path)
  857.                  self._update_recent_files_menu() # Uaktualnij menu
  858.                  self._save_app_state() # Zapisz stan
  859.             return
  860.         if file_path in self.open_files:
  861.             index = -1
  862.             for i in range(self.tab_widget.count()):
  863.                  widget = self.tab_widget.widget(i)
  864.                  if self.open_files.get(file_path) is widget:
  865.                       index = i
  866.                       break
  867.             if index != -1:
  868.                 self.tab_widget.setCurrentIndex(index)
  869.                 self.statusBar().showMessage(f"Plik {os.path.basename(file_path)} jest już otwarty.")
  870.                 if file_path in self.recents["open_files"]:
  871.                      self.recents["open_files"].remove(file_path)
  872.                      self.recents["open_files"].insert(0, file_path)
  873.                      self._update_recent_files_menu()
  874.                      self._save_app_state()
  875.                 return
  876.             else:
  877.                 print(f"Warning: File {file_path} found in open_files but not in tab widget.", file=sys.stderr)
  878.         try:
  879.             content = ""
  880.             try:
  881.                 with open(file_path, 'r', encoding='utf-8') as f:
  882.                     content = f.read()
  883.             except UnicodeDecodeError:
  884.                  try:
  885.                       with open(file_path, 'r', encoding='latin-1') as f:
  886.                           content = f.read()
  887.                  except Exception:
  888.                       with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
  889.                           content = f.read()
  890.         except Exception as e:
  891.             QMessageBox.critical(self, "Błąd otwarcia pliku", f"Nie można odczytać pliku {os.path.basename(file_path)}:\n{e}")
  892.             self.statusBar().showMessage(f"Błąd otwarcia pliku: {os.path.basename(file_path)}")
  893.             return
  894.         editor = QPlainTextEdit()
  895.         editor.setPlainText(content)
  896.         editor.setFont(self.base_editor_font)
  897.         editor.document().setModified(False) # Nowo otwarty plik nie jest zmodyfikowany
  898.         editor.document().modificationChanged.connect(self._handle_modification_changed)
  899.         language = self._get_language_from_path(file_path)
  900.         highlighter = CodeSyntaxHighlighter(editor.document(), language) # Przypisz highlighter do dokumentu edytora
  901.         setattr(editor.document(), '_syntax_highlighter', highlighter)
  902.         tab_index = self.tab_widget.addTab(editor, os.path.basename(file_path))
  903.         self.tab_widget.setCurrentIndex(tab_index)
  904.         self.open_files[file_path] = editor # Zapisz odwołanie do edytora
  905.         self.statusBar().showMessage(f"Otwarto plik: {file_path}")
  906.         if file_path in self.recents["open_files"]:
  907.             self.recents["open_files"].remove(file_path) # Usuń stary wpis, żeby był na górze
  908.         self.recents["open_files"].insert(0, file_path) # Dodaj na początek
  909.         self._update_recent_files_menu() # Uaktualnij menu
  910.         self._save_app_state() # Zapisz stan
  911.     def _reopen_files(self, file_list):
  912.         """Ponownie otwiera listę plików po uruchomieniu programu."""
  913.         files_to_reopen = list(file_list) # Tworzymy kopię
  914.         valid_files = [f for f in files_to_reopen if os.path.exists(f) and os.path.isfile(f)]
  915.         self.recents["open_files"] = valid_files
  916.         self._update_recent_files_menu() # Uaktualnij menu
  917.         for file_path in valid_files:
  918.             QTimer.singleShot(0, lambda path=file_path: self._open_file(path))
  919.         invalid_files = [f for f in files_to_reopen if f not in valid_files]
  920.         if invalid_files:
  921.             msg = "Nie można ponownie otworzyć następujących plików (nie znaleziono):\n" + "\n".join(invalid_files)
  922.             QMessageBox.warning(self, "Błąd otwarcia plików", msg)
  923.     def _update_recent_files_menu(self):
  924.         """Uaktualnia listę plików w menu 'Ostatnio otwierane'."""
  925.         self.recent_files_menu.clear()
  926.         recent_items_to_show = list(self.recents.get("open_files", []))[:15] # Pokaż max 15 (praca na kopii)
  927.         if not recent_items_to_show:
  928.             self.recent_files_menu.addAction("Brak ostatnio otwieranych plików").setEnabled(False)
  929.             return
  930.         actions_to_add = []
  931.         cleaned_recent_files = [] # Zbuduj nową listę poprawnych ścieżek
  932.         for file_path in recent_items_to_show:
  933.             if os.path.exists(file_path) and os.path.isfile(file_path):
  934.                  cleaned_recent_files.append(file_path) # Dodaj do listy czystej
  935.                  menu_text = os.path.basename(file_path) # Pokaż tylko nazwę pliku
  936.                  action = QAction(menu_text, self)
  937.                  action.setData(file_path) # Zapisz pełną ścieżkę w danych akcji
  938.                  action.triggered.connect(lambda checked, path=file_path: self._open_file(path))
  939.                  actions_to_add.append(action)
  940.         all_existing_recent_files = [p for p in self.recents.get("open_files", []) if os.path.exists(p) and os.path.isfile(p)]
  941.         unique_recent_files = []
  942.         for p in all_existing_recent_files:
  943.             if p not in unique_recent_files:
  944.                 unique_recent_files.append(p)
  945.         self.recents["open_files"] = unique_recent_files[:20]
  946.         self.recent_files_menu.clear() # Wyczyść ponownie
  947.         if not self.recents["open_files"]:
  948.              self.recent_files_menu.addAction("Brak ostatnio otwieranych plików").setEnabled(False)
  949.         else:
  950.              for file_path in self.recents["open_files"]:
  951.                   menu_text = os.path.basename(file_path)
  952.                   action = QAction(menu_text, self)
  953.                   action.setData(file_path)
  954.                   action.triggered.connect(lambda checked, path=file_path: self._open_file(path))
  955.                   self.recent_files_menu.addAction(action)
  956.     def _get_language_from_path(self, file_path):
  957.          """Zwraca nazwę języka na podstawie rozszerzenia pliku."""
  958.          if not file_path:
  959.               return 'plaintext'
  960.          file_info = QFileInfo(file_path)
  961.          extension = file_info.suffix().lower()
  962.          if extension == 'py':
  963.              return 'python'
  964.          elif extension == 'js':
  965.              return 'javascript'
  966.          elif extension == 'html':
  967.              return 'html'
  968.          elif extension == 'css':
  969.              return 'css'
  970.          elif extension in ['c', 'cpp', 'h', 'hpp']:
  971.              return 'c++'
  972.          elif extension == 'ini':
  973.              return 'ini'
  974.          elif extension == 'json':
  975.              return 'json'
  976.          else:
  977.              return 'plaintext' # Bez kolorowania
  978.     def _handle_tree_item_double_click(self, index):
  979.         """Obsługa podwójnego kliknięcia w drzewku projektu."""
  980.         file_path = self.project_model.filePath(index)
  981.         if os.path.isfile(file_path):
  982.             self._open_file(file_path)
  983.         elif os.path.isdir(file_path):
  984.              if self.project_tree.isExpanded(index):
  985.                  self.project_tree.collapse(index)
  986.              else:
  987.                  self.project_tree.expand(index)
  988.     def _show_project_tree_context_menu(self, point):
  989.         """Wyświetla menu kontekstowe dla drzewka projektu."""
  990.         index = self.project_tree.indexAt(point)
  991.         menu = QMenu(self)
  992.         create_file_action = QAction(qta.icon('fa5s.file-medical') if qta else QIcon(), "Nowy plik...", self)
  993.         create_folder_action = QAction(qta.icon('fa5s.folder-plus') if qta else QIcon(), "Nowy folder...", self)
  994.         select_all_action = QAction("Zaznacz wszystko", self)
  995.         select_all_action.triggered.connect(self.project_tree.selectAll)
  996.         if index.isValid():
  997.             file_path = self.project_model.filePath(index)
  998.             file_info = self.project_model.fileInfo(index)
  999.             is_root_model = self.project_model.rootPath() == file_path
  1000.             if not is_root_model: # Nie usuwaj/zmieniaj nazwy roota projektu
  1001.                  rename_action = QAction(qta.icon('fa5s.edit') if qta else QIcon(), "Zmień nazwę...", self)
  1002.                  delete_action = QAction(qta.icon('fa5s.trash') if qta else QIcon(), "Usuń", self)
  1003.                  rename_action.triggered.connect(lambda: self._rename_item(index))
  1004.                  delete_action.triggered.connect(lambda: self._delete_item(index))
  1005.                  if file_info.isDir():
  1006.                      menu.addAction(create_file_action) # Nowy plik w folderze
  1007.                      menu.addAction(create_folder_action) # Nowy folder w folderze
  1008.                      menu.addSeparator()
  1009.                      menu.addAction(rename_action)
  1010.                      menu.addAction(delete_action)
  1011.                      menu.addSeparator()
  1012.                      open_in_os_action = QAction(qta.icon('fa5s.external-link-alt') if qta else QIcon(), "Otwórz w eksploratorze", self) # Windows
  1013.                      if platform.system() == "Darwin": open_in_os_action.setText("Otwórz w Finderze") # macOS
  1014.                      elif platform.system() == "Linux": open_in_os_action.setText("Otwórz w menedżerze plików") # Linux
  1015.                      open_in_os_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile(file_path)))
  1016.                      menu.addAction(open_in_os_action)
  1017.                      create_file_action.triggered.connect(lambda: self._create_new_item(file_path, is_folder=False))
  1018.                      create_folder_action.triggered.connect(lambda: self._create_new_item(file_path, is_folder=True))
  1019.                  elif file_info.isFile():
  1020.                      open_action = QAction(qta.icon('fa5s.file') if qta else QIcon(), "Otwórz", self) # Mimo podwójnego kliknięcia
  1021.                      open_action.triggered.connect(lambda: self._open_file(file_path))
  1022.                      duplicate_action = QAction(qta.icon('fa5s.copy') if qta else QIcon(), "Duplikuj", self)
  1023.                      duplicate_action.triggered.connect(lambda: self._duplicate_file(index))
  1024.                      copy_path_action = QAction(qta.icon('fa5s.link') if qta else QIcon(), "Kopiuj ścieżkę", self)
  1025.                      copy_path_action.triggered.connect(lambda: QApplication.clipboard().setText(file_path))
  1026.                      menu.addAction(open_action)
  1027.                      menu.addSeparator()
  1028.                      menu.addAction(rename_action)
  1029.                      menu.addAction(delete_action)
  1030.                      menu.addAction(duplicate_action)
  1031.                      menu.addSeparator()
  1032.                      menu.addAction(copy_path_action)
  1033.                      menu.addSeparator()
  1034.                      open_containing_folder_action = QAction(qta.icon('fa5s.folder-open') if qta else QIcon(), "Pokaż w eksploratorze", self) # Windows
  1035.                      if platform.system() == "Darwin": open_containing_folder_action.setText("Pokaż w Finderze") # macOS
  1036.                      elif platform.system() == "Linux": open_containing_folder_action.setText("Pokaż w menedżerze plików") # Linux
  1037.                      open_containing_folder_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.dirname(file_path))))
  1038.                      menu.addAction(open_containing_folder_action)
  1039.             else: # Kliknięto na root folderu projektu
  1040.                  menu.addAction(create_file_action) # Nowy plik w roocie
  1041.                  menu.addAction(create_folder_action) # Nowy folder w roocie
  1042.                  menu.addSeparator()
  1043.                  open_in_os_action = QAction(qta.icon('fa5s.external-link-alt') if qta else QIcon(), "Otwórz w eksploratorze", self) # Windows
  1044.                  if platform.system() == "Darwin": open_in_os_action.setText("Otwórz w Finderze") # macOS
  1045.                  elif platform.system() == "Linux": open_in_os_action.setText("Otwórz w menedżerze plików") # Linux
  1046.                  open_in_os_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile(file_path)))
  1047.                  menu.addAction(open_in_os_action)
  1048.                  create_file_action.triggered.connect(lambda: self._create_new_item(file_path, is_folder=False))
  1049.                  create_folder_action.triggered.connect(lambda: self._create_new_item(file_path, is_folder=True))
  1050.         else: # Kliknięto na puste miejsce w drzewku
  1051.             if self.current_project_dir and os.path.isdir(self.current_project_dir):
  1052.                 menu.addAction(create_file_action) # Nowy plik w roocie projektu
  1053.                 menu.addAction(create_folder_action) # Nowy folder w roocie projektu
  1054.                 create_file_action.triggered.connect(lambda: self._create_new_item(self.current_project_dir, is_folder=False))
  1055.                 create_folder_action.triggered.connect(lambda: self._create_new_item(self.current_project_dir, is_folder=True))
  1056.                 menu.addSeparator()
  1057.             menu.addAction(select_all_action)
  1058.         if menu.actions(): # Pokaż menu tylko jeśli są jakieś akcje
  1059.             menu.exec(self.project_tree.mapToGlobal(point))
  1060.     def _create_new_item(self, parent_dir, is_folder):
  1061.          """Tworzy nowy plik lub folder w podanym katalogu nadrzędnym."""
  1062.          if not os.path.isdir(parent_dir):
  1063.               QMessageBox.warning(self, "Błąd tworzenia", f"Katalog nadrzędny nie istnieje lub nie jest katalogiem:\n{parent_dir}")
  1064.               self.statusBar().showMessage("Błąd tworzenia nowego elementu.")
  1065.               return
  1066.          dialog = NewItemDialog(parent_dir, is_folder, self)
  1067.          if dialog.exec() == QDialog.DialogCode.Accepted:
  1068.              item_name = dialog.get_item_name()
  1069.              if not item_name: return # Powinno być obsłużone w dialogu, ale zabezpieczenie
  1070.              full_path = os.path.join(parent_dir, item_name)
  1071.              parent_index = self.project_model.index(parent_dir)
  1072.              if not parent_index.isValid():
  1073.                   try:
  1074.                        if is_folder:
  1075.                            os.makedirs(full_path)
  1076.                            self.statusBar().showMessage(f"Utworzono folder: {item_name}")
  1077.                        else:
  1078.                            with open(full_path, 'w') as f: pass # Utwórz pusty plik
  1079.                            self.statusBar().showMessage(f"Utworzono plik: {item_name}")
  1080.                        root_path = self.project_model.rootPath()
  1081.                        if root_path and os.path.isdir(root_path):
  1082.                             self.project_model.refresh(self.project_model.index(root_path))
  1083.                   except OSError as e:
  1084.                        QMessageBox.critical(self, "Błąd tworzenia", f"Nie można utworzyć {'folderu' if is_folder else 'pliku'}:\n{e}")
  1085.                        self.statusBar().showMessage(f"Błąd tworzenia {'folderu' if is_folder else 'pliku'}.")
  1086.                   except Exception as e:
  1087.                       QMessageBox.critical(self, "Nieoczekiwany błąd", f"Wystąpił nieoczekiwany błąd:\n{e}")
  1088.                       self.statusBar().showMessage("Nieoczekiwany błąd.")
  1089.                   return
  1090.              success = False
  1091.              if is_folder:
  1092.                  new_index = self.project_model.mkdir(parent_index, item_name)
  1093.                  if new_index.isValid():
  1094.                       self.statusBar().showMessage(f"Utworzono folder: {item_name}")
  1095.                       success = True
  1096.                       self.project_tree.expand(new_index) # Rozwiń folder, żeby było widać nowy element
  1097.              else:
  1098.                   try:
  1099.                        with open(full_path, 'w') as f: pass # Utwórz pusty plik
  1100.                        self.statusBar().showMessage(f"Utworzono plik: {item_name}")
  1101.                        self.project_model.refresh(parent_index)
  1102.                        success = True
  1103.                   except OSError as e:
  1104.                        QMessageBox.critical(self, "Błąd tworzenia", f"Nie można utworzyć pliku:\n{e}")
  1105.                        self.statusBar().showMessage("Błąd tworzenia pliku.")
  1106.                   except Exception as e:
  1107.                       QMessageBox.critical(self, "Nieoczekiwany błąd", f"Wystąpił nieoczekiwany błąd:\n{e}")
  1108.                       self.statusBar().showMessage("Nieoczekiwany błąd.")
  1109.     def _rename_item(self, index):
  1110.          """Zmienia nazwę pliku lub folderu."""
  1111.          if not index.isValid(): return
  1112.          old_path = self.project_model.filePath(index)
  1113.          old_name = os.path.basename(old_path)
  1114.          parent_dir = os.path.dirname(old_path)
  1115.          if old_path == self.project_model.rootPath() or old_path == self.project_model.filePath(self.project_model.index("")):
  1116.               QMessageBox.warning(self, "Błąd zmiany nazwy", "Nie można zmienić nazwy katalogu głównego.")
  1117.               self.statusBar().showMessage("Nie można zmienić nazwy katalogu głównego.")
  1118.               return
  1119.          dialog = RenameItemDialog(old_path, self)
  1120.          if dialog.exec() == QDialog.DialogCode.Accepted:
  1121.              new_name = dialog.get_new_name()
  1122.              if not new_name or new_name == old_name:
  1123.                   self.statusBar().showMessage("Zmiana nazwy anulowana lub nowa nazwa jest taka sama.")
  1124.                   return # Brak zmiany lub pusta nazwa (powinno być obsłużone w dialogu)
  1125.              new_path = os.path.join(parent_dir, new_name)
  1126.              open_files_to_close = []
  1127.              if os.path.isfile(old_path):
  1128.                   if old_path in self.open_files:
  1129.                        open_files_to_close.append(old_path)
  1130.              elif os.path.isdir(old_path):
  1131.                   normalized_folder_path = os.path.normpath(old_path) + os.sep
  1132.                   for open_file_path in self.open_files.keys():
  1133.                        if os.path.normpath(open_file_path).startswith(normalized_folder_path):
  1134.                             open_files_to_close.append(open_file_path)
  1135.              if open_files_to_close:
  1136.                   open_file_names = [os.path.basename(p) for p in open_files_to_close]
  1137.                   msg = f"{{'Plik' if os.path.isfile(old_path) else 'Folder'}} '{old_name}' zawiera otwarte pliki, które zostaną zamknięte i ponownie otwarte po zmianie nazwy:\\n" + "\\n".join(open_file_names)
  1138.                   reply = QMessageBox.question(self, "Zamknij pliki", msg + "\n\nCzy chcesz kontynuować?",
  1139.                                                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  1140.                   if reply == QMessageBox.StandardButton.No:
  1141.                        self.statusBar().showMessage("Zmiana nazwy anulowana.")
  1142.                        return # Anuluj operację
  1143.                   unsaved_open_files = [p for p in open_files_to_close if self.open_files.get(p) and self.open_files[p].document().isModified()]
  1144.                   if unsaved_open_files:
  1145.                        save_reply = QMessageBox.question(self, "Niezapisane zmiany",
  1146.                                                          f"Niektóre z plików ({len(unsaved_open_files)}) mają niezapisane zmiany. Czy chcesz je zapisać przed zamknięciem i zmianą nazwy?",
  1147.                                                          QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
  1148.                        if save_reply == QMessageBox.StandardButton.Cancel:
  1149.                             self.statusBar().showMessage("Zmiana nazwy anulowana (niezapisane zmiany).")
  1150.                             return # Anuluj operację
  1151.                        if save_reply == QMessageBox.StandardButton.Save:
  1152.                             save_success = True
  1153.                             for file_path_to_save in unsaved_open_files:
  1154.                                  editor = self.open_files.get(file_path_to_save)
  1155.                                  if editor and not self._save_file(editor, file_path_to_save): # _save_file handles Save As if needed
  1156.                                       save_success = False
  1157.                                       break
  1158.                             if not save_success:
  1159.                                  self.statusBar().showMessage("Zmiana nazwy anulowana (błąd zapisu otwartych plików).")
  1160.                                  return # Anuluj operację
  1161.                   for file_path_to_close in reversed(open_files_to_close):
  1162.                        editor_widget = self.open_files.get(file_path_to_close)
  1163.                        if editor_widget:
  1164.                             tab_index = self.tab_widget.indexOf(editor_widget)
  1165.                             if tab_index != -1:
  1166.                                  if hasattr(editor_widget, 'document'):
  1167.                                       editor_widget.document().setModified(False)
  1168.                                  self.tab_widget.removeTab(tab_index)
  1169.                                  del self.open_files[file_path_to_close]
  1170.                                  editor_widget.deleteLater()
  1171.                   self.recents["open_files"] = [p for p in self.recents["open_files"] if p not in open_files_to_close]
  1172.                   self._update_recent_files_menu()
  1173.                   self._save_app_state() # Zapisz stan po zamknięciu plików
  1174.              success = self.project_model.rename(index, new_name)
  1175.              if success:
  1176.                  self.statusBar().showMessage(f"Zmieniono nazwę na: {new_name}")
  1177.                  if open_files_to_close:
  1178.                       parent_index_after_rename = self.project_model.index(parent_dir)
  1179.                       if parent_index_after_rename.isValid():
  1180.                            self.project_model.refresh(parent_index_after_rename)
  1181.                       for old_file_path in open_files_to_close:
  1182.                            relative_path = os.path.relpath(old_file_path, old_path)
  1183.                            new_file_path = os.path.join(new_path, relative_path)
  1184.                            if os.path.exists(new_file_path) and os.path.isfile(new_file_path):
  1185.                                 self._open_file(new_file_path) # Otworzy plik pod nową ścieżką
  1186.              else:
  1187.                   QMessageBox.critical(self, "Błąd zmiany nazwy", f"Nie można zmienić nazwy '{old_name}' na '{new_name}'.\n"
  1188.                                                                    "Sprawdź, czy element o tej nazwie już nie istnieje lub czy masz uprawnienia.")
  1189.                   self.statusBar().showMessage("Błąd zmiany nazwy.")
  1190.     def _delete_item(self, index):
  1191.         """Usuwa plik lub folder."""
  1192.         if not index.isValid(): return
  1193.         file_path = self.project_model.filePath(index)
  1194.         item_name = os.path.basename(file_path)
  1195.         is_dir = self.project_model.fileInfo(index).isDir()
  1196.         if file_path == self.project_model.rootPath() or file_path == self.project_model.filePath(self.project_model.index("")):
  1197.              QMessageBox.warning(self, "Błąd usuwania", "Nie można usunąć katalogu głównego.")
  1198.              self.statusBar().showMessage("Nie można usunąć katalogu głównego.")
  1199.              return
  1200.         open_files_to_close = []
  1201.         if is_dir:
  1202.              normalized_folder_path = os.path.normpath(file_path) + os.sep
  1203.              for open_file_path in self.open_files.keys():
  1204.                   if os.path.normpath(open_file_path).startswith(normalized_folder_path):
  1205.                        open_files_to_close.append(open_file_path)
  1206.         elif file_path in self.open_files:
  1207.              open_files_to_close.append(file_path)
  1208.         if open_files_to_close:
  1209.              open_file_names = [os.path.basename(p) for p in open_files_to_close]
  1210.              msg = f"Nie można usunąć {{'folderu' if is_dir else 'pliku'}} '{item_name}', ponieważ zawiera on otwarte pliki, które muszą zostać zamknięte:\\n" + "\\n".join(open_file_names)
  1211.              QMessageBox.warning(self, "Element jest używany", msg)
  1212.              reply_close = QMessageBox.question(self, "Zamknij pliki",
  1213.                                                 f"Czy chcesz zamknąć te pliki, aby kontynuować usuwanie '{item_name}'?",
  1214.                                                 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  1215.              if reply_close == QMessageBox.StandardButton.No:
  1216.                   self.statusBar().showMessage(f"Usuwanie '{item_name}' anulowane.")
  1217.                   return # Anuluj operację
  1218.              unsaved_open_files = [p for p in open_files_to_close if self.open_files.get(p) and self.open_files[p].document().isModified()]
  1219.              if unsaved_open_files:
  1220.                   save_reply = QMessageBox.question(self, "Niezapisane zmiany",
  1221.                                                     f"Niektóre z plików ({len(unsaved_open_files)}) mają niezapisane zmiany. Czy chcesz je zapisać przed zamknięciem i usunięciem?",
  1222.                                                     QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
  1223.                   if save_reply == QMessageBox.StandardButton.Cancel:
  1224.                        self.statusBar().showMessage("Usuwanie anulowane (niezapisane zmiany).")
  1225.                        return # Anuluj operację
  1226.                   if save_reply == QMessageBox.StandardButton.Save:
  1227.                        save_success = True
  1228.                        for file_path_to_save in unsaved_open_files:
  1229.                             editor = self.open_files.get(file_path_to_save)
  1230.                             if editor and not self._save_file(editor, file_path_to_save): # _save_file handles Save As if needed
  1231.                                  save_success = False
  1232.                                  break
  1233.                        if not save_success:
  1234.                             self.statusBar().showMessage("Usuwanie anulowane (błąd zapisu otwartych plików).")
  1235.                             return # Anuluj operację
  1236.              for file_path_to_close in reversed(open_files_to_close):
  1237.                   editor_widget = self.open_files.get(file_path_to_close)
  1238.                   if editor_widget:
  1239.                        tab_index = self.tab_widget.indexOf(editor_widget)
  1240.                        if tab_index != -1:
  1241.                             if hasattr(editor_widget, 'document'):
  1242.                                 editor_widget.document().setModified(False)
  1243.                             self.tab_widget.removeTab(tab_index)
  1244.                             del self.open_files[file_path_to_close]
  1245.                             editor_widget.deleteLater()
  1246.              self.recents["open_files"] = [p for p in self.recents["open_files"] if p not in open_files_to_close]
  1247.              self._update_recent_files_menu()
  1248.              self._save_app_state() # Zapisz stan po zamknięciu plików
  1249.         item_type = "folder" if is_dir else "plik"
  1250.         reply = QMessageBox.question(self, f"Usuń {item_type}",
  1251.                                      f"Czy na pewno chcesz usunąć {item_type} '{item_name}'?\\n"
  1252.                                      "Ta operacja jest nieodwracalna!", # Dodaj ostrzeżenie
  1253.                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  1254.         if reply == QMessageBox.StandardButton.Yes:
  1255.             success = self.project_model.remove(index)
  1256.             if success:
  1257.                 self.statusBar().showMessage(f"Usunięto {item_type}: {item_name}")
  1258.             else:
  1259.                  QMessageBox.critical(self, f"Błąd usuwania {item_type}", f"Nie można usunąć {item_type} '{item_name}'.\\n"
  1260.                                                                             "Sprawdź, czy masz uprawnienia lub czy element nie jest używany przez inny program.")
  1261.                  self.statusBar().showMessage(f"Błąd usuwania {item_type}.")
  1262.     def _duplicate_file(self, index):
  1263.          """Duplikuje plik."""
  1264.          if not index.isValid(): return
  1265.          file_path = self.project_model.filePath(index)
  1266.          file_info = self.project_model.fileInfo(index)
  1267.          if not file_info.isFile():
  1268.               self.statusBar().showMessage("Można duplikować tylko pliki.")
  1269.               return
  1270.          parent_dir = os.path.dirname(file_path)
  1271.          old_name = os.path.basename(file_path)
  1272.          name, ext = os.path.splitext(old_name)
  1273.          suggested_name = f"{name}_kopia{ext}"
  1274.          counter = 1
  1275.          while os.path.exists(os.path.join(parent_dir, suggested_name)):
  1276.               counter += 1
  1277.               suggested_name = f"{name}_kopia{counter}{ext}"
  1278.          new_name, ok = QInputDialog.getText(self, "Duplikuj plik", f"Podaj nazwę dla kopii '{old_name}':",
  1279.                                             QLineEdit.EchoMode.Normal, suggested_name)
  1280.          if ok and new_name:
  1281.              new_name = new_name.strip()
  1282.              if not new_name or re.search(r'[<>:"/\\|?*\x00-\x1F]', new_name) is not None:
  1283.                  QMessageBox.warning(self, "Nieprawidłowa nazwa", "Podana nazwa jest pusta lub zawiera niedozwolone znaki.")
  1284.                  self.statusBar().showMessage("Duplikowanie anulowane (nieprawidłowa nazwa).")
  1285.                  return
  1286.              new_path = os.path.join(parent_dir, new_name)
  1287.              if os.path.exists(new_path):
  1288.                   QMessageBox.warning(self, "Element już istnieje", f"Element o nazwie '{new_name}' już istnieje.")
  1289.                   self.statusBar().showMessage("Duplikowanie anulowane (element już istnieje).")
  1290.                   return
  1291.              try:
  1292.                  os.makedirs(os.path.dirname(new_path), exist_ok=True)
  1293.                  shutil.copy2(file_path, new_path) # copy2 zachowuje metadane
  1294.                  self.statusBar().showMessage(f"Utworzono kopię: {new_name}")
  1295.                  parent_index = self.project_model.index(parent_dir)
  1296.                  if parent_index.isValid():
  1297.                       self.project_model.refresh(parent_index)
  1298.                  else:
  1299.                       root_path = self.project_model.rootPath()
  1300.                       if root_path and os.path.isdir(root_path):
  1301.                            self.project_model.refresh(self.project_model.index(root_path))
  1302.              except OSError as e:
  1303.                   QMessageBox.critical(self, "Błąd duplikowania", f"Nie można zduplikować pliku '{old_name}':\n{e}")
  1304.                   self.statusBar().showMessage("Błąd duplikowania pliku.")
  1305.              except Exception as e:
  1306.                  QMessageBox.critical(self, "Nieoczekiwany błąd", f"Wystąpił nieoczekiwany błąd:\n{e}")
  1307.                  self.statusBar().showMessage("Nieoczekiwany błąd.")
  1308.     def _close_tab_by_index(self, index):
  1309.         """Zamyka zakładkę o podanym indeksie, pytając o zapisanie zmian."""
  1310.         if index == -1: # Brak otwartych zakładek
  1311.             return
  1312.         widget = self.tab_widget.widget(index)
  1313.         if widget is None: # Zabezpieczenie
  1314.             return
  1315.         file_path_before_save = None
  1316.         for path, editor_widget in list(self.open_files.items()):
  1317.             if editor_widget is widget:
  1318.                 file_path_before_save = path
  1319.                 break
  1320.         if hasattr(widget, 'document') and widget.document().isModified():
  1321.             msg_box = QMessageBox(self)
  1322.             msg_box.setIcon(QMessageBox.Icon.Warning)
  1323.             msg_box.setWindowTitle("Niezapisane zmiany")
  1324.             tab_text = self.tab_widget.tabText(index).rstrip('*')
  1325.             display_name = os.path.basename(file_path_before_save) if file_path_before_save else tab_text
  1326.             msg_box.setText(f"Plik '{display_name}' ma niezapisane zmiany.\nCzy chcesz zapisać przed zamknięciem?")
  1327.             msg_box.setStandardButtons(QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
  1328.             msg_box.setDefaultButton(QMessageBox.StandardButton.Save)
  1329.             reply = msg_box.exec()
  1330.             if reply == QMessageBox.StandardButton.Save:
  1331.                 needs_save_as = (file_path_before_save is None or
  1332.                                  not os.path.exists(file_path_before_save) or
  1333.                                  not QFileInfo(file_path_before_save).isFile())
  1334.                 if needs_save_as:
  1335.                     original_index = self.tab_widget.currentIndex()
  1336.                     self.tab_widget.setCurrentIndex(index)
  1337.                     save_success = self._save_current_file_as() # This handles saving and updating open_files/recents for the NEW path
  1338.                     if original_index != -1 and original_index < self.tab_widget.count():
  1339.                          self.tab_widget.setCurrentIndex(original_index)
  1340.                     else: # If the original tab was closed during Save As (e.g. saving an existing file to itself)
  1341.                          pass # Keep the new tab active
  1342.                     if not save_success:
  1343.                          self.statusBar().showMessage(f"Zamknięcie anulowane (błąd zapisu '{display_name}').")
  1344.                          return # Anulowano lub błąd zapisu "jako", nie zamykaj zakładki
  1345.                 else: # It's an existing file with a valid path
  1346.                     if not self._save_file(widget, file_path_before_save): # Spróbuj zapisać, jeśli się nie uda, nie zamykaj
  1347.                         self.statusBar().showMessage(f"Zamknięcie anulowane (błąd zapisu '{display_name}').")
  1348.                         return # Anulowano lub błąd zapisu
  1349.            elif reply == QMessageBox.StandardButton.Cancel:
  1350.                self.statusBar().showMessage(f"Zamknięcie '{tab_text}' anulowane.")
  1351.                return # Anuluj zamknięcie
  1352.        if file_path_before_save in self.open_files:
  1353.             del self.open_files[file_path_before_save]
  1354.             if file_path_before_save in self.recents["open_files"]:
  1355.                 self.recents["open_files"].remove(file_path_before_save)
  1356.                 self._update_recent_files_menu() # Uaktualnij menu
  1357.        self.tab_widget.removeTab(index)
  1358.        widget.deleteLater() # Usuń widget z pamięci
  1359.        if file_path_before_save:
  1360.            self.statusBar().showMessage(f"Zamknięto plik: {os.path.basename(file_path_before_save)}")
  1361.        else:
  1362.             self.statusBar().showMessage("Zamknięto plik.")
  1363.        self._save_app_state()
  1364.    def _close_current_tab(self):
  1365.        """Zamyka aktualnie aktywną zakładkę."""
  1366.        current_index = self.tab_widget.currentIndex()
  1367.        if current_index != -1:
  1368.            self._close_tab_by_index(current_index)
  1369.    def _save_current_file(self):
  1370.        """Zapisuje aktualnie aktywny plik. Jeśli nowy, wywołuje Save As."""
  1371.        current_widget = self.tab_widget.currentWidget()
  1372.        if not isinstance(current_widget, QPlainTextEdit):
  1373.            self.statusBar().showMessage("Brak aktywnego pliku do zapisu.")
  1374.            return False
  1375.        file_path = None
  1376.        for path, editor_widget in list(self.open_files.items()):
  1377.            if editor_widget is current_widget:
  1378.                file_path = path
  1379.                break
  1380.        is_existing_valid_file = file_path and os.path.exists(file_path) and QFileInfo(file_path).isFile()
  1381.        if is_existing_valid_file:
  1382.             return self._save_file(current_widget, file_path)
  1383.        else:
  1384.             return self._save_current_file_as()
  1385.    def _save_current_file_as(self):
  1386.        """Zapisuje zawartość aktywnego edytora z nową nazwą."""
  1387.        current_widget = self.tab_widget.currentWidget()
  1388.        if not isinstance(current_widget, QPlainTextEdit):
  1389.            self.statusBar().showMessage("Brak aktywnego pliku do zapisu.")
  1390.            return False
  1391.        old_file_path = None
  1392.        for path, editor_widget in list(self.open_files.items()): # Iterate over a copy
  1393.            if editor_widget is current_widget:
  1394.                old_file_path = path
  1395.                break
  1396.        suggested_name = "bez_nazwy.txt"
  1397.        current_tab_index = self.tab_widget.indexOf(current_widget)
  1398.        if current_tab_index != -1:
  1399.             original_tab_text = self.tab_widget.tabText(current_tab_index).rstrip('*')
  1400.             if original_tab_text and original_tab_text != "Nowy plik":
  1401.                  suggested_name = original_tab_text
  1402.             elif current_widget.document().toPlainText().strip():
  1403.                  first_line = current_widget.document().toPlainText().strip().split('\n')[0].strip()
  1404.                  if first_line:
  1405.                       suggested_name = re.sub(r'[\\/:*?"<>|]', '_', first_line) # Remove illegal chars
  1406.                       suggested_name = suggested_name[:50].strip() # Limit length
  1407.                       if not suggested_name:
  1408.                           suggested_name = "bez_nazwy"
  1409.                       if '.' not in os.path.basename(suggested_name):
  1410.                            suggested_name += ".txt"
  1411.                  else:
  1412.                       suggested_name = "bez_nazwy.txt" # Fallback if content is just whitespace
  1413.        start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
  1414.        if old_file_path and os.path.dirname(old_file_path):
  1415.             start_path = os.path.dirname(old_file_path) # Use directory of old file if available
  1416.        elif os.path.isdir(start_path):
  1417.             pass # Use project dir if available
  1418.        else:
  1419.             start_path = os.path.expanduser("~")
  1420.        file_filters = "Wszystkie pliki (*);;Pliki Pythona (*.py);;Pliki JavaScript (*.js);;Pliki HTML (*.html);;Pliki CSS (*.css);;Pliki C++ (*.c *.cpp *.h *.hpp);;Pliki INI (*.ini);;Pliki JSON (*.json)"
  1421.        new_file_path, _ = QFileDialog.getSaveFileName(self, "Zapisz plik jako...", os.path.join(start_path, suggested_name), file_filters)
  1422.        if not new_file_path:
  1423.            self.statusBar().showMessage("Zapisywanie anulowane.")
  1424.            return False # Cancelled
  1425.        new_file_path = os.path.normpath(new_file_path)
  1426.        if old_file_path and old_file_path != new_file_path:
  1427.             if old_file_path in self.open_files:
  1428.                  del self.open_files[old_file_path]
  1429.             if old_file_path in self.recents["open_files"]:
  1430.                 self.recents["open_files"].remove(old_file_path)
  1431.                 self._update_recent_files_menu()
  1432.        self.open_files[new_file_path] = current_widget
  1433.        current_tab_index = self.tab_widget.indexOf(current_widget)
  1434.        if current_tab_index != -1:
  1435.             self.tab_widget.setTabText(current_tab_index, os.path.basename(new_file_path))
  1436.        if new_file_path in self.recents["open_files"]: # Remove if already exists to move to front
  1437.             self.recents["open_files"].remove(new_file_path)
  1438.        self.recents["open_files"].insert(0, new_file_path)
  1439.        self._update_recent_files_menu() # Update the menu
  1440.        language = self._get_language_from_path(new_file_path)
  1441.        old_highlighter = getattr(current_widget.document(), '_syntax_highlighter', None)
  1442.        if old_highlighter:
  1443.             old_highlighter.setDocument(None) # Detach
  1444.        new_highlighter = CodeSyntaxHighlighter(current_widget.document(), language)
  1445.        setattr(current_widget.document(), '_syntax_highlighter', new_highlighter)
  1446.        return self._save_file(current_widget, new_file_path)
  1447.    def _save_file(self, editor_widget, file_path):
  1448.        """Zapisuje zawartość wskazanego edytora do wskazanego pliku."""
  1449.        if not file_path:
  1450.             print("Error: _save_file called with empty path.", file=sys.stderr)
  1451.             self.statusBar().showMessage("Błąd wewnętrzny: próba zapisu bez ścieżki.")
  1452.             return False
  1453.        try:
  1454.            os.makedirs(os.path.dirname(file_path), exist_ok=True)
  1455.            with open(file_path, 'w', encoding='utf-8') as f:
  1456.                f.write(editor_widget.toPlainText())
  1457.            editor_widget.document().setModified(False) # Mark document as unmodified
  1458.            self.statusBar().showMessage(f"Plik zapisano pomyślnie: {os.path.basename(file_path)}")
  1459.            tab_index = self.tab_widget.indexOf(editor_widget)
  1460.            if tab_index != -1:
  1461.                 current_tab_text = self.tab_widget.tabText(tab_index).rstrip('*')
  1462.                 self.tab_widget.setTabText(tab_index, current_tab_text)
  1463.            file_info = QFileInfo(file_path)
  1464.            dir_path = file_info.dir().path()
  1465.            root_path = self.project_model.rootPath()
  1466.            if root_path and dir_path.startswith(root_path):
  1467.                 dir_index = self.project_model.index(dir_path)
  1468.                 if dir_index.isValid():
  1469.                      self.project_model.refresh(dir_index)
  1470.                 file_index = self.project_model.index(file_path)
  1471.                 if file_index.isValid():
  1472.                      self.project_model.dataChanged.emit(file_index, file_index, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.DecorationRole]) # Trigger a view update
  1473.            if file_path in self.recents["open_files"]:
  1474.                 self.recents["open_files"].remove(file_path)
  1475.            self.recents["open_files"].insert(0, file_path)
  1476.            self._update_recent_files_menu()
  1477.            self._save_app_state() # Save state after successful save
  1478.            return True # Save succeeded
  1479.        except Exception as e:
  1480.            QMessageBox.critical(self, "Błąd zapisu pliku", f"Nie można zapisać pliku {os.path.basename(file_path)}:\n{e}")
  1481.            self.statusBar().showMessage(f"Błąd zapisu pliku: {os.path.basename(file_path)}")
  1482.            return False # Save failed
  1483.    def _save_all_files(self):
  1484.        """Zapisuje wszystkie otwarte i zmodyfikowane pliki."""
  1485.        unsaved_files = [path for path, editor in self.open_files.items() if editor.document().isModified()]
  1486.        if not unsaved_files:
  1487.             self.statusBar().showMessage("Brak zmodyfikowanych plików do zapisu.")
  1488.             return True # Nothing to save, counts as success
  1489.        self.statusBar().showMessage(f"Zapisywanie wszystkich zmodyfikowanych plików ({len(unsaved_files)})...")
  1490.        total_saved = 0
  1491.        total_failed = 0
  1492.        files_to_save = list(unsaved_files)
  1493.        for file_path in files_to_save:
  1494.             editor_widget = self.open_files.get(file_path)
  1495.             if editor_widget is None or self.tab_widget.indexOf(editor_widget) == -1:
  1496.                  print(f"Warning: Skipping save for {file_path} - editor widget not found or invalid.", file=sys.stderr)
  1497.                  continue # Go to the next file
  1498.             if not editor_widget.document().isModified():
  1499.                  continue # Already saved
  1500.             needs_save_as = (file_path is None or
  1501.                              not os.path.exists(file_path) or
  1502.                              not QFileInfo(file_path).isFile())
  1503.             save_success = False
  1504.             if needs_save_as:
  1505.                  tab_index = self.tab_widget.indexOf(editor_widget)
  1506.                  if tab_index != -1:
  1507.                       original_index = self.tab_widget.currentIndex()
  1508.                       self.tab_widget.setCurrentIndex(tab_index)
  1509.                       save_success = self._save_current_file_as()
  1510.                       if original_index != -1 and original_index < self.tab_widget.count():
  1511.                           self.tab_widget.setCurrentIndex(original_index)
  1512.                  else:
  1513.                       print(f"Error: Cannot save '{os.path.basename(file_path if file_path else 'Nowy plik')}' (Save As needed) - widget not found in tabs.", file=sys.stderr)
  1514.                       total_failed += 1
  1515.                       continue # Skip this file, couldn't save it via Save As mechanism
  1516.             else:
  1517.                  save_success = self._save_file(editor_widget, file_path) # This updates modified state and status bar
  1518.             if save_success:
  1519.                  total_saved += 1
  1520.             else:
  1521.                  total_failed += 1
  1522.        if total_saved > 0 and total_failed == 0:
  1523.             self.statusBar().showMessage(f"Zapisano pomyślnie wszystkie {total_saved} pliki.")
  1524.             return True
  1525.        elif total_saved > 0 and total_failed > 0:
  1526.             self.statusBar().showMessage(f"Zapisano {total_saved} plików, {total_failed} plików nie udało się zapisać.")
  1527.             QMessageBox.warning(self, "Błąd zapisu wszystkich plików", f"Nie udało się zapisać {total_failed} plików. Sprawdź konsolę lub logi błędów.")
  1528.             return False
  1529.        elif total_saved == 0 and total_failed > 0:
  1530.             self.statusBar().showMessage(f"Nie udało się zapisać żadnego z {total_failed} plików.")
  1531.             QMessageBox.critical(self, "Błąd zapisu wszystkich plików", f"Nie udało się zapisać żadnego z plików. Sprawdź konsolę lub logi błędów.")
  1532.             return False
  1533.        else: # total_saved == 0 and total_failed == 0 (already handled by initial check, but good fallback)
  1534.             self.statusBar().showMessage("Brak zmodyfikowanych plików do zapisu.")
  1535.             return True
  1536.    def _handle_modification_changed(self, modified):
  1537.        """Obsługa zmiany stanu modyfikacji dokumentu w aktywnym edytorze."""
  1538.        editor_document = self.sender() # Dokument, który wywołał sygnał
  1539.        if not isinstance(editor_document, QTextDocument): return
  1540.        editor = None
  1541.        for editor_widget in self.open_files.values():
  1542.             if editor_widget.document() is editor_document:
  1543.                  editor = editor_widget
  1544.                  break
  1545.        if editor is None:
  1546.             return
  1547.        index = self.tab_widget.indexOf(editor)
  1548.        if index != -1:
  1549.            tab_text = self.tab_widget.tabText(index)
  1550.            if modified and not tab_text.endswith('*'):
  1551.                self.tab_widget.setTabText(index, tab_text + '*')
  1552.            elif not modified and tab_text.endswith('*'):
  1553.                self.tab_widget.setTabText(index, tab_text.rstrip('*'))
  1554.    def _handle_tab_change(self, index):
  1555.         """Obsługa zmiany aktywnej zakładki."""
  1556.         self._hide_find_bar()
  1557.         if index != -1:
  1558.             widget = self.tab_widget.widget(index)
  1559.             if isinstance(widget, QPlainTextEdit):
  1560.                  file_path = next((path for path, ed in self.open_files.items() if ed is widget), None)
  1561.                  if file_path:
  1562.                       self.statusBar().showMessage(f"Edytujesz: {os.path.basename(file_path)}")
  1563.                  else:
  1564.                       self.statusBar().showMessage("Edytujesz: Nowy plik")
  1565.         else:
  1566.             self.statusBar().showMessage("Gotowy.") # Reset status bar
  1567.    def _find_next(self):
  1568.         """Znajduje kolejne wystąpienie tekstu z pola wyszukiwania."""
  1569.         text_to_find = self.search_input.text()
  1570.         self._find_text(text_to_find, 'next')
  1571.    def _find_previous(self):
  1572.         """Znajduje poprzednie wystąpienie tekstu z pola wyszukiwania."""
  1573.         text_to_find = self.search_input.text()
  1574.         self._find_text(text_to_find, 'previous')
  1575.    def _find_text(self, text, direction='next'):
  1576.        """Szuka tekstu w aktualnym edytorze."""
  1577.        editor = self.tab_widget.currentWidget()
  1578.        if not isinstance(editor, QPlainTextEdit):
  1579.            self.statusBar().showMessage("Brak aktywnego edytora do wyszukiwania.")
  1580.            return
  1581.        if not text:
  1582.             self.statusBar().showMessage("Wpisz tekst do wyszukania.")
  1583.             return
  1584.        flags = QTextDocument.FindFlag(0) # Default: case-sensitive in Qt find
  1585.        if direction == 'previous':
  1586.             flags |= QTextDocument.FindFlag.FindBackward
  1587.        found = editor.find(text, flags)
  1588.        if found:
  1589.            self.statusBar().showMessage(f"Znaleziono '{text}'.")
  1590.        else:
  1591.            self.statusBar().showMessage(f"Nie znaleziono '{text}'. Zawijanie...")
  1592.            cursor = editor.textCursor()
  1593.            original_position = cursor.position() # Remember position before wrapping search
  1594.            cursor.clearSelection() # Clear selection before moving cursor position programmatically
  1595.            cursor.movePosition(cursor.MoveOperation.Start if direction == 'next' else cursor.MoveOperation.End)
  1596.            editor.setTextCursor(cursor)
  1597.            found_wrapped = editor.find(text, flags)
  1598.            if found_wrapped:
  1599.                self.statusBar().showMessage(f"Znaleziono '{text}' po zawinięciu.")
  1600.            else:
  1601.                 self.statusBar().showMessage(f"Nie znaleziono '{text}' w całym pliku.")
  1602.                 cursor.clearSelection()
  1603.                 cursor.setPosition(original_position)
  1604.                 editor.setTextCursor(cursor)
  1605.    def _show_find_bar(self):
  1606.        """Pokazuje pasek wyszukiwania."""
  1607.        if self.search_input.isVisible():
  1608.             self._hide_find_bar()
  1609.             return
  1610.        self.search_input.setVisible(True)
  1611.        self.find_next_button.setVisible(True)
  1612.        self.find_prev_button.setVisible(True)
  1613.        self.search_input.setFocus() # Ustaw kursor w polu wyszukiwania
  1614.    def _hide_find_bar(self):
  1615.        """Ukrywa pasek wyszukiwania."""
  1616.        if self.search_input.isVisible(): # Check if visible before hiding
  1617.            self.search_input.setVisible(False)
  1618.            self.find_next_button.setVisible(False)
  1619.            self.find_prev_button.setVisible(False)
  1620.            self.search_input.clear() # Wyczyść pole wyszukiwania
  1621.    def _show_settings_dialog(self):
  1622.        """Wyświetla okno dialogowe ustawień."""
  1623.        temp_settings = self.settings.copy()
  1624.        dialog = SettingsDialog(temp_settings, self) # Pass the copy
  1625.        font_size_label = QLabel("Rozmiar czcionki edytora:")
  1626.        font_size_spinbox = QSpinBox()
  1627.        font_size_spinbox.setRange(6, 72) # Typical font sizes
  1628.        font_size_spinbox.setValue(temp_settings.get("editor_font_size", 10))
  1629.        dialog_layout = dialog.layout()
  1630.        button_box_row_index = dialog_layout.rowCount() - 1
  1631.        dialog_layout.insertRow(button_box_row_index, font_size_label, font_size_spinbox)
  1632.        setattr(dialog, 'editor_font_size_spinbox', font_size_spinbox)
  1633.        if dialog.exec() == QDialog.DialogCode.Accepted:
  1634.            updated_settings = dialog.get_settings()
  1635.            updated_settings["editor_font_size"] = font_size_spinbox.value()
  1636.            self.settings = updated_settings
  1637.            self._apply_theme(self.settings.get("theme", "light")) # Apply theme
  1638.            self._apply_editor_font_size() # Apply new font size
  1639.            self._apply_view_settings() # Apply view settings (tree/console visibility)
  1640.            self._save_app_state() # Save updated settings and state
  1641.            self.statusBar().showMessage("Ustawienia zapisane.")
  1642.        else:
  1643.            self.statusBar().showMessage("Zmiany w ustawieniach anulowane.")
  1644.    def _show_about_dialog(self):
  1645.        """Wyświetla okno informacyjne 'O programie'."""
  1646.        QMessageBox.about(self, "O programie",
  1647.                          "<h2>Proste IDE</h2>"
  1648.                          "<p>Prosty edytor kodu napisany w Pythonie z użyciem biblioteki PyQt6.</p>"
  1649.                          "<p>Wersja: 1.0</p>"
  1650.                          "<p>Autor: [Twoje imię lub nazwa]</p>"
  1651.                          "<p>Dostępne funkcje:</p>"
  1652.                          "<ul>"
  1653.                          "<li>Przeglądanie plików i folderów</li>"
  1654.                          "<li>Otwieranie i edycja plików tekstowych/kodów</li>"
  1655.                          "<li>Kolorowanie składni (Python, JavaScript, HTML, CSS, C++, INI, JSON)</li>"
  1656.                          "<li>Uruchamianie plików Python/JavaScript (z konfigurowalnymi ścieżkami)</li>"
  1657.                          "<li>Uruchamianie skryptów npm (po otwarciu folderu z package.json)</li>"
  1658.                          "<li>**Wprowadzanie poleceń w konsoli**</li>" # Dodano
  1659.                          "<li>Zapisywanie plików (Zapisz, Zapisz jako, Zapisz wszystko)</li>"
  1660.                          "<li>Historia ostatnio otwieranych plików/folderów</li>"
  1661.                          "<li>Proste wyszukiwanie tekstu</li>"
  1662.                          "<li>Podstawowe motywy (Jasny/Ciemny)</li>"
  1663.                          "<li>Ukrywanie/pokazywanie paneli (Drzewko, Konsola)</li>"
  1664.                          "<li>Zmiana rozmiaru czcionki edytora</li>"
  1665.                          "<li>Operacje na plikach/folderach (Nowy, Zmień nazwę, Usuń, Duplikuj)</li>"
  1666.                          "</ul>"
  1667.                          "<p>Użyte technologie: PyQt6, Python, opcjonalnie qtawesome.</p>")
  1668.    def _update_run_button_menu(self):
  1669.        """Uaktualnia menu przypisane do QToolButton 'Uruchom'."""
  1670.        menu = QMenu(self)
  1671.        menu.addAction(self.action_run_file)
  1672.        if self.node_scripts:
  1673.             menu.addSeparator()
  1674.             menu.addSection("Skrypty npm:")
  1675.             sorted_scripts = sorted(self.node_scripts.keys())
  1676.             for script_name in sorted_scripts:
  1677.                action = QAction(script_name, self)
  1678.                action.triggered.connect(lambda checked, name=script_name: self._run_npm_script(name))
  1679.                menu.addAction(action)
  1680.        else:
  1681.             no_scripts_action = QAction("Brak skryptów npm (package.json)", self)
  1682.             no_scripts_action.setEnabled(False) # Wyłącz akcję
  1683.             menu.addAction(no_scripts_action)
  1684.        self.run_toolbutton.setMenu(menu)
  1685.    def _run_current_file(self):
  1686.        """Uruchamia aktualnie otwarty plik."""
  1687.        current_widget = self.tab_widget.currentWidget()
  1688.        if not isinstance(current_widget, QPlainTextEdit):
  1689.            self._append_console_output("Brak aktywnego pliku do uruchomienia.", is_error=True)
  1690.            return
  1691.        file_path = next((path for path, editor_widget in self.open_files.items() if editor_widget is current_widget), None)
  1692.        if not file_path or not os.path.exists(file_path) or not os.path.isfile(file_path):
  1693.             self._append_console_output("Ścieżka aktywnego pliku jest nieprawidłowa lub plik nie istnieje.", is_error=True)
  1694.             return
  1695.        if current_widget.document().isModified():
  1696.             self._append_console_output("Zapisywanie pliku przed uruchomieniem...")
  1697.             if not self._save_file(current_widget, file_path):
  1698.                 self._append_console_output("Zapisywanie nie powiodło się. Anulowano uruchomienie.", is_error=True)
  1699.                 return
  1700.        language = self._get_language_from_path(file_path)
  1701.        command = []
  1702.        working_dir = os.path.dirname(file_path) # Directory of the file
  1703.        python_path = self.settings.get("python_path")
  1704.        node_path = self.settings.get("node_path")
  1705.        if language == 'python':
  1706.            interpreter = python_path if python_path and os.path.exists(python_path) else "python"
  1707.            command = [interpreter, "-u", file_path]
  1708.        elif language == 'javascript':
  1709.            interpreter = node_path if node_path and os.path.exists(node_path) else "node"
  1710.            command = [interpreter, file_path]
  1711.        elif language in ['html', 'css']:
  1712.             self._append_console_output(f"Otwieranie pliku {os.path.basename(file_path)} w domyślnej przeglądarce...")
  1713.             try:
  1714.                  QDesktopServices.openUrl(QUrl.fromLocalFile(file_path))
  1715.                  self.statusBar().showMessage(f"Otwarto plik w przeglądarce: {os.path.basename(file_path)}")
  1716.             except Exception as e:
  1717.                  self._append_console_output(f"Błąd podczas otwierania pliku w przeglądarce: {e}", is_error=True)
  1718.                  self.statusBar().showMessage("Błąd otwierania w przeglądarce.")
  1719.             return # Do not run as a process
  1720.        else:
  1721.            self._append_console_output(f"Nieznany typ pliku do uruchomienia: {language}", is_error=True)
  1722.            return
  1723.        if command:
  1724.             self._run_command(command, working_dir)
  1725.    def _check_package_json(self, folder_path):
  1726.        """Checks if package.json exists in the folder and parses scripts."""
  1727.        if not folder_path or not os.path.isdir(folder_path):
  1728.             self.node_scripts = {} # Clear any previous scripts
  1729.             self._update_run_button_menu() # Update run menu button
  1730.             return
  1731.        package_json_path = os.path.join(folder_path, 'package.json')
  1732.        self.node_scripts = {} # Clear previous scripts
  1733.        if os.path.exists(package_json_path):
  1734.            try:
  1735.                with open(package_json_path, 'r', encoding='utf-8') as f:
  1736.                    package_data = json.load(f)
  1737.                scripts = package_data.get('scripts', {})
  1738.                if isinstance(scripts, dict) and scripts:
  1739.                    self.node_scripts = scripts
  1740.                    self._append_console_output(f"Znaleziono {len(scripts)} skryptów w package.json w {os.path.basename(folder_path)}.")
  1741.                else:
  1742.                     self._append_console_output(f"Znaleziono package.json w {os.path.basename(folder_path)}, ale brak skryptów w sekcji 'scripts'.")
  1743.            except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
  1744.                self._append_console_output(f"Błąd podczas parsowania package.json w {os.path.basename(folder_path)}:\n{e}", is_error=True)
  1745.                self.node_scripts = {} # Ensure scripts are empty on error
  1746.        else:
  1747.             pass # Silence is golden
  1748.        self._update_run_button_menu() # After checking package.json, update the run menu button
  1749.    def _run_npm_script(self, script_name):
  1750.        """Runs the specified npm script."""
  1751.        if not self.current_project_dir or not os.path.isdir(self.current_project_dir):
  1752.            self._append_console_output("Brak otwartego folderu projektu do uruchomienia skryptu npm.", is_error=True)
  1753.            return
  1754.        if script_name not in self.node_scripts:
  1755.            self._append_console_output(f"Skrypt '{script_name}' nie znaleziono w package.json otwartego projektu.", is_error=True)
  1756.            return
  1757.        node_path = self.settings.get("node_path")
  1758.        npm_command = "npm" # Default command
  1759.        if node_path and os.path.exists(node_path):
  1760.             node_dir = os.path.dirname(node_path)
  1761.             npm_candidates = [os.path.join(node_dir, 'npm')]
  1762.             if platform.system() == "Windows":
  1763.                  npm_candidates.append(os.path.join(node_dir, 'npm.cmd'))
  1764.             found_npm = None
  1765.             for candidate in npm_candidates:
  1766.                  if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
  1767.                       found_npm = candidate
  1768.                       break
  1769.             if found_npm:
  1770.                  npm_command = found_npm
  1771.             else:
  1772.                  self._append_console_output(f"Ostrzeżenie: Nie znaleziono 'npm' (lub nie jest wykonywalne) obok skonfigurowanej ścieżki node '{node_path}'. Polegam na systemowym PATH.", is_error=True)
  1773.        command = [npm_command, "run", script_name]
  1774.        working_dir = self.current_project_dir # Npm scripts are run in the project directory
  1775.        self._run_command(command, working_dir)
  1776.    def _run_console_command(self):
  1777.        """Uruchamia komendę wpisaną w polu tekstowym konsoli."""
  1778.        command_text = self.console_input.text().strip()
  1779.        if not command_text:
  1780.            return # Nic nie wpisano
  1781.        self.console_input.clear() # Wyczyść pole wprowadzania
  1782.        self._append_console_output(f"> {command_text}") # Wyświetl wpisaną komendę w konsoli
  1783.        try:
  1784.            command = shlex.split(command_text)
  1785.        except ValueError as e:
  1786.             self._append_console_output(f"Błąd parsowania komendy: {e}", is_error=True)
  1787.             self.statusBar().showMessage("Błąd parsowania komendy.")
  1788.             return
  1789.        if not command:
  1790.             self._append_console_output("Błąd: Pusta komenda po parsowaniu.", is_error=True)
  1791.             self.statusBar().showMessage("Błąd: Pusta komenda.")
  1792.             return
  1793.        working_dir = self.current_project_dir if self.current_project_dir and os.path.isdir(self.current_project_dir) else os.getcwd()
  1794.        self._run_command(command, working_dir)
  1795.    def _run_command(self, command, working_dir):
  1796.        """Uruchamia podaną komendę w QProcess."""
  1797.        if self.process.state() != QProcess.ProcessState.NotRunning:
  1798.            self._append_console_output("Inny proces jest już uruchomiony. Zakończ go, aby uruchomić nowy.", is_error=True)
  1799.            self._append_console_output("Możesz zatrzymać proces używając przycisku stop lub wpisując polecenie 'stop' w konsoli (jeśli obsługiwane przez program).")
  1800.            return
  1801.        command_str = shlex.join(command) # Better command formatting
  1802.        self._append_console_output(f"Uruchamianie: {command_str}\nw katalogu: {working_dir}\n---")
  1803.         self.statusBar().showMessage("Proces uruchomiony...")
  1804.         try:
  1805.             if not command:
  1806.                  self._append_console_output("Komenda do uruchomienia jest pusta.", is_error=True)
  1807.                  self.statusBar().showMessage("Błąd: Pusta komenda.")
  1808.                  return
  1809.             program = command[0]
  1810.             arguments = command[1:]
  1811.             self.process.setWorkingDirectory(working_dir)
  1812.             process_environment = QProcessEnvironment.systemEnvironment()
  1813.             current_path = process_environment.value("PATH", "") # Get system PATH
  1814.             paths_to_prepend = []
  1815.             py_path = self.settings.get("python_path")
  1816.             if py_path and os.path.exists(py_path):
  1817.                  py_dir = os.path.dirname(py_path)
  1818.                  current_path_dirs = [os.path.normcase(p) for p in current_path.split(os.pathsep) if p]
  1819.                  if os.path.normcase(py_dir) not in current_path_dirs:
  1820.                       paths_to_prepend.append(py_dir)
  1821.             node_path = self.settings.get("node_path")
  1822.             if node_path and os.path.exists(node_path):
  1823.                  node_dir = os.path.dirname(node_path)
  1824.                  if os.path.normcase(node_dir) not in current_path_dirs:
  1825.                      paths_to_prepend.append(node_dir)
  1826.             if paths_to_prepend:
  1827.                  new_path = os.pathsep.join(paths_to_prepend) + (os.pathsep + current_path if current_path else "")
  1828.             else:
  1829.                  new_path = current_path # No new paths to add
  1830.             process_environment.insert("PATH", new_path) # Overwrite or add PATH
  1831.             if platform.system() == "Windows" and process_environment.value("Path") is not None:
  1832.                  if process_environment.value("Path") != current_path: # Avoid unnecessary update
  1833.                       process_environment.insert("Path", new_path)
  1834.             self.process.setProcessEnvironment(process_environment)
  1835.             self.process.start(program, arguments)
  1836.             if not self.process.waitForStarted(1000):
  1837.                  error_string = self.process.errorString()
  1838.                  self._append_console_output(f"Nie udało się uruchomić procesu '{program}': {error_string}", is_error=True)
  1839.                  self.statusBar().showMessage(f"Błąd uruchamiania: {program}")
  1840.                  return # Process didn't start, exit
  1841.        except Exception as e:
  1842.            self._append_console_output(f"Wystąpił nieoczekiwany błąd podczas próby uruchomienia:\n{e}", is_error=True)
  1843.            self.statusBar().showMessage("Błąd podczas uruchamiania.")
  1844.    def _append_console_output(self, text, is_error=False):
  1845.        """Adds text to the console, optionally formatting as an error."""
  1846.        cursor = self.console.textCursor()
  1847.        cursor.movePosition(cursor.MoveOperation.End)
  1848.        original_fmt = cursor.charFormat()
  1849.        fmt = QTextCharFormat()
  1850.        if is_error:
  1851.            fmt.setForeground(QColor("#DC143C")) # Crimson
  1852.        cursor.setCharFormat(fmt)
  1853.        text_to_insert = text
  1854.        if text and not text.endswith('\n'):
  1855.             text_to_insert += '\n'
  1856.        cursor.insertText(text_to_insert)
  1857.        cursor.setCharFormat(original_fmt)
  1858.        self.console.ensureCursorVisible()
  1859.    def _handle_stdout(self):
  1860.        """Reads standard output of the process and displays it in the console."""
  1861.        while self.process.bytesAvailable(): # Correct usage
  1862.            data = self.process.readAllStandardOutput()
  1863.            try:
  1864.                 text = bytes(data).decode('utf-8')
  1865.            except UnicodeDecodeError:
  1866.                 try:
  1867.                     text = bytes(data).decode('latin-1')
  1868.                 except Exception:
  1869.                     text = bytes(data).decode('utf-8', errors='replace') # Replace unknown characters
  1870.            self._append_console_output(text)
  1871.    def _handle_stderr(self):
  1872.        """Reads standard error of the process and displays it in the console (in red)."""
  1873.        while self.process.bytesAvailable(): # Correct usage
  1874.            data = self.process.readAllStandardError()
  1875.            try:
  1876.                 text = bytes(data).decode('utf-8')
  1877.            except UnicodeDecodeError:
  1878.                 try:
  1879.                     text = bytes(data).decode('latin-1')
  1880.                 except Exception:
  1881.                     text = bytes(data).decode('utf-8', errors='replace') # Replace unknown characters
  1882.            self._append_console_output(text, is_error=True)
  1883.    def _handle_process_finished(self, exitCode, exitStatus):
  1884.        """Handles the process finishing."""
  1885.        self._handle_stdout()
  1886.        self._handle_stderr()
  1887.        self._append_console_output("\n--- Zakończono proces ---") # Add a clear separator
  1888.        if exitStatus == QProcess.ExitStatus.NormalExit:
  1889.            self._append_console_output(f"Proces zakończył się z kodem wyjścia: {exitCode}")
  1890.            self.statusBar().showMessage(f"Proces zakończył się. Kod wyjścia: {exitCode}")
  1891.        else:
  1892.            self._append_console_output(f"Proces zakończył się awaryjnie z kodem wyjścia: {exitCode}", is_error=True)
  1893.            self.statusBar().showMessage(f"Proces zakończył się awaryjnie. Kod wyjścia: {exitCode}")
  1894.        if self.settings.get("show_console", True):
  1895.             self._show_console_panel() # Ensure console is visible and correctly sized
  1896.    def _copy_console(self):
  1897.         """Copies the entire content of the console to the clipboard."""
  1898.         clipboard = QApplication.clipboard()
  1899.         clipboard.setText(self.console.toPlainText())
  1900.         self.statusBar().showMessage("Zawartość konsoli skopiowana do schowka.")
  1901.    def _toggle_tree_view(self, checked):
  1902.         """Shows/hides the file tree panel."""
  1903.         self.main_splitter.widget(0).setVisible(checked)
  1904.         self.settings["show_tree"] = checked # Save state
  1905.         self._save_app_state() # Save settings change
  1906.         self._apply_view_settings() # Re-apply sizes to ensure panels aren't too small/large
  1907.     def _toggle_console(self, checked):
  1908.          """Shows/hides the console panel."""
  1909.          self.right_splitter.widget(1).setVisible(checked)
  1910.          self.settings["show_console"] = checked # Save state
  1911.          self._save_app_state() # Save settings change
  1912.          if checked:
  1913.               self._show_console_panel() # Use method that sets sizes
  1914.     def _show_console_panel(self):
  1915.         """Ensures the console panel is visible and has a reasonable size."""
  1916.         self.right_splitter.widget(1).setVisible(True)
  1917.         self.action_toggle_console.setChecked(True)
  1918.         sizes = self.right_splitter.sizes()
  1919.         if len(sizes) == 2:
  1920.             total_height = sum(sizes)
  1921.             min_console_height = 100
  1922.             min_editor_height = 100
  1923.             if sizes[1] < min_console_height or (sizes[1] < 10 and self.settings.get("show_console", True)):
  1924.                  if total_height > min_console_height + min_editor_height:
  1925.                       self.right_splitter.setSizes([total_height - min_console_height, min_console_height])
  1926.                  elif total_height > 200: # Ensure some minimal height if window is small
  1927.                       self.right_splitter.setSizes([total_height // 2, total_height // 2]) # Split equally
  1928.     def _apply_view_settings(self):
  1929.         """Apply panel visibility settings after loading."""
  1930.         show_tree = self.settings.get("show_tree", True)
  1931.         show_console = self.settings.get("show_console", True)
  1932.         self.main_splitter.widget(0).setVisible(show_tree)
  1933.         self.action_toggle_tree.setChecked(show_tree)
  1934.         self.right_splitter.widget(1).setVisible(show_console)
  1935.         self.action_toggle_console.setChecked(show_console)
  1936.         main_sizes = self.main_splitter.sizes()
  1937.         if len(main_sizes) == 2:
  1938.              total_width = sum(main_sizes)
  1939.              min_tree_width = 150 # Minimal reasonable width for the tree view
  1940.              min_right_panel_width = 200 # Minimal width for the editor/console side
  1941.              if main_sizes[0] < min_tree_width or (main_sizes[0] < 10 and show_tree):
  1942.                   if total_width > min_tree_width + min_right_panel_width:
  1943.                        self.main_splitter.setSizes([min_tree_width, total_width - min_tree_width])
  1944.                   elif total_width > 300: # Ensure some minimal width if window is small
  1945.                        self.main_splitter.setSizes([total_width // 3, 2 * total_width // 3]) # Split approx 1/3, 2/3
  1946.         right_sizes = self.right_splitter.sizes()
  1947.         if len(right_sizes) == 2:
  1948.              total_height = sum(right_sizes)
  1949.              min_console_height = 100
  1950.              min_editor_height = 100 # Minimal height for the editor area
  1951.              if right_sizes[1] < min_console_height or (right_sizes[1] < 10 and show_console):
  1952.                    if total_height > min_console_height + min_editor_height:
  1953.                        self.right_splitter.setSizes([total_height - min_console_height, min_console_height])
  1954.                    elif total_height > 200: # Ensure some minimal height if window is small
  1955.                        self.right_splitter.setSizes([total_height // 2, total_height // 2]) # Split equally
  1956.              elif right_sizes[0] < min_editor_height and show_console and total_height > min_console_height + min_editor_height:
  1957.                    self.right_splitter.setSizes([min_editor_height, total_height - min_editor_height])
  1958.     def _apply_editor_font_size(self):
  1959.         """Apply the editor font size to all open editors and the console."""
  1960.         font_size = self.settings.get("editor_font_size", 10)
  1961.         new_font = QFont("Courier New", font_size) # Use Courier New, change size
  1962.         self.base_editor_font = new_font # Update the base font
  1963.         self.console.setFont(new_font)
  1964.         self.console_input.setFont(new_font)
  1965.         for editor_widget in self.open_files.values():
  1966.             editor_widget.setFont(new_font)
  1967.             if hasattr(editor_widget.document(), '_syntax_highlighter'):
  1968.                  editor_widget.document().rehighlight()
  1969.     def _apply_theme(self, theme_name):
  1970.         """Applies the selected color theme."""
  1971.         if theme_name == "dark":
  1972.             self.setStyleSheet("""
  1973.                QMainWindow, QWidget {
  1974.                    background-color: #2E2E2E;
  1975.                    color: #D3D3D3;
  1976.                }
  1977.                QMenuBar {
  1978.                    background-color: #3C3C3C;
  1979.                    color: #D3D3D3;
  1980.                }
  1981.                QMenuBar::item:selected {
  1982.                    background-color: #505050;
  1983.                }
  1984.                QMenu {
  1985.                    background-color: #3C3C3C;
  1986.                    color: #D3D3D3;
  1987.                    border: 1px solid #505050;
  1988.                }
  1989.                QMenu::item:selected {
  1990.                    background-color: #505050;
  1991.                }
  1992.                QToolBar {
  1993.                    background-color: #3C3C3C;
  1994.                    color: #D3D3D3;
  1995.                    spacing: 5px;
  1996.                    padding: 2px;
  1997.                }
  1998.                QToolButton { /* Buttons on toolbar */
  1999.                    background-color: transparent;
  2000.                    border: 1px solid transparent;
  2001.                    padding: 3px;
  2002.                    border-radius: 4px; /* Slight rounding */
  2003.                }
  2004.                QToolButton:hover {
  2005.                    border: 1px solid #505050;
  2006.                    background-color: #454545;
  2007.                }
  2008.                 QToolButton:pressed {
  2009.                     background-color: #404040;
  2010.                 }
  2011.                QPushButton { /* Standard buttons (e.g., Run, Find) */
  2012.                    background-color: #505050;
  2013.                    color: #D3D3D3;
  2014.                    border: 1px solid #606060;
  2015.                    padding: 4px 8px;
  2016.                    border-radius: 4px;
  2017.                }
  2018.                QPushButton:hover {
  2019.                    background-color: #606060;
  2020.                }
  2021.                QPushButton:pressed {
  2022.                    background-color: #404040;
  2023.                }
  2024.                QStatusBar {
  2025.                    background-color: #3C3C3C;
  2026.                    color: #D3D3D3;
  2027.                }
  2028.                QSplitter::handle {
  2029.                    background-color: #505050;
  2030.                }
  2031.                QSplitter::handle:horizontal {
  2032.                    width: 5px;
  2033.                }
  2034.                QSplitter::handle:vertical {
  2035.                    height: 5px;
  2036.                }
  2037.                QTreeView {
  2038.                    background-color: #1E1E1E;
  2039.                    color: #D3D3D3;
  2040.                    border: 1px solid #3C3C3C;
  2041.                    alternate-background-color: #252525; /* Alternating row colors */
  2042.                }
  2043.                QTreeView::item:selected {
  2044.                    background-color: #007acc; /* Selection color (VS Code blue) */
  2045.                    color: white;
  2046.                }
  2047.                QTreeView::branch:selected {
  2048.                     background-color: #007acc; /* Selection color for branch indicator */
  2049.                }
  2050.                QTabWidget::pane { /* Frame around tab content area */
  2051.                    border: 1px solid #3C3C3C;
  2052.                    background-color: #1E1E1E;
  2053.                }
  2054.                QTabWidget::tab-bar:top {
  2055.                    left: 5px; /* Leave some space on the left */
  2056.                }
  2057.                QTabBar::tab {
  2058.                    background: #3C3C3C;
  2059.                    color: #D3D3D3;
  2060.                    border: 1px solid #3C3C3C;
  2061.                    border-bottom-color: #1E1E1E; /* Blend bottom border with pane background */
  2062.                    border-top-left-radius: 4px;
  2063.                    border-top-right-radius: 4px;
  2064.                    padding: 4px 8px;
  2065.                    margin-right: 1px;
  2066.                }
  2067.                QTabBar::tab:selected {
  2068.                    background: #1E1E1E; /* Match pane background for selected tab */
  2069.                    border-bottom-color: #1E1E1E;
  2070.                }
  2071.                QTabBar::tab:hover {
  2072.                    background: #454545; /* Hover effect */
  2073.                }
  2074.                /* Optional: Style for close button */
  2075.                /* QTabBar::close-button { image: url(:/path/to/close_icon_dark.png); } */
  2076.                QPlainTextEdit { /* Code Editor */
  2077.                    background-color: #1E1E1E;
  2078.                    color: #D3D3D3;
  2079.                    border: none; /* Border is on QTabWidget::pane */
  2080.                    selection-background-color: #007acc; /* Selection color */
  2081.                    selection-color: white;
  2082.                }
  2083.                 QPlainTextEdit[readOnly="true"] { /* Console */
  2084.                     background-color: #1E1E1E; /* Same as editor background */
  2085.                     color: #CCCCCC; /* Console text color */
  2086.                      selection-background-color: #007acc;
  2087.                      selection-color: white;
  2088.                 }
  2089.                QLineEdit { /* Search bar, input fields in settings */
  2090.                    background-color: #3C3C3C;
  2091.                    color: #D3D3D3;
  2092.                    border: 1px solid #505050;
  2093.                    padding: 2px;
  2094.                     selection-background-color: #007acc;
  2095.                     selection-color: white;
  2096.                     border-radius: 3px; /* Slight rounding */
  2097.                }
  2098.                 QComboBox { /* ComboBox in settings */
  2099.                    background-color: #3C3C3C;
  2100.                    color: #D3D3D3;
  2101.                    border: 1px solid #505050;
  2102.                    padding: 2px;
  2103.                     border-radius: 3px;
  2104.                 }
  2105.                 QComboBox::drop-down {
  2106.                    border: 0px; /* Remove default arrow */
  2107.                 }
  2108.                  QComboBox QAbstractItemView { /* Dropdown list of ComboBox */
  2109.                     background-color: #3C3C3C;
  2110.                     color: #D3D3D3;
  2111.                     selection-background-color: #007acc;
  2112.                     border: 1px solid #505050;
  2113.                 }
  2114.                 QDialog { /* Settings dialog window */
  2115.                     background-color: #2E2E2E;
  2116.                     color: #D3D3D3;
  2117.                 }
  2118.                  QLabel { /* Labels in dialogs */
  2119.                      color: #D3D3D3;
  2120.                  }
  2121.                  QDialogButtonBox QPushButton { /* Buttons in dialogs */
  2122.                      background-color: #505050;
  2123.                      color: #D3D3D3;
  2124.                      border: 1px solid #606060;
  2125.                      padding: 5px 10px;
  2126.                      border-radius: 4px;
  2127.                  }
  2128.                   QDialogButtonBox QPushButton:hover {
  2129.                      background-color: #606060;
  2130.                   }
  2131.                    QDialogButtonBox QPushButton:pressed {
  2132.                       background-color: #404040;
  2133.                   }
  2134.                   QSpinBox { /* Spinbox in settings */
  2135.                       background-color: #3C3C3C;
  2136.                       color: #D3D3D3;
  2137.                       border: 1px solid #505050;
  2138.                       padding: 2px;
  2139.                        selection-background-color: #007acc;
  2140.                        selection-color: white;
  2141.                         border-radius: 3px;
  2142.                   }
  2143.                   QFrame { /* Frames like the one in SettingsDialog */
  2144.                       border: none;
  2145.                   }
  2146.                /* Style for the console input field */
  2147.                QLineEdit[placeholderText="Wpisz polecenie..."] {
  2148.                    background-color: #3C3C3C;
  2149.                    color: #D3D3D3;
  2150.                    border: 1px solid #505050;
  2151.                    padding: 2px;
  2152.                    margin: 0px; /* Remove default margins */
  2153.                    border-radius: 0px; /* No rounding for console input */
  2154.                }
  2155.            """)
  2156.         elif theme_name == "light":
  2157.              self.setStyleSheet("")
  2158.         self.statusBar().showMessage(f"Zmieniono motyw na: {theme_name.capitalize()}")
  2159. if __name__ == '__main__':
  2160.     app = QApplication(sys.argv)
  2161.     QLocale.setDefault(QLocale(QLocale.Language.Polish, QLocale.Country.Poland))
  2162.     main_window = IDEWindow()
  2163.     main_window.show()
  2164.     sys.exit(app.exec())
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement