Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import sys
- import os
- import json
- import subprocess
- import re
- import platform
- import shutil # Do kopiowania plików
- import shlex # Do bezpiecznego formatowania komend
- from PyQt6.QtWidgets import (
- QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
- QSplitter, QTreeView, QTabWidget, QPlainTextEdit,
- QPushButton, QLineEdit, QFileDialog, QMenuBar, QToolBar, QStatusBar,
- QMessageBox, QMenu, QStyleFactory, QDialog, QFormLayout,
- QLabel, QDialogButtonBox, QComboBox, QToolButton,
- QInputDialog, QSpinBox, QSizePolicy, QAbstractItemView,
- QFrame # Dodano do okna ustawień
- )
- from PyQt6.QtGui import (
- QIcon, QAction, QKeySequence, QTextCharFormat, QFont,
- QSyntaxHighlighter, QTextDocument, QColor, QFileSystemModel,
- QDesktopServices, # Do otwierania plików w domyślnych aplikacjach
- QPalette # Do motywów
- )
- from PyQt6.QtCore import (
- QDir, Qt, QProcess, QSettings, QFileInfo, QThread, pyqtSignal, QTimer, QSize,
- QStandardPaths, QUrl, QLocale, QCoreApplication, QProcessEnvironment
- )
- try:
- import qtawesome as qta
- except ImportError:
- qta = None
- print("Zainstaluj qtawesome ('pip install qtawesome') dla lepszych ikon.", file=sys.stderr)
- APP_DIR = os.path.dirname(os.path.abspath(__file__))
- DATA_DIR = os.path.join(APP_DIR, 'data')
- PROJECTS_DIR = os.path.join(APP_DIR, 'projects')
- SETTINGS_FILE = os.path.join(DATA_DIR, 'settings.json')
- RECENTS_FILE = os.path.join(DATA_DIR, 'recents.json')
- os.makedirs(DATA_DIR, exist_ok=True)
- os.makedirs(PROJECTS_DIR, exist_ok=True)
- FORMAT_DEFAULT = QTextCharFormat()
- FORMAT_KEYWORD = QTextCharFormat()
- FORMAT_KEYWORD.setForeground(QColor("#000080")) # Navy
- FORMAT_STRING = QTextCharFormat()
- FORMAT_STRING.setForeground(QColor("#008000")) # Green
- FORMAT_COMMENT = QTextCharFormat()
- FORMAT_COMMENT.setForeground(QColor("#808080")) # Gray
- FORMAT_COMMENT.setFontItalic(True)
- FORMAT_FUNCTION = QTextCharFormat()
- FORMAT_FUNCTION.setForeground(QColor("#0000FF")) # Blue
- FORMAT_CLASS = QTextCharFormat()
- FORMAT_CLASS.setForeground(QColor("#A52A2A")) # Brown
- FORMAT_CLASS.setFontWeight(QFont.Weight.Bold)
- FORMAT_NUMBERS = QTextCharFormat()
- FORMAT_NUMBERS.setForeground(QColor("#FF0000")) # Red
- FORMAT_OPERATOR = QTextCharFormat()
- FORMAT_OPERATOR.setForeground(QColor("#A62929")) # Dark Red
- FORMAT_BUILTIN = QTextCharFormat()
- FORMAT_BUILTIN.setForeground(QColor("#008080")) # Teal
- FORMAT_SECTION = QTextCharFormat() # Dla sekcji w INI
- FORMAT_SECTION.setForeground(QColor("#800080")) # Purple
- FORMAT_SECTION.setFontWeight(QFont.Weight.Bold)
- FORMAT_PROPERTY = QTextCharFormat() # Dla kluczy/właściwości w INI/JSON
- FORMAT_PROPERTY.setForeground(QColor("#B8860B")) # DarkGoldenrod
- HIGHLIGHTING_RULES = {
- 'python': {
- 'keywords': ['and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else',
- 'except', 'False', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'None',
- 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield'],
- 'builtins': ['print', 'len', 'range', 'list', 'dict', 'tuple', 'set', 'str', 'int', 'float', 'bool', 'open', 'isinstance'],
- 'patterns': [
- (r'\b[A-Za-z_][A-Za-z0-9_]*\s*\(', FORMAT_FUNCTION), # Funkcje (proste wykrycie, litera/podkreślnik na początku)
- (r'\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b', FORMAT_CLASS), # Klasy
- (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS), # Liczby
- (r'[+\-*/=<>!&|]', FORMAT_OPERATOR), # Operatory
- (r'".*?"', FORMAT_STRING), # Stringi w cudzysłowach podwójnych
- (r"'.*?'", FORMAT_STRING), # Stringi w cudzysłowach pojedynczych
- (r'#.*', FORMAT_COMMENT), # Komentarze liniowe
- ]
- },
- 'javascript': {
- 'keywords': ['abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue',
- 'debugger', 'default', 'delete', 'do', 'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final',
- 'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', 'int', 'interface',
- 'let', 'long', 'native', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static',
- 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 'typeof', 'var', 'void',
- 'volatile', 'while', 'with', 'yield'],
- 'builtins': ['console', 'log', 'warn', 'error', 'info', 'Math', 'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'RegExp', 'JSON', 'Promise', 'setTimeout', 'setInterval'], # Przykładowe
- 'patterns': [
- (r'\b[A-Za-z_][A-Za-z0-9_]*\s*\(', FORMAT_FUNCTION), # Funkcje (proste wykrycie)
- (r'\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b', FORMAT_CLASS), # Klasy
- (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS), # Liczby
- (r'[+\-*/=<>!&|]', FORMAT_OPERATOR), # Operatory
- (r'".*?"', FORMAT_STRING), # Stringi w cudzysłowach podwójnych
- (r"'.*?'", FORMAT_STRING), # Stringi w cudzysłowach pojedynczych
- (r'//.*', FORMAT_COMMENT), # Komentarze liniowe
- ]
- },
- 'html': {
- 'keywords': [], # HTML nie ma tradycyjnych słów kluczowych w ten sposób
- 'builtins': [], # Encje HTML można potraktować jako builtins
- 'patterns': [
- (r'<[^>]+>', FORMAT_KEYWORD), # Tagi HTML (uproszczone, bez atrybutów)
- (r'[a-zA-Z0-9_-]+\s*=', FORMAT_OPERATOR), # Znaki '=' w atrybutach
- (r'".*?"', FORMAT_STRING), # Wartości atrybutów
- (r"'.*?'", FORMAT_STRING), # Wartości atrybutów
- (r'&[a-zA-Z0-9]+;', FORMAT_BUILTIN), # Encje HTML
- (r'<!--.*?-->', FORMAT_COMMENT, re.DOTALL), # Komentarze (z re.DOTALL, aby objęły wiele linii)
- ]
- },
- 'css': {
- 'keywords': [],
- 'builtins': [], # Selektory ID
- 'patterns': [
- (r'\.[a-zA-Z0-9_-]+', FORMAT_CLASS), # Selektory klas
- (r'#[a-zA-Z0-9_-]+', FORMAT_BUILTIN), # Selektory ID
- (r'[a-zA-Z0-9_-]+\s*:', FORMAT_KEYWORD), # Właściwości CSS
- (r';', FORMAT_OPERATOR), # Średniki
- (r'\{|\}', FORMAT_OPERATOR), # Nawiasy klamrowe
- (r'\(|\)', FORMAT_OPERATOR), # Nawiasy okrągłe (np. w rgb())
- (r'\b\d+(\.\d*)?(px|em|%|vh|vw|rem|pt|cm|mm)?\b', FORMAT_NUMBERS), # Liczby z jednostkami
- (r'#[0-9a-fA-F]{3,6}', FORMAT_NUMBERS), # Kolory HEX
- (r'".*?"', FORMAT_STRING), # Wartości stringów
- (r"'.*?'", FORMAT_STRING), # Wartości stringów
- ]
- },
- 'c++': {
- 'keywords': ['alignas', 'alignof', 'and', 'and_eq', 'asm', 'atomic_cancel', 'atomic_commit', 'atomic_noexcept', 'auto',
- 'bitand', 'bitor', 'bool', 'break', 'case', 'catch', 'char', 'char8_t', 'char16_t', 'char32_t', 'class',
- 'compl', 'concept', 'const', 'consteval', 'constexpr', 'constinit', 'const_cast', 'continue', 'co_await',
- 'co_return', 'decltype', 'default', 'delete', 'do', 'double', 'dynamic_cast', 'else', 'enum',
- 'explicit', 'export', 'extern', 'false', 'float', 'for', 'friend', 'goto', 'if', 'inline', 'int', 'long',
- 'mutable', 'namespace', 'new', 'noexcept', 'not', 'not_eq', 'nullptr', 'operator', 'or', 'or_eq', 'private',
- 'protected', 'public', 'reflexpr', 'register', 'reinterpret_cast', 'requires', 'return', 'short', 'signed',
- 'sizeof', 'static', 'static_assert', 'static_cast', 'struct', 'switch', 'synchronized', 'template',
- 'this', 'thread_local', 'throw', 'true', 'try', 'typedef', 'typeid', 'typename', 'union', 'unsigned',
- 'using', 'virtual', 'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq'],
- 'builtins': ['cout', 'cin', 'endl', 'string', 'vector', 'map', 'set', 'array', 'queue', 'stack', 'pair', 'algorithm', 'iostream', 'fstream', 'sstream', 'cmath', 'cstdlib', 'cstdio', 'ctime'], # Przykładowe popularne
- 'patterns': [
- (r'\b[A-Za-z_][A-Za-z0-9_]*\s*\(', FORMAT_FUNCTION), # Funkcje (proste wykrycie)
- (r'\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b', FORMAT_CLASS), # Klasy
- (r'\bstruct\s+([A-Za-z_][A-Za-z0-9_]*)\b', FORMAT_CLASS), # Struktury
- (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS), # Liczby
- (r'[+\-*/=<>!&|%^~?:]', FORMAT_OPERATOR), # Operatory
- (r'".*?"', FORMAT_STRING), # Stringi w cudzysłowach podwójnych
- (r"'.*?'", FORMAT_STRING), # Stringi w cudzysłowach pojedynczych (pojedyncze znaki)
- (r'//.*', FORMAT_COMMENT), # Komentarze liniowe
- ]
- },
- 'ini': { # Nowe reguły dla INI
- 'keywords': [], # Brak tradycyjnych słów kluczowych
- 'builtins': [], # Brak typowych builtins
- 'patterns': [
- (r'^\[.*?\]', FORMAT_SECTION), # Sekcje [section]
- (r'^[a-zA-Z0-9_-]+\s*=', FORMAT_PROPERTY), # Klucze property=
- (r';.*', FORMAT_COMMENT), # Komentarze po średniku
- (r'#.*', FORMAT_COMMENT), # Komentarze po krzyżyku
- (r'[+\-*/=<>!&|]', FORMAT_OPERATOR), # Operatory (np. =)
- (r'=\s*".*?"', FORMAT_STRING), # "value"
- (r"=\s*'.*?'", FORMAT_STRING), # 'value'
- (r'=\s*[^;#"\'].*', FORMAT_STRING), # value without quotes or comments/sections
- ]
- },
- 'json': { # Nowe reguły dla JSON
- 'keywords': ['true', 'false', 'null'], # Literały JSON
- 'builtins': [], # Brak typowych builtins
- 'patterns': [
- (r'"(?:[^"\\]|\\.)*"\s*:', FORMAT_PROPERTY), # Klucze w cudzysłowach z następującym ':'
- (r'".*?"', FORMAT_STRING), # Wartości stringów (muszą być po kluczach, żeby nie nadpisać klucza)
- (r'\b-?\d+(\.\d+)?([eE][+-]?\d+)?\b', FORMAT_NUMBERS), # Liczby
- (r'\{|\}|\[|\]|:|,', FORMAT_OPERATOR), # Nawiasy, dwukropek, przecinek
- ]
- }
- }
- class CodeSyntaxHighlighter(QSyntaxHighlighter):
- def __init__(self, parent: QTextDocument, language: str):
- super().__init__(parent)
- self._language = language.lower()
- self._rules = []
- lang_config = HIGHLIGHTING_RULES.get(self._language, {})
- keywords = lang_config.get('keywords', [])
- builtins = lang_config.get('builtins', [])
- patterns = lang_config.get('patterns', [])
- keyword_format = FORMAT_KEYWORD
- for keyword in keywords:
- pattern = r'\b' + re.escape(keyword) + r'\b' # Użyj re.escape dla słów kluczowych
- self._rules.append((re.compile(pattern), keyword_format))
- builtin_format = FORMAT_BUILTIN
- for builtin in builtins:
- pattern = r'\b' + re.escape(builtin) + r'\b'
- self._rules.append((re.compile(pattern), builtin_format))
- for pattern_str, format, *flags in patterns: # Opcjonalne flagi regex np. re.DOTALL
- try:
- pattern = re.compile(pattern_str, *flags)
- self._rules.append((pattern, format))
- except re.error as e:
- print(f"Błąd kompilacji regex '{pattern_str}' dla języka {self._language}: {e}", file=sys.stderr)
- def highlightBlock(self, text: str):
- """Główna metoda kolorująca blok tekstu (linię)."""
- self.setFormat(0, len(text), FORMAT_DEFAULT)
- self.setCurrentBlockState(0) # Domyślny stan dla tego bloku
- block_comment_delimiters = []
- if self._language in ['javascript', 'css', 'c++']:
- block_comment_delimiters.append(("/*", "*/", FORMAT_COMMENT))
- if self._language == 'html':
- pass # Rely on regex pattern for HTML comments
- comment_start_in_prev_block = (self.previousBlockState() == 1) # State 1 means inside /* ... */
- if comment_start_in_prev_block:
- end_delimiter_index = text.find("*/")
- if end_delimiter_index >= 0:
- self.setFormat(0, end_delimiter_index + 2, FORMAT_COMMENT)
- self.setCurrentBlockState(0) # Reset state
- start_pos = end_delimiter_index + 2
- else:
- self.setFormat(0, len(text), FORMAT_COMMENT)
- self.setCurrentBlockState(1) # Keep state as inside comment
- return # Entire line is a comment, no need to parse further
- else:
- start_pos = 0
- start_delimiter = "/*"
- end_delimiter = "*/"
- startIndex = text.find(start_delimiter, start_pos)
- while startIndex >= 0:
- endIndex = text.find(end_delimiter, startIndex)
- if endIndex >= 0:
- length = endIndex - startIndex + len(end_delimiter)
- self.setFormat(startIndex, startIndex + length, FORMAT_COMMENT)
- startIndex = text.find(start_delimiter, startIndex + length)
- else:
- self.setFormat(startIndex, len(text) - startIndex, FORMAT_COMMENT)
- self.setCurrentBlockState(1) # Set state to inside block comment
- break # No more pairs starting in this line
- for pattern, format in self._rules:
- if format == FORMAT_COMMENT and (pattern.pattern.startswith(re.escape('/*')) or pattern.pattern.startswith(re.escape('<!--'))):
- continue
- if format == FORMAT_COMMENT and pattern.pattern.startswith('//') and self.currentBlockState() == 1:
- continue
- for match in pattern.finditer(text):
- start, end = match.span()
- self.setFormat(start, end, format)
- class CustomFileSystemModel(QFileSystemModel):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.icon_map = {
- '.py': 'fa5s.file-code',
- '.js': 'fa5s.file-code',
- '.json': 'fa5s.file-code', # JSON też jako plik kodu
- '.html': 'fa5s.file-code',
- '.css': 'fa5s.file-code',
- '.ini': 'fa5s.file-alt', # Plik konfiguracji
- '.txt': 'fa5s.file-alt',
- '.md': 'fa5s.file-alt',
- '.c': 'fa5s.file-code',
- '.cpp': 'fa5s.file-code',
- '.h': 'fa5s.file-code',
- '.hpp': 'fa5s.file-code',
- }
- self.folder_icon_name = 'fa5s.folder'
- self.default_file_icon_name = 'fa5s.file'
- self._has_qtawesome = qta is not None
- def rename(self, index, new_name):
- """Zmienia nazwę pliku lub folderu reprezentowanego przez podany index."""
- if not index.isValid():
- return False
- old_path = self.filePath(index)
- new_path = os.path.join(os.path.dirname(old_path), new_name)
- try:
- os.rename(old_path, new_path)
- self.refresh() # Możliwe, że trzeba wymusić odświeżenie modelu
- return True
- except Exception as e:
- print(f"Błąd podczas zmiany nazwy: {e}")
- return False
- def data(self, index, role=Qt.ItemDataRole.DisplayRole):
- if not index.isValid():
- return None
- if role == Qt.ItemDataRole.DecorationRole:
- file_info = self.fileInfo(index)
- if file_info.isDir():
- if self._has_qtawesome:
- return qta.icon(self.folder_icon_name)
- else:
- return super().data(index, role)
- elif file_info.isFile():
- extension = file_info.suffix().lower()
- dotted_extension = '.' + extension
- if dotted_extension in self.icon_map and self._has_qtawesome:
- return qta.icon(self.icon_map[dotted_extension])
- else:
- if self._has_qtawesome:
- return qta.icon(self.default_file_icon_name)
- else:
- return super().data(index, role)
- return super().data(index, role)
- def refresh(self, *args):
- self.setRootPath(self.rootPath())
- class NewProjectDialog(QDialog):
- def __init__(self, projects_dir, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Nowy projekt")
- self.projects_dir = projects_dir
- self.setModal(True)
- layout = QFormLayout(self)
- self.name_edit = QLineEdit()
- layout.addRow("Nazwa projektu:", self.name_edit)
- self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- self.button_box.button(QDialogButtonBox.StandardButton.Ok).setText("Utwórz") # Zmieniamy tekst przycisku
- self.button_box.accepted.connect(self.accept)
- self.button_box.rejected.connect(self.reject)
- layout.addRow(self.button_box)
- self.name_edit.textChanged.connect(self._validate_name)
- self.name_edit.textChanged.emit(self.name_edit.text()) # Wywołaj walidację od razu
- def _validate_name(self, name):
- """Sprawdza poprawność nazwy projektu."""
- name = name.strip()
- is_empty = not name
- is_valid_chars = re.fullmatch(r'[a-zA-Z0-9_-]+', name) is not None or name == ""
- full_path = os.path.join(self.projects_dir, name)
- dir_exists = os.path.exists(full_path)
- enable_ok = not is_empty and is_valid_chars and not dir_exists
- self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(enable_ok)
- if is_empty:
- self.name_edit.setToolTip("Nazwa projektu nie może być pusta.")
- elif not is_valid_chars:
- self.name_edit.setToolTip("Nazwa projektu może zawierać tylko litery, cyfry, podkreślenia i myślniki.")
- elif dir_exists:
- self.name_edit.setToolTip(f"Projekt o nazwie '{name}' już istnieje w:\n{self.projects_dir}")
- else:
- self.name_edit.setToolTip(f"Katalog projektu zostanie utworzony w:\n{full_path}")
- if not enable_ok and not is_empty:
- self.name_edit.setStyleSheet("background-color: #ffe0e0;") # Jasnoczerwony
- else:
- self.name_edit.setStyleSheet("")
- def get_project_name(self):
- return self.name_edit.text().strip()
- def get_project_path(self):
- return os.path.join(self.projects_dir, self.get_project_name())
- class NewItemDialog(QDialog):
- def __init__(self, parent_dir, is_folder=False, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Nowy folder" if is_folder else "Nowy plik")
- self.parent_dir = parent_dir
- self.is_folder = is_folder
- self.setModal(True)
- layout = QFormLayout(self)
- self.item_type_label = "Nazwa folderu:" if is_folder else "Nazwa pliku:"
- self.name_edit = QLineEdit()
- layout.addRow(self.item_type_label, self.name_edit)
- self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- self.button_box.accepted.connect(self.accept)
- self.button_box.rejected.connect(self.reject)
- layout.addRow(self.button_box)
- self.name_edit.textChanged.connect(self._validate_name)
- self.name_edit.textChanged.emit(self.name_edit.text()) # Initial validation
- def _validate_name(self, name):
- """Sprawdza poprawność nazwy pliku/folderu."""
- name = name.strip()
- is_empty = not name
- illegal_chars_pattern = r'[<>:"/\\|?*\x00-\x1F]'
- is_valid_chars = re.search(illegal_chars_pattern, name) is None
- full_path = os.path.join(self.parent_dir, name)
- item_exists = os.path.exists(full_path)
- enable_create = not is_empty and is_valid_chars and not item_exists
- self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(enable_create)
- if is_empty:
- self.name_edit.setToolTip(f"{self.item_type_label} nie może być pusta.")
- elif not is_valid_chars:
- self.name_edit.setToolTip("Nazwa zawiera niedozwolone znaki.")
- elif item_exists:
- self.name_edit.setToolTip(f"Element o nazwie '{name}' już istnieje w:\n{self.parent_dir}")
- else:
- self.name_edit.setToolTip("")
- if not enable_create and not is_empty:
- self.name_edit.setStyleSheet("background-color: #ffe0e0;")
- else:
- self.name_edit.setStyleSheet("")
- def get_item_name(self):
- return self.name_edit.text().strip()
- class RenameItemDialog(QDialog):
- def __init__(self, current_path, parent=None):
- super().__init__(parent)
- self.current_path = current_path
- self.is_folder = os.path.isdir(current_path)
- old_name = os.path.basename(current_path)
- self.setWindowTitle("Zmień nazwę")
- layout = QVBoxLayout(self)
- self.label = QLabel(f"Nowa nazwa dla '{old_name}':", self)
- layout.addWidget(self.label)
- self.line_edit = QLineEdit(old_name, self)
- layout.addWidget(self.line_edit)
- self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self)
- layout.addWidget(self.button_box)
- self.button_box.accepted.connect(self.accept)
- self.button_box.rejected.connect(self.reject)
- self.line_edit.textChanged.connect(self._validate_name)
- self._validate_name(self.line_edit.text()) # Od razu sprawdź
- def _validate_name(self, name):
- name = name.strip()
- is_empty = not name
- illegal_chars_pattern = r'[<>:"/\\|?*\x00-\x1F]'
- is_valid_chars = re.search(illegal_chars_pattern, name) is None
- old_name = os.path.basename(self.current_path)
- is_same_name = name == old_name
- parent_dir = os.path.dirname(self.current_path)
- new_full_path = os.path.join(parent_dir, name)
- item_exists_at_new_path = os.path.exists(new_full_path)
- enable_ok = not is_empty and is_valid_chars and (is_same_name or not item_exists_at_new_path)
- self.button_box.button(QDialogButtonBox.Ok).setEnabled(enable_ok)
- def get_new_name(self):
- return self.line_edit.text().strip()
- class SettingsDialog(QDialog):
- def __init__(self, settings, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Ustawienia")
- self._settings = settings # Pracujemy na kopii
- self.setModal(True)
- layout = QFormLayout(self)
- self.theme_combo = QComboBox()
- self.theme_combo.addItems(["light", "dark"])
- self.theme_combo.setCurrentText(self._settings.get("theme", "light"))
- layout.addRow("Motyw:", self.theme_combo)
- self.python_path_edit = QLineEdit(self._settings.get("python_path", ""))
- self.python_path_button = QPushButton("Przeglądaj...")
- python_path_layout = QHBoxLayout()
- python_path_layout.addWidget(self.python_path_edit)
- python_path_layout.addWidget(self.python_path_button)
- layout.addRow("Ścieżka Python:", python_path_layout)
- self.python_path_button.clicked.connect(lambda: self._browse_file(self.python_path_edit))
- self.node_path_edit = QLineEdit(self._settings.get("node_path", ""))
- self.node_path_button = QPushButton("Przeglądaj...")
- node_path_layout = QHBoxLayout()
- node_path_layout.addWidget(self.node_path_edit)
- node_path_layout.addWidget(self.node_path_button)
- layout.addRow("Ścieżka Node.js:", node_path_layout)
- self.node_path_button.clicked.connect(lambda: self._browse_file(self.node_path_edit))
- self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- self.button_box.accepted.connect(self.accept)
- self.button_box.rejected.connect(self.reject)
- layout.addRow(self.button_box)
- def _browse_file(self, line_edit):
- """Otwiera dialog wyboru pliku dla pola QLineEdit."""
- start_dir = os.path.dirname(line_edit.text()) if os.path.dirname(line_edit.text()) else os.path.expanduser("~")
- if platform.system() == "Windows":
- filter_str = "Wykonywalne pliki (*.exe *.bat *.cmd);;Wszystkie pliki (*)"
- else:
- filter_str = "Wszystkie pliki (*)" # Na Linux/macOS pliki wykonywalne nie mają konkretnego rozszerzenia
- file_path, _ = QFileDialog.getOpenFileName(self, "Wybierz plik", start_dir, filter_str)
- if file_path:
- line_edit.setText(os.path.normpath(file_path)) # Znormalizuj ścieżkę przed ustawieniem
- def get_settings(self):
- self._settings["theme"] = self.theme_combo.currentText()
- self._settings["python_path"] = self.python_path_edit.text().strip()
- self._settings["node_path"] = self.node_path_edit.text().strip()
- return self._settings
- def layout(self):
- return super().layout()
- class IDEWindow(QMainWindow):
- def __init__(self):
- super().__init__()
- self.settings = {} # Słownik na ustawienia aplikacji
- self.recents = {"last_project_dir": None, "open_files": []} # Słownik na historię
- self._load_app_state() # Wczytaj stan aplikacji (ustawienia i historię)
- self.setWindowTitle("Proste IDE - Bez nazwy")
- self.setGeometry(100, 100, 1200, 800)
- if qta:
- self.setWindowIcon(qta.icon('fa5s.code'))
- else:
- self.setWindowIcon(QIcon.fromTheme("applications-development")) # Przykładowa ikona systemowa
- self.current_project_dir = self.recents.get("last_project_dir")
- self.open_files = {} # {ścieżka_pliku: edytor_widget}
- self.base_editor_font = QFont("Courier New", 10) # Ustaw domyślną czcionkę początkową
- self._setup_ui()
- self._setup_menu()
- self._setup_toolbar()
- self._setup_status_bar()
- self._setup_connections()
- self._apply_theme(self.settings.get("theme", "light"))
- self._apply_editor_font_size() # Zastosuj rozmiar czcionki do wszystkich otwartych edytorów (choć na start puste)
- self.process = QProcess(self) # Proces do uruchamiania kodu
- self.process.readyReadStandardOutput.connect(self._handle_stdout)
- self.process.readyReadStandardError.connect(self._handle_stderr)
- self.process.finished.connect(self._handle_process_finished)
- self.node_scripts = {} # Słownik na skrypty z package.json
- QTimer.singleShot(10, self._initial_setup)
- def _setup_ui(self):
- """Konfiguracja głównych elementów interfejsu."""
- central_widget = QWidget()
- main_layout = QVBoxLayout(central_widget)
- main_layout.setContentsMargins(0, 0, 0, 0)
- self.main_splitter = QSplitter(Qt.Orientation.Horizontal)
- main_layout.addWidget(self.main_splitter)
- self.project_model = CustomFileSystemModel() # Użyj niestandardowego modelu z ikonami
- self.project_model.setFilter(QDir.Filter.AllDirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot)
- self.project_tree = QTreeView()
- self.project_tree.setModel(self.project_model)
- self.project_tree.setHeaderHidden(True)
- self.project_tree.hideColumn(1)
- self.project_tree.hideColumn(2)
- self.project_tree.hideColumn(3)
- self.project_tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
- self.main_splitter.addWidget(self.project_tree)
- self.right_splitter = QSplitter(Qt.Orientation.Vertical)
- self.main_splitter.addWidget(self.right_splitter)
- self.tab_widget = QTabWidget()
- self.tab_widget.setTabsClosable(True)
- self.tab_widget.setMovable(True)
- self.right_splitter.addWidget(self.tab_widget)
- self.console_widget = QWidget()
- self.console_layout = QVBoxLayout(self.console_widget)
- self.console_layout.setContentsMargins(0, 0, 0, 0)
- self.console = QPlainTextEdit()
- self.console.setReadOnly(True)
- self.console.setFont(self.base_editor_font)
- self.console_layout.addWidget(self.console, 1) # Rozciągnij pole konsoli
- self.console_input = QLineEdit()
- self.console_input.setPlaceholderText("Wpisz polecenie...")
- self.console_layout.addWidget(self.console_input, 0) # Nie rozciągaj pola wprowadzania
- self.console_buttons_layout = QHBoxLayout()
- self.console_buttons_layout.setContentsMargins(0, 0, 0, 0)
- self.console_buttons_layout.addStretch(1)
- self.clear_console_button = QPushButton("Wyczyść konsolę")
- self.console_buttons_layout.addWidget(self.clear_console_button)
- self.copy_console_button = QPushButton("Skopiuj")
- self.console_buttons_layout.addWidget(self.copy_console_button)
- self.console_layout.addLayout(self.console_buttons_layout)
- self.right_splitter.addWidget(self.console_widget)
- self.main_splitter.setSizes([200, 800])
- self.right_splitter.setSizes([600, 200])
- self.setCentralWidget(central_widget)
- self.action_toggle_tree = QAction("Pokaż/Ukryj drzewko", self)
- self.action_toggle_tree.setCheckable(True)
- self.action_toggle_tree.setChecked(True)
- self.action_toggle_tree.triggered.connect(self._toggle_tree_panel)
- self.action_toggle_console = QAction("Pokaż/Ukryj konsolę", self)
- self.action_toggle_console.setCheckable(True)
- self.action_toggle_console.setChecked(True)
- self.action_toggle_console.triggered.connect(self._toggle_console_panel)
- self._apply_view_settings()
- def _toggle_tree_panel(self, checked):
- self.main_splitter.widget(0).setVisible(checked)
- def _toggle_console_panel(self, checked):
- self.right_splitter.widget(1).setVisible(checked)
- def _setup_menu(self):
- """Konfiguracja paska menu."""
- menu_bar = self.menuBar()
- file_menu = menu_bar.addMenu("&Plik") # & dodaje skrót klawiszowy Alt+P
- self.action_new_project = QAction(qta.icon('fa5s.folder-plus') if qta else QIcon(), "&Nowy projekt...", self)
- self.action_new_project.triggered.connect(self._new_project)
- file_menu.addAction(self.action_new_project)
- self.action_open_folder = QAction(qta.icon('fa5s.folder-open') if qta else QIcon(), "Otwórz &folder projektu...", self)
- self.action_open_folder.triggered.connect(self._open_project_folder) # Użyj bezpośredniego połączenia
- file_menu.addAction(self.action_open_folder)
- self.action_open_file = QAction(qta.icon('fa5s.file-code') if qta else QIcon(), "Otwórz &plik...", self)
- self.action_open_file.triggered.connect(self._open_file_dialog)
- file_menu.addAction(self.action_open_file)
- file_menu.addSeparator()
- self.recent_files_menu = QMenu("Ostatnio otwierane", self)
- file_menu.addMenu(self.recent_files_menu)
- file_menu.addSeparator()
- self.action_save = QAction(qta.icon('fa5s.save') if qta else QIcon(), "&Zapisz", self)
- self.action_save.setShortcut(QKeySequence.StandardKey.Save)
- self.action_save.triggered.connect(self._save_current_file)
- file_menu.addAction(self.action_save)
- self.action_save_as = QAction(qta.icon('fa5s.file-export') if qta else QIcon(), "Zapisz &jako...", self)
- self.action_save_as.setShortcut(QKeySequence.StandardKey.SaveAs)
- self.action_save_as.triggered.connect(self._save_current_file_as)
- file_menu.addAction(self.action_save_as)
- self.action_save_all = QAction(qta.icon('fa5s.save') if qta else QIcon(), "Zapisz wszys&tko", self)
- self.action_save_all.setShortcut(QKeySequence("Ctrl+Shift+S")) # Standardowy skrót
- self.action_save_all.triggered.connect(self._save_all_files)
- file_menu.addAction(self.action_save_all)
- file_menu.addSeparator()
- self.action_close_file = QAction(qta.icon('fa5s.window-close') if qta else QIcon(), "Zamknij ak&tualny plik", self)
- self.action_close_file.triggered.connect(self._close_current_tab)
- file_menu.addAction(self.action_close_file)
- file_menu.addSeparator()
- self.action_exit = QAction(qta.icon('fa5s.door-open') if qta else QIcon(), "&Zakończ", self)
- self.action_exit.setShortcut(QKeySequence.StandardKey.Quit)
- self.action_exit.triggered.connect(self.close)
- file_menu.addAction(self.action_exit)
- edit_menu = menu_bar.addMenu("&Edycja")
- view_menu = menu_bar.addMenu("&Widok")
- self.action_toggle_tree = QAction(qta.icon('fa5s.sitemap') if qta else QIcon(), "Pokaż &drzewko plików", self)
- self.action_toggle_tree.setCheckable(True)
- self.action_toggle_tree.setChecked(self.settings.get("show_tree", True)) # Ustaw stan z ustawień
- self.action_toggle_tree.triggered.connect(self._toggle_tree_view)
- view_menu.addAction(self.action_toggle_tree)
- self.action_toggle_console = QAction(qta.icon('fa5s.terminal') if qta else QIcon(), "Pokaż &konsolę", self)
- self.action_toggle_console.setCheckable(True)
- self.action_toggle_console.setChecked(self.settings.get("show_console", True)) # Ustaw stan z ustawień
- self.action_toggle_console.triggered.connect(self._toggle_console)
- view_menu.addAction(self.action_toggle_console)
- search_menu = menu_bar.addMenu("&Wyszukaj")
- self.action_find = QAction(qta.icon('fa5s.search') if qta else QIcon(), "&Znajdź...", self)
- self.action_find.setShortcut(QKeySequence.StandardKey.Find)
- self.action_find.triggered.connect(self._show_find_bar)
- search_menu.addAction(self.action_find)
- run_menu = menu_bar.addMenu("&Uruchom")
- self.action_run_file = QAction(qta.icon('fa5s.play') if qta else QIcon(), "&Uruchom aktualny plik", self)
- self.action_run_file.setShortcut(QKeySequence("F5")) # Przykładowy skrót
- self.action_run_file.triggered.connect(self._run_current_file)
- run_menu.addAction(self.action_run_file)
- tools_menu = menu_bar.addMenu("&Narzędzia")
- self.action_settings = QAction(qta.icon('fa5s.cog') if qta else QIcon(), "&Ustawienia...", self)
- self.action_settings.triggered.connect(self._show_settings_dialog)
- tools_menu.addAction(self.action_settings)
- help_menu = menu_bar.addMenu("&Pomoc")
- self.action_about = QAction(qta.icon('fa5s.info-circle') if qta else QIcon(), "&O programie...", self)
- self.action_about.triggered.connect(self._show_about_dialog)
- help_menu.addAction(self.action_about)
- def _setup_toolbar(self):
- """Konfiguracja paska narzędzi."""
- toolbar = self.addToolBar("Główne narzędzia")
- toolbar.setMovable(False) # Nie można go przesuwać
- toolbar.setIconSize(QSize(16, 16)) # Rozmiar ikon
- toolbar.addAction(self.action_new_project) # Nowy projekt
- toolbar.addAction(self.action_open_folder) # Otwórz folder
- toolbar.addAction(self.action_open_file) # Otwórz plik
- toolbar.addSeparator()
- toolbar.addAction(self.action_save) # Zapisz
- toolbar.addAction(self.action_save_all) # Zapisz wszystko
- toolbar.addSeparator()
- self.run_toolbutton = QToolButton(self)
- self.run_toolbutton.setDefaultAction(self.action_run_file)
- self.run_toolbutton.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
- toolbar.addWidget(self.run_toolbutton) # Dodaj QToolButton do toolbara
- toolbar.addSeparator()
- self.search_input = QLineEdit(self)
- self.search_input.setPlaceholderText("Szukaj w pliku...")
- self.search_input.setClearButtonEnabled(True) # Przycisk czyszczenia
- self.search_input.returnPressed.connect(lambda: self._find_text(self.search_input.text(), 'next')) # Szukaj po wciśnięciu Enter
- self.find_next_button = QPushButton("Znajdź dalej")
- self.find_next_button.clicked.connect(lambda: self._find_text(self.search_input.text(), 'next'))
- self.find_prev_button = QPushButton("Znajdź poprzedni")
- self.find_prev_button.clicked.connect(lambda: self._find_text(self.search_input.text(), 'previous'))
- toolbar.addWidget(self.search_input)
- toolbar.addWidget(self.find_next_button)
- toolbar.addWidget(self.find_prev_button)
- self.search_input.setVisible(False)
- self.find_next_button.setVisible(False)
- self.find_prev_button.setVisible(False)
- def _setup_status_bar(self):
- """Konfiguracja paska stanu."""
- self.statusBar().showMessage("Gotowy.")
- def _setup_connections(self):
- """Połączenie sygnałów ze slotami."""
- self.project_tree.doubleClicked.connect(self._handle_tree_item_double_click)
- self.tab_widget.tabCloseRequested.connect(self._close_tab_by_index)
- self.tab_widget.currentChanged.connect(self._handle_tab_change)
- self.project_tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) # Upewnij się, że policy jest ustawione
- self.project_tree.customContextMenuRequested.connect(self._show_project_tree_context_menu)
- self.clear_console_button.clicked.connect(self.console.clear)
- self.copy_console_button.clicked.connect(self._copy_console)
- self.console_input.returnPressed.connect(self._run_console_command)
- def _initial_setup(self):
- """Wstępna konfiguracja po uruchomieniu, w tym ładowanie ostatniego stanu."""
- initial_dir = self.recents.get("last_project_dir")
- if not initial_dir or not os.path.isdir(initial_dir):
- initial_dir = PROJECTS_DIR # Użyj domyślnego katalogu projects
- os.makedirs(PROJECTS_DIR, exist_ok=True)
- if os.path.isdir(initial_dir):
- self._open_project_folder(initial_dir)
- else:
- self.statusBar().showMessage(f"Brak domyślnego katalogu projektu. Otwórz folder ręcznie lub utwórz nowy.")
- self.project_model.setRootPath("") # Brak roota w drzewku
- self.current_project_dir = None # Resetuj current_project_dir
- self._update_run_button_menu()
- recent_files = self.recents.get("open_files", [])
- QTimer.singleShot(200, lambda: self._reopen_files(recent_files)) # Krótsze opóźnienie
- self._update_recent_files_menu() # Uaktualnij menu ostatnio otwieranych
- def _load_app_state(self):
- """Wczytuje ustawienia i historię z plików JSON."""
- try:
- if os.path.exists(SETTINGS_FILE):
- with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
- loaded_settings = json.load(f)
- self.settings = {
- "theme": loaded_settings.get("theme", "light"),
- "python_path": loaded_settings.get("python_path", ""),
- "node_path": loaded_settings.get("node_path", ""),
- "show_tree": loaded_settings.get("show_tree", True),
- "show_console": loaded_settings.get("show_console", True),
- "editor_font_size": loaded_settings.get("editor_font_size", 10)
- }
- else:
- self.settings = {
- "theme": "light",
- "python_path": "",
- "node_path": "",
- "show_tree": True,
- "show_console": True,
- "editor_font_size": 10
- }
- if os.path.exists(RECENTS_FILE):
- with open(RECENTS_FILE, 'r', encoding='utf-8') as f:
- loaded_recents = json.load(f)
- self.recents = {
- "last_project_dir": loaded_recents.get("last_project_dir"),
- "open_files": loaded_recents.get("open_files", [])
- }
- else:
- self.recents = {"last_project_dir": None, "open_files": []}
- except (json.JSONDecodeError, Exception) as e:
- print(f"Błąd podczas wczytywania stanu aplikacji: {e}", file=sys.stderr)
- self.settings = {
- "theme": "light",
- "python_path": "",
- "node_path": "",
- "show_tree": True,
- "show_console": True,
- "editor_font_size": 10
- }
- self.recents = {"last_project_dir": None, "open_files": []}
- def _save_app_state(self):
- """Zapisuje ustawienia i historię do plików JSON."""
- try:
- self.recents["open_files"] = list(self.open_files.keys())
- if self.current_project_dir and os.path.isdir(self.current_project_dir):
- self.recents["last_project_dir"] = os.path.normpath(self.current_project_dir) # Znormalizuj przed zapisem
- else:
- self.recents["last_project_dir"] = None # Nie zapisuj jeśli nie ma folderu
- with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
- json.dump(self.settings, f, indent=4)
- with open(RECENTS_FILE, 'w', encoding='utf-8') as f:
- normalized_open_files = [os.path.normpath(p) for p in self.recents["open_files"]]
- unique_open_files = []
- for p in normalized_open_files:
- if p not in unique_open_files:
- unique_open_files.append(p)
- self.recents["open_files"] = unique_open_files[:20] # Ostatnie 20 unikalnych
- json.dump(self.recents, f, indent=4)
- except Exception as e:
- print(f"Błąd podczas zapisu stanu aplikacji: {e}", file=sys.stderr)
- def closeEvent(self, event):
- """Obsługa zdarzenia zamykania okna."""
- unsaved_files = [path for path, editor in self.open_files.items() if editor.document().isModified()]
- if unsaved_files:
- msg_box = QMessageBox(self)
- msg_box.setIcon(QMessageBox.Icon.Warning)
- msg_box.setWindowTitle("Niezapisane zmiany")
- msg_box.setText(f"Masz niezapisane zmiany w {len(unsaved_files)} plikach.\nCzy chcesz zapisać przed zamknięciem?")
- msg_box.setStandardButtons(QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
- msg_box.setDefaultButton(QMessageBox.StandardButton.Save)
- reply = msg_box.exec()
- if reply == QMessageBox.StandardButton.Save:
- if self._save_all_files():
- self._save_app_state() # Zapisz stan po pomyślnym zapisie plików
- event.accept() # Akceptuj zamknięcie
- else:
- event.ignore() # Nie zamykaj, jeśli zapis się nie udał
- elif reply == QMessageBox.StandardButton.Discard:
- for i in range(self.tab_widget.count() - 1, -1, -1):
- widget = self.tab_widget.widget(i)
- if hasattr(widget, 'document'):
- widget.document().setModified(False)
- self._close_tab_by_index(i) # Ta metoda usunie z open_files i recents
- self._save_app_state() # Zapisz stan (lista otwartych plików będzie aktualna)
- event.accept() # Akceptuj zamknięcie
- else:
- event.ignore() # Ignoruj zamknięcie
- else:
- self._save_app_state() # Zapisz stan, bo nie ma niezapisanych plików
- event.accept() # Akceptuj zamknięcie
- def _new_project(self):
- """Tworzy nowy katalog projektu."""
- dialog = NewProjectDialog(PROJECTS_DIR, self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- project_name = dialog.get_project_name()
- project_path = dialog.get_project_path()
- try:
- if os.path.exists(project_path):
- QMessageBox.warning(self, "Projekt już istnieje", f"Projekt o nazwie '{project_name}' już istnieje.")
- return
- os.makedirs(project_path)
- self.statusBar().showMessage(f"Utworzono nowy projekt: {project_name}")
- self._open_project_folder(project_path)
- except OSError as e:
- QMessageBox.critical(self, "Błąd tworzenia projektu", f"Nie można utworzyć katalogu projektu:\n{e}")
- self.statusBar().showMessage("Błąd tworzenia projektu.")
- except Exception as e:
- QMessageBox.critical(self, "Nieoczekiwany błąd", f"Wystąpił nieoczekiwany błąd:\n{e}")
- self.statusBar().showMessage("Nieoczekiwany błąd.")
- def _open_project_folder(self, path=None):
- """Otwiera okno dialogowe wyboru folderu projektu lub otwiera wskazany folder."""
- if path is None:
- start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
- dialog_path = QFileDialog.getExistingDirectory(self, "Otwórz folder projektu", start_path)
- if not dialog_path:
- return
- path = dialog_path
- path = os.path.normpath(path)
- if not os.path.isdir(path):
- QMessageBox.critical(self, "Błąd", f"Wybrana ścieżka nie jest katalogiem lub nie istnieje:\n{path}")
- self.statusBar().showMessage(f"Błąd: Nie można otworzyć folderu: {path}")
- return
- if self.current_project_dir and self.current_project_dir != path:
- unsaved_files_count = sum(1 for editor in self.open_files.values() if editor.document().isModified())
- if unsaved_files_count > 0:
- reply = QMessageBox.question(self, "Niezapisane zmiany",
- f"Obecny projekt ma {unsaved_files_count} niezapisanych plików.\n"
- "Czy chcesz zapisać zmiany przed otwarciem nowego folderu?",
- QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
- if reply == QMessageBox.StandardButton.Cancel:
- self.statusBar().showMessage("Otwieranie folderu anulowane.")
- return # Anuluj operację
- if reply == QMessageBox.StandardButton.Save:
- if not self._save_all_files(): # Ta metoda obsłuży Save As dla nowych plików
- self.statusBar().showMessage("Otwieranie folderu anulowane (błąd zapisu).")
- return # Anuluj operację
- self._close_all_files() # Ta metoda już nie pyta o zapis
- self.current_project_dir = path
- self.project_model.setRootPath(path)
- root_index = self.project_model.index(path)
- if not root_index.isValid():
- QMessageBox.critical(self, "Błąd", f"Nie można ustawić katalogu głównego drzewka dla ścieżki:\n{path}\n"
- "Sprawdź uprawnienia lub czy ścieżka jest dostępna dla systemu plików.")
- self.statusBar().showMessage(f"Błąd ustawienia katalogu głównego: {path}")
- self.project_tree.setRootIndex(self.project_model.index("")) # Wyczyść roota
- self.current_project_dir = None # Resetuj current_project_dir
- 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)]
- self._update_recent_files_menu()
- self._save_app_state() # Zapisz zaktualizowany stan
- self._update_run_button_menu() # Uaktualnij menu uruchamiania (brak projektu)
- return
- self.project_tree.setRootIndex(root_index)
- self.setWindowTitle(f"Proste IDE - {os.path.basename(path)}")
- self.statusBar().showMessage(f"Otwarto folder: {path}")
- self._check_package_json(path)
- self.recents["last_project_dir"] = path
- self._save_app_state() # Zapisz, żeby zapamiętać ostatni folder
- def _close_all_files(self):
- """Zamyka wszystkie otwarte zakładki edytora bez pytania o zapis."""
- for file_path in list(self.open_files.keys()):
- editor_widget = self.open_files.get(file_path)
- if editor_widget:
- tab_index = self.tab_widget.indexOf(editor_widget)
- if tab_index != -1:
- if hasattr(editor_widget, 'document'):
- editor_widget.document().setModified(False)
- self.tab_widget.removeTab(tab_index)
- if file_path in self.open_files:
- del self.open_files[file_path]
- self.recents["open_files"] = [] # Wyczyść listę otwartych plików
- self._update_recent_files_menu() # Uaktualnij menu
- def _open_file_dialog(self):
- """Otwiera okno dialogowe wyboru pliku i otwiera go w edytorze."""
- start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
- 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)")
- if file_path:
- self._open_file(file_path)
- def _open_file(self, file_path):
- """Otwiera wskazany plik w nowej zakładce edytora."""
- file_path = os.path.normpath(file_path)
- if not os.path.exists(file_path) or not os.path.isfile(file_path):
- self.statusBar().showMessage(f"Błąd: Plik nie istnieje lub nie jest plikiem: {file_path}")
- if file_path in self.recents["open_files"]:
- self.recents["open_files"].remove(file_path)
- self._update_recent_files_menu() # Uaktualnij menu
- self._save_app_state() # Zapisz stan
- return
- if file_path in self.open_files:
- index = -1
- for i in range(self.tab_widget.count()):
- widget = self.tab_widget.widget(i)
- if self.open_files.get(file_path) is widget:
- index = i
- break
- if index != -1:
- self.tab_widget.setCurrentIndex(index)
- self.statusBar().showMessage(f"Plik {os.path.basename(file_path)} jest już otwarty.")
- if file_path in self.recents["open_files"]:
- self.recents["open_files"].remove(file_path)
- self.recents["open_files"].insert(0, file_path)
- self._update_recent_files_menu()
- self._save_app_state()
- return
- else:
- print(f"Warning: File {file_path} found in open_files but not in tab widget.", file=sys.stderr)
- try:
- content = ""
- try:
- with open(file_path, 'r', encoding='utf-8') as f:
- content = f.read()
- except UnicodeDecodeError:
- try:
- with open(file_path, 'r', encoding='latin-1') as f:
- content = f.read()
- except Exception:
- with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
- content = f.read()
- except Exception as e:
- QMessageBox.critical(self, "Błąd otwarcia pliku", f"Nie można odczytać pliku {os.path.basename(file_path)}:\n{e}")
- self.statusBar().showMessage(f"Błąd otwarcia pliku: {os.path.basename(file_path)}")
- return
- editor = QPlainTextEdit()
- editor.setPlainText(content)
- editor.setFont(self.base_editor_font)
- editor.document().setModified(False) # Nowo otwarty plik nie jest zmodyfikowany
- editor.document().modificationChanged.connect(self._handle_modification_changed)
- language = self._get_language_from_path(file_path)
- highlighter = CodeSyntaxHighlighter(editor.document(), language) # Przypisz highlighter do dokumentu edytora
- setattr(editor.document(), '_syntax_highlighter', highlighter)
- tab_index = self.tab_widget.addTab(editor, os.path.basename(file_path))
- self.tab_widget.setCurrentIndex(tab_index)
- self.open_files[file_path] = editor # Zapisz odwołanie do edytora
- self.statusBar().showMessage(f"Otwarto plik: {file_path}")
- if file_path in self.recents["open_files"]:
- self.recents["open_files"].remove(file_path) # Usuń stary wpis, żeby był na górze
- self.recents["open_files"].insert(0, file_path) # Dodaj na początek
- self._update_recent_files_menu() # Uaktualnij menu
- self._save_app_state() # Zapisz stan
- def _reopen_files(self, file_list):
- """Ponownie otwiera listę plików po uruchomieniu programu."""
- files_to_reopen = list(file_list) # Tworzymy kopię
- valid_files = [f for f in files_to_reopen if os.path.exists(f) and os.path.isfile(f)]
- self.recents["open_files"] = valid_files
- self._update_recent_files_menu() # Uaktualnij menu
- for file_path in valid_files:
- QTimer.singleShot(0, lambda path=file_path: self._open_file(path))
- invalid_files = [f for f in files_to_reopen if f not in valid_files]
- if invalid_files:
- msg = "Nie można ponownie otworzyć następujących plików (nie znaleziono):\n" + "\n".join(invalid_files)
- QMessageBox.warning(self, "Błąd otwarcia plików", msg)
- def _update_recent_files_menu(self):
- """Uaktualnia listę plików w menu 'Ostatnio otwierane'."""
- self.recent_files_menu.clear()
- recent_items_to_show = list(self.recents.get("open_files", []))[:15] # Pokaż max 15 (praca na kopii)
- if not recent_items_to_show:
- self.recent_files_menu.addAction("Brak ostatnio otwieranych plików").setEnabled(False)
- return
- actions_to_add = []
- cleaned_recent_files = [] # Zbuduj nową listę poprawnych ścieżek
- for file_path in recent_items_to_show:
- if os.path.exists(file_path) and os.path.isfile(file_path):
- cleaned_recent_files.append(file_path) # Dodaj do listy czystej
- menu_text = os.path.basename(file_path) # Pokaż tylko nazwę pliku
- action = QAction(menu_text, self)
- action.setData(file_path) # Zapisz pełną ścieżkę w danych akcji
- action.triggered.connect(lambda checked, path=file_path: self._open_file(path))
- actions_to_add.append(action)
- all_existing_recent_files = [p for p in self.recents.get("open_files", []) if os.path.exists(p) and os.path.isfile(p)]
- unique_recent_files = []
- for p in all_existing_recent_files:
- if p not in unique_recent_files:
- unique_recent_files.append(p)
- self.recents["open_files"] = unique_recent_files[:20]
- self.recent_files_menu.clear() # Wyczyść ponownie
- if not self.recents["open_files"]:
- self.recent_files_menu.addAction("Brak ostatnio otwieranych plików").setEnabled(False)
- else:
- for file_path in self.recents["open_files"]:
- menu_text = os.path.basename(file_path)
- action = QAction(menu_text, self)
- action.setData(file_path)
- action.triggered.connect(lambda checked, path=file_path: self._open_file(path))
- self.recent_files_menu.addAction(action)
- def _get_language_from_path(self, file_path):
- """Zwraca nazwę języka na podstawie rozszerzenia pliku."""
- if not file_path:
- return 'plaintext'
- file_info = QFileInfo(file_path)
- extension = file_info.suffix().lower()
- if extension == 'py':
- return 'python'
- elif extension == 'js':
- return 'javascript'
- elif extension == 'html':
- return 'html'
- elif extension == 'css':
- return 'css'
- elif extension in ['c', 'cpp', 'h', 'hpp']:
- return 'c++'
- elif extension == 'ini':
- return 'ini'
- elif extension == 'json':
- return 'json'
- else:
- return 'plaintext' # Bez kolorowania
- def _handle_tree_item_double_click(self, index):
- """Obsługa podwójnego kliknięcia w drzewku projektu."""
- file_path = self.project_model.filePath(index)
- if os.path.isfile(file_path):
- self._open_file(file_path)
- elif os.path.isdir(file_path):
- if self.project_tree.isExpanded(index):
- self.project_tree.collapse(index)
- else:
- self.project_tree.expand(index)
- def _show_project_tree_context_menu(self, point):
- """Wyświetla menu kontekstowe dla drzewka projektu."""
- index = self.project_tree.indexAt(point)
- menu = QMenu(self)
- create_file_action = QAction(qta.icon('fa5s.file-medical') if qta else QIcon(), "Nowy plik...", self)
- create_folder_action = QAction(qta.icon('fa5s.folder-plus') if qta else QIcon(), "Nowy folder...", self)
- select_all_action = QAction("Zaznacz wszystko", self)
- select_all_action.triggered.connect(self.project_tree.selectAll)
- if index.isValid():
- file_path = self.project_model.filePath(index)
- file_info = self.project_model.fileInfo(index)
- is_root_model = self.project_model.rootPath() == file_path
- if not is_root_model: # Nie usuwaj/zmieniaj nazwy roota projektu
- rename_action = QAction(qta.icon('fa5s.edit') if qta else QIcon(), "Zmień nazwę...", self)
- delete_action = QAction(qta.icon('fa5s.trash') if qta else QIcon(), "Usuń", self)
- rename_action.triggered.connect(lambda: self._rename_item(index))
- delete_action.triggered.connect(lambda: self._delete_item(index))
- if file_info.isDir():
- menu.addAction(create_file_action) # Nowy plik w folderze
- menu.addAction(create_folder_action) # Nowy folder w folderze
- menu.addSeparator()
- menu.addAction(rename_action)
- menu.addAction(delete_action)
- menu.addSeparator()
- open_in_os_action = QAction(qta.icon('fa5s.external-link-alt') if qta else QIcon(), "Otwórz w eksploratorze", self) # Windows
- if platform.system() == "Darwin": open_in_os_action.setText("Otwórz w Finderze") # macOS
- elif platform.system() == "Linux": open_in_os_action.setText("Otwórz w menedżerze plików") # Linux
- open_in_os_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile(file_path)))
- menu.addAction(open_in_os_action)
- create_file_action.triggered.connect(lambda: self._create_new_item(file_path, is_folder=False))
- create_folder_action.triggered.connect(lambda: self._create_new_item(file_path, is_folder=True))
- elif file_info.isFile():
- open_action = QAction(qta.icon('fa5s.file') if qta else QIcon(), "Otwórz", self) # Mimo podwójnego kliknięcia
- open_action.triggered.connect(lambda: self._open_file(file_path))
- duplicate_action = QAction(qta.icon('fa5s.copy') if qta else QIcon(), "Duplikuj", self)
- duplicate_action.triggered.connect(lambda: self._duplicate_file(index))
- copy_path_action = QAction(qta.icon('fa5s.link') if qta else QIcon(), "Kopiuj ścieżkę", self)
- copy_path_action.triggered.connect(lambda: QApplication.clipboard().setText(file_path))
- menu.addAction(open_action)
- menu.addSeparator()
- menu.addAction(rename_action)
- menu.addAction(delete_action)
- menu.addAction(duplicate_action)
- menu.addSeparator()
- menu.addAction(copy_path_action)
- menu.addSeparator()
- open_containing_folder_action = QAction(qta.icon('fa5s.folder-open') if qta else QIcon(), "Pokaż w eksploratorze", self) # Windows
- if platform.system() == "Darwin": open_containing_folder_action.setText("Pokaż w Finderze") # macOS
- elif platform.system() == "Linux": open_containing_folder_action.setText("Pokaż w menedżerze plików") # Linux
- open_containing_folder_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.dirname(file_path))))
- menu.addAction(open_containing_folder_action)
- else: # Kliknięto na root folderu projektu
- menu.addAction(create_file_action) # Nowy plik w roocie
- menu.addAction(create_folder_action) # Nowy folder w roocie
- menu.addSeparator()
- open_in_os_action = QAction(qta.icon('fa5s.external-link-alt') if qta else QIcon(), "Otwórz w eksploratorze", self) # Windows
- if platform.system() == "Darwin": open_in_os_action.setText("Otwórz w Finderze") # macOS
- elif platform.system() == "Linux": open_in_os_action.setText("Otwórz w menedżerze plików") # Linux
- open_in_os_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile(file_path)))
- menu.addAction(open_in_os_action)
- create_file_action.triggered.connect(lambda: self._create_new_item(file_path, is_folder=False))
- create_folder_action.triggered.connect(lambda: self._create_new_item(file_path, is_folder=True))
- else: # Kliknięto na puste miejsce w drzewku
- if self.current_project_dir and os.path.isdir(self.current_project_dir):
- menu.addAction(create_file_action) # Nowy plik w roocie projektu
- menu.addAction(create_folder_action) # Nowy folder w roocie projektu
- create_file_action.triggered.connect(lambda: self._create_new_item(self.current_project_dir, is_folder=False))
- create_folder_action.triggered.connect(lambda: self._create_new_item(self.current_project_dir, is_folder=True))
- menu.addSeparator()
- menu.addAction(select_all_action)
- if menu.actions(): # Pokaż menu tylko jeśli są jakieś akcje
- menu.exec(self.project_tree.mapToGlobal(point))
- def _create_new_item(self, parent_dir, is_folder):
- """Tworzy nowy plik lub folder w podanym katalogu nadrzędnym."""
- if not os.path.isdir(parent_dir):
- QMessageBox.warning(self, "Błąd tworzenia", f"Katalog nadrzędny nie istnieje lub nie jest katalogiem:\n{parent_dir}")
- self.statusBar().showMessage("Błąd tworzenia nowego elementu.")
- return
- dialog = NewItemDialog(parent_dir, is_folder, self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- item_name = dialog.get_item_name()
- if not item_name: return # Powinno być obsłużone w dialogu, ale zabezpieczenie
- full_path = os.path.join(parent_dir, item_name)
- parent_index = self.project_model.index(parent_dir)
- if not parent_index.isValid():
- try:
- if is_folder:
- os.makedirs(full_path)
- self.statusBar().showMessage(f"Utworzono folder: {item_name}")
- else:
- with open(full_path, 'w') as f: pass # Utwórz pusty plik
- self.statusBar().showMessage(f"Utworzono plik: {item_name}")
- root_path = self.project_model.rootPath()
- if root_path and os.path.isdir(root_path):
- self.project_model.refresh(self.project_model.index(root_path))
- except OSError as e:
- QMessageBox.critical(self, "Błąd tworzenia", f"Nie można utworzyć {'folderu' if is_folder else 'pliku'}:\n{e}")
- self.statusBar().showMessage(f"Błąd tworzenia {'folderu' if is_folder else 'pliku'}.")
- except Exception as e:
- QMessageBox.critical(self, "Nieoczekiwany błąd", f"Wystąpił nieoczekiwany błąd:\n{e}")
- self.statusBar().showMessage("Nieoczekiwany błąd.")
- return
- success = False
- if is_folder:
- new_index = self.project_model.mkdir(parent_index, item_name)
- if new_index.isValid():
- self.statusBar().showMessage(f"Utworzono folder: {item_name}")
- success = True
- self.project_tree.expand(new_index) # Rozwiń folder, żeby było widać nowy element
- else:
- try:
- with open(full_path, 'w') as f: pass # Utwórz pusty plik
- self.statusBar().showMessage(f"Utworzono plik: {item_name}")
- self.project_model.refresh(parent_index)
- success = True
- except OSError as e:
- QMessageBox.critical(self, "Błąd tworzenia", f"Nie można utworzyć pliku:\n{e}")
- self.statusBar().showMessage("Błąd tworzenia pliku.")
- except Exception as e:
- QMessageBox.critical(self, "Nieoczekiwany błąd", f"Wystąpił nieoczekiwany błąd:\n{e}")
- self.statusBar().showMessage("Nieoczekiwany błąd.")
- def _rename_item(self, index):
- """Zmienia nazwę pliku lub folderu."""
- if not index.isValid(): return
- old_path = self.project_model.filePath(index)
- old_name = os.path.basename(old_path)
- parent_dir = os.path.dirname(old_path)
- if old_path == self.project_model.rootPath() or old_path == self.project_model.filePath(self.project_model.index("")):
- QMessageBox.warning(self, "Błąd zmiany nazwy", "Nie można zmienić nazwy katalogu głównego.")
- self.statusBar().showMessage("Nie można zmienić nazwy katalogu głównego.")
- return
- dialog = RenameItemDialog(old_path, self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- new_name = dialog.get_new_name()
- if not new_name or new_name == old_name:
- self.statusBar().showMessage("Zmiana nazwy anulowana lub nowa nazwa jest taka sama.")
- return # Brak zmiany lub pusta nazwa (powinno być obsłużone w dialogu)
- new_path = os.path.join(parent_dir, new_name)
- open_files_to_close = []
- if os.path.isfile(old_path):
- if old_path in self.open_files:
- open_files_to_close.append(old_path)
- elif os.path.isdir(old_path):
- normalized_folder_path = os.path.normpath(old_path) + os.sep
- for open_file_path in self.open_files.keys():
- if os.path.normpath(open_file_path).startswith(normalized_folder_path):
- open_files_to_close.append(open_file_path)
- if open_files_to_close:
- open_file_names = [os.path.basename(p) for p in open_files_to_close]
- 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)
- reply = QMessageBox.question(self, "Zamknij pliki", msg + "\n\nCzy chcesz kontynuować?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
- if reply == QMessageBox.StandardButton.No:
- self.statusBar().showMessage("Zmiana nazwy anulowana.")
- return # Anuluj operację
- unsaved_open_files = [p for p in open_files_to_close if self.open_files.get(p) and self.open_files[p].document().isModified()]
- if unsaved_open_files:
- save_reply = QMessageBox.question(self, "Niezapisane zmiany",
- f"Niektóre z plików ({len(unsaved_open_files)}) mają niezapisane zmiany. Czy chcesz je zapisać przed zamknięciem i zmianą nazwy?",
- QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
- if save_reply == QMessageBox.StandardButton.Cancel:
- self.statusBar().showMessage("Zmiana nazwy anulowana (niezapisane zmiany).")
- return # Anuluj operację
- if save_reply == QMessageBox.StandardButton.Save:
- save_success = True
- for file_path_to_save in unsaved_open_files:
- editor = self.open_files.get(file_path_to_save)
- if editor and not self._save_file(editor, file_path_to_save): # _save_file handles Save As if needed
- save_success = False
- break
- if not save_success:
- self.statusBar().showMessage("Zmiana nazwy anulowana (błąd zapisu otwartych plików).")
- return # Anuluj operację
- for file_path_to_close in reversed(open_files_to_close):
- editor_widget = self.open_files.get(file_path_to_close)
- if editor_widget:
- tab_index = self.tab_widget.indexOf(editor_widget)
- if tab_index != -1:
- if hasattr(editor_widget, 'document'):
- editor_widget.document().setModified(False)
- self.tab_widget.removeTab(tab_index)
- del self.open_files[file_path_to_close]
- editor_widget.deleteLater()
- self.recents["open_files"] = [p for p in self.recents["open_files"] if p not in open_files_to_close]
- self._update_recent_files_menu()
- self._save_app_state() # Zapisz stan po zamknięciu plików
- success = self.project_model.rename(index, new_name)
- if success:
- self.statusBar().showMessage(f"Zmieniono nazwę na: {new_name}")
- if open_files_to_close:
- parent_index_after_rename = self.project_model.index(parent_dir)
- if parent_index_after_rename.isValid():
- self.project_model.refresh(parent_index_after_rename)
- for old_file_path in open_files_to_close:
- relative_path = os.path.relpath(old_file_path, old_path)
- new_file_path = os.path.join(new_path, relative_path)
- if os.path.exists(new_file_path) and os.path.isfile(new_file_path):
- self._open_file(new_file_path) # Otworzy plik pod nową ścieżką
- else:
- QMessageBox.critical(self, "Błąd zmiany nazwy", f"Nie można zmienić nazwy '{old_name}' na '{new_name}'.\n"
- "Sprawdź, czy element o tej nazwie już nie istnieje lub czy masz uprawnienia.")
- self.statusBar().showMessage("Błąd zmiany nazwy.")
- def _delete_item(self, index):
- """Usuwa plik lub folder."""
- if not index.isValid(): return
- file_path = self.project_model.filePath(index)
- item_name = os.path.basename(file_path)
- is_dir = self.project_model.fileInfo(index).isDir()
- if file_path == self.project_model.rootPath() or file_path == self.project_model.filePath(self.project_model.index("")):
- QMessageBox.warning(self, "Błąd usuwania", "Nie można usunąć katalogu głównego.")
- self.statusBar().showMessage("Nie można usunąć katalogu głównego.")
- return
- open_files_to_close = []
- if is_dir:
- normalized_folder_path = os.path.normpath(file_path) + os.sep
- for open_file_path in self.open_files.keys():
- if os.path.normpath(open_file_path).startswith(normalized_folder_path):
- open_files_to_close.append(open_file_path)
- elif file_path in self.open_files:
- open_files_to_close.append(file_path)
- if open_files_to_close:
- open_file_names = [os.path.basename(p) for p in open_files_to_close]
- 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)
- QMessageBox.warning(self, "Element jest używany", msg)
- reply_close = QMessageBox.question(self, "Zamknij pliki",
- f"Czy chcesz zamknąć te pliki, aby kontynuować usuwanie '{item_name}'?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
- if reply_close == QMessageBox.StandardButton.No:
- self.statusBar().showMessage(f"Usuwanie '{item_name}' anulowane.")
- return # Anuluj operację
- unsaved_open_files = [p for p in open_files_to_close if self.open_files.get(p) and self.open_files[p].document().isModified()]
- if unsaved_open_files:
- save_reply = QMessageBox.question(self, "Niezapisane zmiany",
- f"Niektóre z plików ({len(unsaved_open_files)}) mają niezapisane zmiany. Czy chcesz je zapisać przed zamknięciem i usunięciem?",
- QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
- if save_reply == QMessageBox.StandardButton.Cancel:
- self.statusBar().showMessage("Usuwanie anulowane (niezapisane zmiany).")
- return # Anuluj operację
- if save_reply == QMessageBox.StandardButton.Save:
- save_success = True
- for file_path_to_save in unsaved_open_files:
- editor = self.open_files.get(file_path_to_save)
- if editor and not self._save_file(editor, file_path_to_save): # _save_file handles Save As if needed
- save_success = False
- break
- if not save_success:
- self.statusBar().showMessage("Usuwanie anulowane (błąd zapisu otwartych plików).")
- return # Anuluj operację
- for file_path_to_close in reversed(open_files_to_close):
- editor_widget = self.open_files.get(file_path_to_close)
- if editor_widget:
- tab_index = self.tab_widget.indexOf(editor_widget)
- if tab_index != -1:
- if hasattr(editor_widget, 'document'):
- editor_widget.document().setModified(False)
- self.tab_widget.removeTab(tab_index)
- del self.open_files[file_path_to_close]
- editor_widget.deleteLater()
- self.recents["open_files"] = [p for p in self.recents["open_files"] if p not in open_files_to_close]
- self._update_recent_files_menu()
- self._save_app_state() # Zapisz stan po zamknięciu plików
- item_type = "folder" if is_dir else "plik"
- reply = QMessageBox.question(self, f"Usuń {item_type}",
- f"Czy na pewno chcesz usunąć {item_type} '{item_name}'?\\n"
- "Ta operacja jest nieodwracalna!", # Dodaj ostrzeżenie
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
- if reply == QMessageBox.StandardButton.Yes:
- success = self.project_model.remove(index)
- if success:
- self.statusBar().showMessage(f"Usunięto {item_type}: {item_name}")
- else:
- QMessageBox.critical(self, f"Błąd usuwania {item_type}", f"Nie można usunąć {item_type} '{item_name}'.\\n"
- "Sprawdź, czy masz uprawnienia lub czy element nie jest używany przez inny program.")
- self.statusBar().showMessage(f"Błąd usuwania {item_type}.")
- def _duplicate_file(self, index):
- """Duplikuje plik."""
- if not index.isValid(): return
- file_path = self.project_model.filePath(index)
- file_info = self.project_model.fileInfo(index)
- if not file_info.isFile():
- self.statusBar().showMessage("Można duplikować tylko pliki.")
- return
- parent_dir = os.path.dirname(file_path)
- old_name = os.path.basename(file_path)
- name, ext = os.path.splitext(old_name)
- suggested_name = f"{name}_kopia{ext}"
- counter = 1
- while os.path.exists(os.path.join(parent_dir, suggested_name)):
- counter += 1
- suggested_name = f"{name}_kopia{counter}{ext}"
- new_name, ok = QInputDialog.getText(self, "Duplikuj plik", f"Podaj nazwę dla kopii '{old_name}':",
- QLineEdit.EchoMode.Normal, suggested_name)
- if ok and new_name:
- new_name = new_name.strip()
- if not new_name or re.search(r'[<>:"/\\|?*\x00-\x1F]', new_name) is not None:
- QMessageBox.warning(self, "Nieprawidłowa nazwa", "Podana nazwa jest pusta lub zawiera niedozwolone znaki.")
- self.statusBar().showMessage("Duplikowanie anulowane (nieprawidłowa nazwa).")
- return
- new_path = os.path.join(parent_dir, new_name)
- if os.path.exists(new_path):
- QMessageBox.warning(self, "Element już istnieje", f"Element o nazwie '{new_name}' już istnieje.")
- self.statusBar().showMessage("Duplikowanie anulowane (element już istnieje).")
- return
- try:
- os.makedirs(os.path.dirname(new_path), exist_ok=True)
- shutil.copy2(file_path, new_path) # copy2 zachowuje metadane
- self.statusBar().showMessage(f"Utworzono kopię: {new_name}")
- parent_index = self.project_model.index(parent_dir)
- if parent_index.isValid():
- self.project_model.refresh(parent_index)
- else:
- root_path = self.project_model.rootPath()
- if root_path and os.path.isdir(root_path):
- self.project_model.refresh(self.project_model.index(root_path))
- except OSError as e:
- QMessageBox.critical(self, "Błąd duplikowania", f"Nie można zduplikować pliku '{old_name}':\n{e}")
- self.statusBar().showMessage("Błąd duplikowania pliku.")
- except Exception as e:
- QMessageBox.critical(self, "Nieoczekiwany błąd", f"Wystąpił nieoczekiwany błąd:\n{e}")
- self.statusBar().showMessage("Nieoczekiwany błąd.")
- def _close_tab_by_index(self, index):
- """Zamyka zakładkę o podanym indeksie, pytając o zapisanie zmian."""
- if index == -1: # Brak otwartych zakładek
- return
- widget = self.tab_widget.widget(index)
- if widget is None: # Zabezpieczenie
- return
- file_path_before_save = None
- for path, editor_widget in list(self.open_files.items()):
- if editor_widget is widget:
- file_path_before_save = path
- break
- if hasattr(widget, 'document') and widget.document().isModified():
- msg_box = QMessageBox(self)
- msg_box.setIcon(QMessageBox.Icon.Warning)
- msg_box.setWindowTitle("Niezapisane zmiany")
- tab_text = self.tab_widget.tabText(index).rstrip('*')
- display_name = os.path.basename(file_path_before_save) if file_path_before_save else tab_text
- msg_box.setText(f"Plik '{display_name}' ma niezapisane zmiany.\nCzy chcesz zapisać przed zamknięciem?")
- msg_box.setStandardButtons(QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
- msg_box.setDefaultButton(QMessageBox.StandardButton.Save)
- reply = msg_box.exec()
- if reply == QMessageBox.StandardButton.Save:
- needs_save_as = (file_path_before_save is None or
- not os.path.exists(file_path_before_save) or
- not QFileInfo(file_path_before_save).isFile())
- if needs_save_as:
- original_index = self.tab_widget.currentIndex()
- self.tab_widget.setCurrentIndex(index)
- save_success = self._save_current_file_as() # This handles saving and updating open_files/recents for the NEW path
- if original_index != -1 and original_index < self.tab_widget.count():
- self.tab_widget.setCurrentIndex(original_index)
- else: # If the original tab was closed during Save As (e.g. saving an existing file to itself)
- pass # Keep the new tab active
- if not save_success:
- self.statusBar().showMessage(f"Zamknięcie anulowane (błąd zapisu '{display_name}').")
- return # Anulowano lub błąd zapisu "jako", nie zamykaj zakładki
- else: # It's an existing file with a valid path
- if not self._save_file(widget, file_path_before_save): # Spróbuj zapisać, jeśli się nie uda, nie zamykaj
- self.statusBar().showMessage(f"Zamknięcie anulowane (błąd zapisu '{display_name}').")
- return # Anulowano lub błąd zapisu
- elif reply == QMessageBox.StandardButton.Cancel:
- self.statusBar().showMessage(f"Zamknięcie '{tab_text}' anulowane.")
- return # Anuluj zamknięcie
- if file_path_before_save in self.open_files:
- del self.open_files[file_path_before_save]
- if file_path_before_save in self.recents["open_files"]:
- self.recents["open_files"].remove(file_path_before_save)
- self._update_recent_files_menu() # Uaktualnij menu
- self.tab_widget.removeTab(index)
- widget.deleteLater() # Usuń widget z pamięci
- if file_path_before_save:
- self.statusBar().showMessage(f"Zamknięto plik: {os.path.basename(file_path_before_save)}")
- else:
- self.statusBar().showMessage("Zamknięto plik.")
- self._save_app_state()
- def _close_current_tab(self):
- """Zamyka aktualnie aktywną zakładkę."""
- current_index = self.tab_widget.currentIndex()
- if current_index != -1:
- self._close_tab_by_index(current_index)
- def _save_current_file(self):
- """Zapisuje aktualnie aktywny plik. Jeśli nowy, wywołuje Save As."""
- current_widget = self.tab_widget.currentWidget()
- if not isinstance(current_widget, QPlainTextEdit):
- self.statusBar().showMessage("Brak aktywnego pliku do zapisu.")
- return False
- file_path = None
- for path, editor_widget in list(self.open_files.items()):
- if editor_widget is current_widget:
- file_path = path
- break
- is_existing_valid_file = file_path and os.path.exists(file_path) and QFileInfo(file_path).isFile()
- if is_existing_valid_file:
- return self._save_file(current_widget, file_path)
- else:
- return self._save_current_file_as()
- def _save_current_file_as(self):
- """Zapisuje zawartość aktywnego edytora z nową nazwą."""
- current_widget = self.tab_widget.currentWidget()
- if not isinstance(current_widget, QPlainTextEdit):
- self.statusBar().showMessage("Brak aktywnego pliku do zapisu.")
- return False
- old_file_path = None
- for path, editor_widget in list(self.open_files.items()): # Iterate over a copy
- if editor_widget is current_widget:
- old_file_path = path
- break
- suggested_name = "bez_nazwy.txt"
- current_tab_index = self.tab_widget.indexOf(current_widget)
- if current_tab_index != -1:
- original_tab_text = self.tab_widget.tabText(current_tab_index).rstrip('*')
- if original_tab_text and original_tab_text != "Nowy plik":
- suggested_name = original_tab_text
- elif current_widget.document().toPlainText().strip():
- first_line = current_widget.document().toPlainText().strip().split('\n')[0].strip()
- if first_line:
- suggested_name = re.sub(r'[\\/:*?"<>|]', '_', first_line) # Remove illegal chars
- suggested_name = suggested_name[:50].strip() # Limit length
- if not suggested_name:
- suggested_name = "bez_nazwy"
- if '.' not in os.path.basename(suggested_name):
- suggested_name += ".txt"
- else:
- suggested_name = "bez_nazwy.txt" # Fallback if content is just whitespace
- start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
- if old_file_path and os.path.dirname(old_file_path):
- start_path = os.path.dirname(old_file_path) # Use directory of old file if available
- elif os.path.isdir(start_path):
- pass # Use project dir if available
- else:
- start_path = os.path.expanduser("~")
- 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)"
- new_file_path, _ = QFileDialog.getSaveFileName(self, "Zapisz plik jako...", os.path.join(start_path, suggested_name), file_filters)
- if not new_file_path:
- self.statusBar().showMessage("Zapisywanie anulowane.")
- return False # Cancelled
- new_file_path = os.path.normpath(new_file_path)
- if old_file_path and old_file_path != new_file_path:
- if old_file_path in self.open_files:
- del self.open_files[old_file_path]
- if old_file_path in self.recents["open_files"]:
- self.recents["open_files"].remove(old_file_path)
- self._update_recent_files_menu()
- self.open_files[new_file_path] = current_widget
- current_tab_index = self.tab_widget.indexOf(current_widget)
- if current_tab_index != -1:
- self.tab_widget.setTabText(current_tab_index, os.path.basename(new_file_path))
- if new_file_path in self.recents["open_files"]: # Remove if already exists to move to front
- self.recents["open_files"].remove(new_file_path)
- self.recents["open_files"].insert(0, new_file_path)
- self._update_recent_files_menu() # Update the menu
- language = self._get_language_from_path(new_file_path)
- old_highlighter = getattr(current_widget.document(), '_syntax_highlighter', None)
- if old_highlighter:
- old_highlighter.setDocument(None) # Detach
- new_highlighter = CodeSyntaxHighlighter(current_widget.document(), language)
- setattr(current_widget.document(), '_syntax_highlighter', new_highlighter)
- return self._save_file(current_widget, new_file_path)
- def _save_file(self, editor_widget, file_path):
- """Zapisuje zawartość wskazanego edytora do wskazanego pliku."""
- if not file_path:
- print("Error: _save_file called with empty path.", file=sys.stderr)
- self.statusBar().showMessage("Błąd wewnętrzny: próba zapisu bez ścieżki.")
- return False
- try:
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
- with open(file_path, 'w', encoding='utf-8') as f:
- f.write(editor_widget.toPlainText())
- editor_widget.document().setModified(False) # Mark document as unmodified
- self.statusBar().showMessage(f"Plik zapisano pomyślnie: {os.path.basename(file_path)}")
- tab_index = self.tab_widget.indexOf(editor_widget)
- if tab_index != -1:
- current_tab_text = self.tab_widget.tabText(tab_index).rstrip('*')
- self.tab_widget.setTabText(tab_index, current_tab_text)
- file_info = QFileInfo(file_path)
- dir_path = file_info.dir().path()
- root_path = self.project_model.rootPath()
- if root_path and dir_path.startswith(root_path):
- dir_index = self.project_model.index(dir_path)
- if dir_index.isValid():
- self.project_model.refresh(dir_index)
- file_index = self.project_model.index(file_path)
- if file_index.isValid():
- self.project_model.dataChanged.emit(file_index, file_index, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.DecorationRole]) # Trigger a view update
- if file_path in self.recents["open_files"]:
- self.recents["open_files"].remove(file_path)
- self.recents["open_files"].insert(0, file_path)
- self._update_recent_files_menu()
- self._save_app_state() # Save state after successful save
- return True # Save succeeded
- except Exception as e:
- QMessageBox.critical(self, "Błąd zapisu pliku", f"Nie można zapisać pliku {os.path.basename(file_path)}:\n{e}")
- self.statusBar().showMessage(f"Błąd zapisu pliku: {os.path.basename(file_path)}")
- return False # Save failed
- def _save_all_files(self):
- """Zapisuje wszystkie otwarte i zmodyfikowane pliki."""
- unsaved_files = [path for path, editor in self.open_files.items() if editor.document().isModified()]
- if not unsaved_files:
- self.statusBar().showMessage("Brak zmodyfikowanych plików do zapisu.")
- return True # Nothing to save, counts as success
- self.statusBar().showMessage(f"Zapisywanie wszystkich zmodyfikowanych plików ({len(unsaved_files)})...")
- total_saved = 0
- total_failed = 0
- files_to_save = list(unsaved_files)
- for file_path in files_to_save:
- editor_widget = self.open_files.get(file_path)
- if editor_widget is None or self.tab_widget.indexOf(editor_widget) == -1:
- print(f"Warning: Skipping save for {file_path} - editor widget not found or invalid.", file=sys.stderr)
- continue # Go to the next file
- if not editor_widget.document().isModified():
- continue # Already saved
- needs_save_as = (file_path is None or
- not os.path.exists(file_path) or
- not QFileInfo(file_path).isFile())
- save_success = False
- if needs_save_as:
- tab_index = self.tab_widget.indexOf(editor_widget)
- if tab_index != -1:
- original_index = self.tab_widget.currentIndex()
- self.tab_widget.setCurrentIndex(tab_index)
- save_success = self._save_current_file_as()
- if original_index != -1 and original_index < self.tab_widget.count():
- self.tab_widget.setCurrentIndex(original_index)
- else:
- 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)
- total_failed += 1
- continue # Skip this file, couldn't save it via Save As mechanism
- else:
- save_success = self._save_file(editor_widget, file_path) # This updates modified state and status bar
- if save_success:
- total_saved += 1
- else:
- total_failed += 1
- if total_saved > 0 and total_failed == 0:
- self.statusBar().showMessage(f"Zapisano pomyślnie wszystkie {total_saved} pliki.")
- return True
- elif total_saved > 0 and total_failed > 0:
- self.statusBar().showMessage(f"Zapisano {total_saved} plików, {total_failed} plików nie udało się zapisać.")
- 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.")
- return False
- elif total_saved == 0 and total_failed > 0:
- self.statusBar().showMessage(f"Nie udało się zapisać żadnego z {total_failed} plików.")
- 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.")
- return False
- else: # total_saved == 0 and total_failed == 0 (already handled by initial check, but good fallback)
- self.statusBar().showMessage("Brak zmodyfikowanych plików do zapisu.")
- return True
- def _handle_modification_changed(self, modified):
- """Obsługa zmiany stanu modyfikacji dokumentu w aktywnym edytorze."""
- editor_document = self.sender() # Dokument, który wywołał sygnał
- if not isinstance(editor_document, QTextDocument): return
- editor = None
- for editor_widget in self.open_files.values():
- if editor_widget.document() is editor_document:
- editor = editor_widget
- break
- if editor is None:
- return
- index = self.tab_widget.indexOf(editor)
- if index != -1:
- tab_text = self.tab_widget.tabText(index)
- if modified and not tab_text.endswith('*'):
- self.tab_widget.setTabText(index, tab_text + '*')
- elif not modified and tab_text.endswith('*'):
- self.tab_widget.setTabText(index, tab_text.rstrip('*'))
- def _handle_tab_change(self, index):
- """Obsługa zmiany aktywnej zakładki."""
- self._hide_find_bar()
- if index != -1:
- widget = self.tab_widget.widget(index)
- if isinstance(widget, QPlainTextEdit):
- file_path = next((path for path, ed in self.open_files.items() if ed is widget), None)
- if file_path:
- self.statusBar().showMessage(f"Edytujesz: {os.path.basename(file_path)}")
- else:
- self.statusBar().showMessage("Edytujesz: Nowy plik")
- else:
- self.statusBar().showMessage("Gotowy.") # Reset status bar
- def _find_next(self):
- """Znajduje kolejne wystąpienie tekstu z pola wyszukiwania."""
- text_to_find = self.search_input.text()
- self._find_text(text_to_find, 'next')
- def _find_previous(self):
- """Znajduje poprzednie wystąpienie tekstu z pola wyszukiwania."""
- text_to_find = self.search_input.text()
- self._find_text(text_to_find, 'previous')
- def _find_text(self, text, direction='next'):
- """Szuka tekstu w aktualnym edytorze."""
- editor = self.tab_widget.currentWidget()
- if not isinstance(editor, QPlainTextEdit):
- self.statusBar().showMessage("Brak aktywnego edytora do wyszukiwania.")
- return
- if not text:
- self.statusBar().showMessage("Wpisz tekst do wyszukania.")
- return
- flags = QTextDocument.FindFlag(0) # Default: case-sensitive in Qt find
- if direction == 'previous':
- flags |= QTextDocument.FindFlag.FindBackward
- found = editor.find(text, flags)
- if found:
- self.statusBar().showMessage(f"Znaleziono '{text}'.")
- else:
- self.statusBar().showMessage(f"Nie znaleziono '{text}'. Zawijanie...")
- cursor = editor.textCursor()
- original_position = cursor.position() # Remember position before wrapping search
- cursor.clearSelection() # Clear selection before moving cursor position programmatically
- cursor.movePosition(cursor.MoveOperation.Start if direction == 'next' else cursor.MoveOperation.End)
- editor.setTextCursor(cursor)
- found_wrapped = editor.find(text, flags)
- if found_wrapped:
- self.statusBar().showMessage(f"Znaleziono '{text}' po zawinięciu.")
- else:
- self.statusBar().showMessage(f"Nie znaleziono '{text}' w całym pliku.")
- cursor.clearSelection()
- cursor.setPosition(original_position)
- editor.setTextCursor(cursor)
- def _show_find_bar(self):
- """Pokazuje pasek wyszukiwania."""
- if self.search_input.isVisible():
- self._hide_find_bar()
- return
- self.search_input.setVisible(True)
- self.find_next_button.setVisible(True)
- self.find_prev_button.setVisible(True)
- self.search_input.setFocus() # Ustaw kursor w polu wyszukiwania
- def _hide_find_bar(self):
- """Ukrywa pasek wyszukiwania."""
- if self.search_input.isVisible(): # Check if visible before hiding
- self.search_input.setVisible(False)
- self.find_next_button.setVisible(False)
- self.find_prev_button.setVisible(False)
- self.search_input.clear() # Wyczyść pole wyszukiwania
- def _show_settings_dialog(self):
- """Wyświetla okno dialogowe ustawień."""
- temp_settings = self.settings.copy()
- dialog = SettingsDialog(temp_settings, self) # Pass the copy
- font_size_label = QLabel("Rozmiar czcionki edytora:")
- font_size_spinbox = QSpinBox()
- font_size_spinbox.setRange(6, 72) # Typical font sizes
- font_size_spinbox.setValue(temp_settings.get("editor_font_size", 10))
- dialog_layout = dialog.layout()
- button_box_row_index = dialog_layout.rowCount() - 1
- dialog_layout.insertRow(button_box_row_index, font_size_label, font_size_spinbox)
- setattr(dialog, 'editor_font_size_spinbox', font_size_spinbox)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- updated_settings = dialog.get_settings()
- updated_settings["editor_font_size"] = font_size_spinbox.value()
- self.settings = updated_settings
- self._apply_theme(self.settings.get("theme", "light")) # Apply theme
- self._apply_editor_font_size() # Apply new font size
- self._apply_view_settings() # Apply view settings (tree/console visibility)
- self._save_app_state() # Save updated settings and state
- self.statusBar().showMessage("Ustawienia zapisane.")
- else:
- self.statusBar().showMessage("Zmiany w ustawieniach anulowane.")
- def _show_about_dialog(self):
- """Wyświetla okno informacyjne 'O programie'."""
- QMessageBox.about(self, "O programie",
- "<h2>Proste IDE</h2>"
- "<p>Prosty edytor kodu napisany w Pythonie z użyciem biblioteki PyQt6.</p>"
- "<p>Wersja: 1.0</p>"
- "<p>Autor: [Twoje imię lub nazwa]</p>"
- "<p>Dostępne funkcje:</p>"
- "<ul>"
- "<li>Przeglądanie plików i folderów</li>"
- "<li>Otwieranie i edycja plików tekstowych/kodów</li>"
- "<li>Kolorowanie składni (Python, JavaScript, HTML, CSS, C++, INI, JSON)</li>"
- "<li>Uruchamianie plików Python/JavaScript (z konfigurowalnymi ścieżkami)</li>"
- "<li>Uruchamianie skryptów npm (po otwarciu folderu z package.json)</li>"
- "<li>**Wprowadzanie poleceń w konsoli**</li>" # Dodano
- "<li>Zapisywanie plików (Zapisz, Zapisz jako, Zapisz wszystko)</li>"
- "<li>Historia ostatnio otwieranych plików/folderów</li>"
- "<li>Proste wyszukiwanie tekstu</li>"
- "<li>Podstawowe motywy (Jasny/Ciemny)</li>"
- "<li>Ukrywanie/pokazywanie paneli (Drzewko, Konsola)</li>"
- "<li>Zmiana rozmiaru czcionki edytora</li>"
- "<li>Operacje na plikach/folderach (Nowy, Zmień nazwę, Usuń, Duplikuj)</li>"
- "</ul>"
- "<p>Użyte technologie: PyQt6, Python, opcjonalnie qtawesome.</p>")
- def _update_run_button_menu(self):
- """Uaktualnia menu przypisane do QToolButton 'Uruchom'."""
- menu = QMenu(self)
- menu.addAction(self.action_run_file)
- if self.node_scripts:
- menu.addSeparator()
- menu.addSection("Skrypty npm:")
- sorted_scripts = sorted(self.node_scripts.keys())
- for script_name in sorted_scripts:
- action = QAction(script_name, self)
- action.triggered.connect(lambda checked, name=script_name: self._run_npm_script(name))
- menu.addAction(action)
- else:
- no_scripts_action = QAction("Brak skryptów npm (package.json)", self)
- no_scripts_action.setEnabled(False) # Wyłącz akcję
- menu.addAction(no_scripts_action)
- self.run_toolbutton.setMenu(menu)
- def _run_current_file(self):
- """Uruchamia aktualnie otwarty plik."""
- current_widget = self.tab_widget.currentWidget()
- if not isinstance(current_widget, QPlainTextEdit):
- self._append_console_output("Brak aktywnego pliku do uruchomienia.", is_error=True)
- return
- file_path = next((path for path, editor_widget in self.open_files.items() if editor_widget is current_widget), None)
- if not file_path or not os.path.exists(file_path) or not os.path.isfile(file_path):
- self._append_console_output("Ścieżka aktywnego pliku jest nieprawidłowa lub plik nie istnieje.", is_error=True)
- return
- if current_widget.document().isModified():
- self._append_console_output("Zapisywanie pliku przed uruchomieniem...")
- if not self._save_file(current_widget, file_path):
- self._append_console_output("Zapisywanie nie powiodło się. Anulowano uruchomienie.", is_error=True)
- return
- language = self._get_language_from_path(file_path)
- command = []
- working_dir = os.path.dirname(file_path) # Directory of the file
- python_path = self.settings.get("python_path")
- node_path = self.settings.get("node_path")
- if language == 'python':
- interpreter = python_path if python_path and os.path.exists(python_path) else "python"
- command = [interpreter, "-u", file_path]
- elif language == 'javascript':
- interpreter = node_path if node_path and os.path.exists(node_path) else "node"
- command = [interpreter, file_path]
- elif language in ['html', 'css']:
- self._append_console_output(f"Otwieranie pliku {os.path.basename(file_path)} w domyślnej przeglądarce...")
- try:
- QDesktopServices.openUrl(QUrl.fromLocalFile(file_path))
- self.statusBar().showMessage(f"Otwarto plik w przeglądarce: {os.path.basename(file_path)}")
- except Exception as e:
- self._append_console_output(f"Błąd podczas otwierania pliku w przeglądarce: {e}", is_error=True)
- self.statusBar().showMessage("Błąd otwierania w przeglądarce.")
- return # Do not run as a process
- else:
- self._append_console_output(f"Nieznany typ pliku do uruchomienia: {language}", is_error=True)
- return
- if command:
- self._run_command(command, working_dir)
- def _check_package_json(self, folder_path):
- """Checks if package.json exists in the folder and parses scripts."""
- if not folder_path or not os.path.isdir(folder_path):
- self.node_scripts = {} # Clear any previous scripts
- self._update_run_button_menu() # Update run menu button
- return
- package_json_path = os.path.join(folder_path, 'package.json')
- self.node_scripts = {} # Clear previous scripts
- if os.path.exists(package_json_path):
- try:
- with open(package_json_path, 'r', encoding='utf-8') as f:
- package_data = json.load(f)
- scripts = package_data.get('scripts', {})
- if isinstance(scripts, dict) and scripts:
- self.node_scripts = scripts
- self._append_console_output(f"Znaleziono {len(scripts)} skryptów w package.json w {os.path.basename(folder_path)}.")
- else:
- self._append_console_output(f"Znaleziono package.json w {os.path.basename(folder_path)}, ale brak skryptów w sekcji 'scripts'.")
- except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
- self._append_console_output(f"Błąd podczas parsowania package.json w {os.path.basename(folder_path)}:\n{e}", is_error=True)
- self.node_scripts = {} # Ensure scripts are empty on error
- else:
- pass # Silence is golden
- self._update_run_button_menu() # After checking package.json, update the run menu button
- def _run_npm_script(self, script_name):
- """Runs the specified npm script."""
- if not self.current_project_dir or not os.path.isdir(self.current_project_dir):
- self._append_console_output("Brak otwartego folderu projektu do uruchomienia skryptu npm.", is_error=True)
- return
- if script_name not in self.node_scripts:
- self._append_console_output(f"Skrypt '{script_name}' nie znaleziono w package.json otwartego projektu.", is_error=True)
- return
- node_path = self.settings.get("node_path")
- npm_command = "npm" # Default command
- if node_path and os.path.exists(node_path):
- node_dir = os.path.dirname(node_path)
- npm_candidates = [os.path.join(node_dir, 'npm')]
- if platform.system() == "Windows":
- npm_candidates.append(os.path.join(node_dir, 'npm.cmd'))
- found_npm = None
- for candidate in npm_candidates:
- if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
- found_npm = candidate
- break
- if found_npm:
- npm_command = found_npm
- else:
- 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)
- command = [npm_command, "run", script_name]
- working_dir = self.current_project_dir # Npm scripts are run in the project directory
- self._run_command(command, working_dir)
- def _run_console_command(self):
- """Uruchamia komendę wpisaną w polu tekstowym konsoli."""
- command_text = self.console_input.text().strip()
- if not command_text:
- return # Nic nie wpisano
- self.console_input.clear() # Wyczyść pole wprowadzania
- self._append_console_output(f"> {command_text}") # Wyświetl wpisaną komendę w konsoli
- try:
- command = shlex.split(command_text)
- except ValueError as e:
- self._append_console_output(f"Błąd parsowania komendy: {e}", is_error=True)
- self.statusBar().showMessage("Błąd parsowania komendy.")
- return
- if not command:
- self._append_console_output("Błąd: Pusta komenda po parsowaniu.", is_error=True)
- self.statusBar().showMessage("Błąd: Pusta komenda.")
- return
- working_dir = self.current_project_dir if self.current_project_dir and os.path.isdir(self.current_project_dir) else os.getcwd()
- self._run_command(command, working_dir)
- def _run_command(self, command, working_dir):
- """Uruchamia podaną komendę w QProcess."""
- if self.process.state() != QProcess.ProcessState.NotRunning:
- self._append_console_output("Inny proces jest już uruchomiony. Zakończ go, aby uruchomić nowy.", is_error=True)
- 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).")
- return
- command_str = shlex.join(command) # Better command formatting
- self._append_console_output(f"Uruchamianie: {command_str}\nw katalogu: {working_dir}\n---")
- self.statusBar().showMessage("Proces uruchomiony...")
- try:
- if not command:
- self._append_console_output("Komenda do uruchomienia jest pusta.", is_error=True)
- self.statusBar().showMessage("Błąd: Pusta komenda.")
- return
- program = command[0]
- arguments = command[1:]
- self.process.setWorkingDirectory(working_dir)
- process_environment = QProcessEnvironment.systemEnvironment()
- current_path = process_environment.value("PATH", "") # Get system PATH
- paths_to_prepend = []
- py_path = self.settings.get("python_path")
- if py_path and os.path.exists(py_path):
- py_dir = os.path.dirname(py_path)
- current_path_dirs = [os.path.normcase(p) for p in current_path.split(os.pathsep) if p]
- if os.path.normcase(py_dir) not in current_path_dirs:
- paths_to_prepend.append(py_dir)
- node_path = self.settings.get("node_path")
- if node_path and os.path.exists(node_path):
- node_dir = os.path.dirname(node_path)
- if os.path.normcase(node_dir) not in current_path_dirs:
- paths_to_prepend.append(node_dir)
- if paths_to_prepend:
- new_path = os.pathsep.join(paths_to_prepend) + (os.pathsep + current_path if current_path else "")
- else:
- new_path = current_path # No new paths to add
- process_environment.insert("PATH", new_path) # Overwrite or add PATH
- if platform.system() == "Windows" and process_environment.value("Path") is not None:
- if process_environment.value("Path") != current_path: # Avoid unnecessary update
- process_environment.insert("Path", new_path)
- self.process.setProcessEnvironment(process_environment)
- self.process.start(program, arguments)
- if not self.process.waitForStarted(1000):
- error_string = self.process.errorString()
- self._append_console_output(f"Nie udało się uruchomić procesu '{program}': {error_string}", is_error=True)
- self.statusBar().showMessage(f"Błąd uruchamiania: {program}")
- return # Process didn't start, exit
- except Exception as e:
- self._append_console_output(f"Wystąpił nieoczekiwany błąd podczas próby uruchomienia:\n{e}", is_error=True)
- self.statusBar().showMessage("Błąd podczas uruchamiania.")
- def _append_console_output(self, text, is_error=False):
- """Adds text to the console, optionally formatting as an error."""
- cursor = self.console.textCursor()
- cursor.movePosition(cursor.MoveOperation.End)
- original_fmt = cursor.charFormat()
- fmt = QTextCharFormat()
- if is_error:
- fmt.setForeground(QColor("#DC143C")) # Crimson
- cursor.setCharFormat(fmt)
- text_to_insert = text
- if text and not text.endswith('\n'):
- text_to_insert += '\n'
- cursor.insertText(text_to_insert)
- cursor.setCharFormat(original_fmt)
- self.console.ensureCursorVisible()
- def _handle_stdout(self):
- """Reads standard output of the process and displays it in the console."""
- while self.process.bytesAvailable(): # Correct usage
- data = self.process.readAllStandardOutput()
- try:
- text = bytes(data).decode('utf-8')
- except UnicodeDecodeError:
- try:
- text = bytes(data).decode('latin-1')
- except Exception:
- text = bytes(data).decode('utf-8', errors='replace') # Replace unknown characters
- self._append_console_output(text)
- def _handle_stderr(self):
- """Reads standard error of the process and displays it in the console (in red)."""
- while self.process.bytesAvailable(): # Correct usage
- data = self.process.readAllStandardError()
- try:
- text = bytes(data).decode('utf-8')
- except UnicodeDecodeError:
- try:
- text = bytes(data).decode('latin-1')
- except Exception:
- text = bytes(data).decode('utf-8', errors='replace') # Replace unknown characters
- self._append_console_output(text, is_error=True)
- def _handle_process_finished(self, exitCode, exitStatus):
- """Handles the process finishing."""
- self._handle_stdout()
- self._handle_stderr()
- self._append_console_output("\n--- Zakończono proces ---") # Add a clear separator
- if exitStatus == QProcess.ExitStatus.NormalExit:
- self._append_console_output(f"Proces zakończył się z kodem wyjścia: {exitCode}")
- self.statusBar().showMessage(f"Proces zakończył się. Kod wyjścia: {exitCode}")
- else:
- self._append_console_output(f"Proces zakończył się awaryjnie z kodem wyjścia: {exitCode}", is_error=True)
- self.statusBar().showMessage(f"Proces zakończył się awaryjnie. Kod wyjścia: {exitCode}")
- if self.settings.get("show_console", True):
- self._show_console_panel() # Ensure console is visible and correctly sized
- def _copy_console(self):
- """Copies the entire content of the console to the clipboard."""
- clipboard = QApplication.clipboard()
- clipboard.setText(self.console.toPlainText())
- self.statusBar().showMessage("Zawartość konsoli skopiowana do schowka.")
- def _toggle_tree_view(self, checked):
- """Shows/hides the file tree panel."""
- self.main_splitter.widget(0).setVisible(checked)
- self.settings["show_tree"] = checked # Save state
- self._save_app_state() # Save settings change
- self._apply_view_settings() # Re-apply sizes to ensure panels aren't too small/large
- def _toggle_console(self, checked):
- """Shows/hides the console panel."""
- self.right_splitter.widget(1).setVisible(checked)
- self.settings["show_console"] = checked # Save state
- self._save_app_state() # Save settings change
- if checked:
- self._show_console_panel() # Use method that sets sizes
- def _show_console_panel(self):
- """Ensures the console panel is visible and has a reasonable size."""
- self.right_splitter.widget(1).setVisible(True)
- self.action_toggle_console.setChecked(True)
- sizes = self.right_splitter.sizes()
- if len(sizes) == 2:
- total_height = sum(sizes)
- min_console_height = 100
- min_editor_height = 100
- if sizes[1] < min_console_height or (sizes[1] < 10 and self.settings.get("show_console", True)):
- if total_height > min_console_height + min_editor_height:
- self.right_splitter.setSizes([total_height - min_console_height, min_console_height])
- elif total_height > 200: # Ensure some minimal height if window is small
- self.right_splitter.setSizes([total_height // 2, total_height // 2]) # Split equally
- def _apply_view_settings(self):
- """Apply panel visibility settings after loading."""
- show_tree = self.settings.get("show_tree", True)
- show_console = self.settings.get("show_console", True)
- self.main_splitter.widget(0).setVisible(show_tree)
- self.action_toggle_tree.setChecked(show_tree)
- self.right_splitter.widget(1).setVisible(show_console)
- self.action_toggle_console.setChecked(show_console)
- main_sizes = self.main_splitter.sizes()
- if len(main_sizes) == 2:
- total_width = sum(main_sizes)
- min_tree_width = 150 # Minimal reasonable width for the tree view
- min_right_panel_width = 200 # Minimal width for the editor/console side
- if main_sizes[0] < min_tree_width or (main_sizes[0] < 10 and show_tree):
- if total_width > min_tree_width + min_right_panel_width:
- self.main_splitter.setSizes([min_tree_width, total_width - min_tree_width])
- elif total_width > 300: # Ensure some minimal width if window is small
- self.main_splitter.setSizes([total_width // 3, 2 * total_width // 3]) # Split approx 1/3, 2/3
- right_sizes = self.right_splitter.sizes()
- if len(right_sizes) == 2:
- total_height = sum(right_sizes)
- min_console_height = 100
- min_editor_height = 100 # Minimal height for the editor area
- if right_sizes[1] < min_console_height or (right_sizes[1] < 10 and show_console):
- if total_height > min_console_height + min_editor_height:
- self.right_splitter.setSizes([total_height - min_console_height, min_console_height])
- elif total_height > 200: # Ensure some minimal height if window is small
- self.right_splitter.setSizes([total_height // 2, total_height // 2]) # Split equally
- elif right_sizes[0] < min_editor_height and show_console and total_height > min_console_height + min_editor_height:
- self.right_splitter.setSizes([min_editor_height, total_height - min_editor_height])
- def _apply_editor_font_size(self):
- """Apply the editor font size to all open editors and the console."""
- font_size = self.settings.get("editor_font_size", 10)
- new_font = QFont("Courier New", font_size) # Use Courier New, change size
- self.base_editor_font = new_font # Update the base font
- self.console.setFont(new_font)
- self.console_input.setFont(new_font)
- for editor_widget in self.open_files.values():
- editor_widget.setFont(new_font)
- if hasattr(editor_widget.document(), '_syntax_highlighter'):
- editor_widget.document().rehighlight()
- def _apply_theme(self, theme_name):
- """Applies the selected color theme."""
- if theme_name == "dark":
- self.setStyleSheet("""
- QMainWindow, QWidget {
- background-color: #2E2E2E;
- color: #D3D3D3;
- }
- QMenuBar {
- background-color: #3C3C3C;
- color: #D3D3D3;
- }
- QMenuBar::item:selected {
- background-color: #505050;
- }
- QMenu {
- background-color: #3C3C3C;
- color: #D3D3D3;
- border: 1px solid #505050;
- }
- QMenu::item:selected {
- background-color: #505050;
- }
- QToolBar {
- background-color: #3C3C3C;
- color: #D3D3D3;
- spacing: 5px;
- padding: 2px;
- }
- QToolButton { /* Buttons on toolbar */
- background-color: transparent;
- border: 1px solid transparent;
- padding: 3px;
- border-radius: 4px; /* Slight rounding */
- }
- QToolButton:hover {
- border: 1px solid #505050;
- background-color: #454545;
- }
- QToolButton:pressed {
- background-color: #404040;
- }
- QPushButton { /* Standard buttons (e.g., Run, Find) */
- background-color: #505050;
- color: #D3D3D3;
- border: 1px solid #606060;
- padding: 4px 8px;
- border-radius: 4px;
- }
- QPushButton:hover {
- background-color: #606060;
- }
- QPushButton:pressed {
- background-color: #404040;
- }
- QStatusBar {
- background-color: #3C3C3C;
- color: #D3D3D3;
- }
- QSplitter::handle {
- background-color: #505050;
- }
- QSplitter::handle:horizontal {
- width: 5px;
- }
- QSplitter::handle:vertical {
- height: 5px;
- }
- QTreeView {
- background-color: #1E1E1E;
- color: #D3D3D3;
- border: 1px solid #3C3C3C;
- alternate-background-color: #252525; /* Alternating row colors */
- }
- QTreeView::item:selected {
- background-color: #007acc; /* Selection color (VS Code blue) */
- color: white;
- }
- QTreeView::branch:selected {
- background-color: #007acc; /* Selection color for branch indicator */
- }
- QTabWidget::pane { /* Frame around tab content area */
- border: 1px solid #3C3C3C;
- background-color: #1E1E1E;
- }
- QTabWidget::tab-bar:top {
- left: 5px; /* Leave some space on the left */
- }
- QTabBar::tab {
- background: #3C3C3C;
- color: #D3D3D3;
- border: 1px solid #3C3C3C;
- border-bottom-color: #1E1E1E; /* Blend bottom border with pane background */
- border-top-left-radius: 4px;
- border-top-right-radius: 4px;
- padding: 4px 8px;
- margin-right: 1px;
- }
- QTabBar::tab:selected {
- background: #1E1E1E; /* Match pane background for selected tab */
- border-bottom-color: #1E1E1E;
- }
- QTabBar::tab:hover {
- background: #454545; /* Hover effect */
- }
- /* Optional: Style for close button */
- /* QTabBar::close-button { image: url(:/path/to/close_icon_dark.png); } */
- QPlainTextEdit { /* Code Editor */
- background-color: #1E1E1E;
- color: #D3D3D3;
- border: none; /* Border is on QTabWidget::pane */
- selection-background-color: #007acc; /* Selection color */
- selection-color: white;
- }
- QPlainTextEdit[readOnly="true"] { /* Console */
- background-color: #1E1E1E; /* Same as editor background */
- color: #CCCCCC; /* Console text color */
- selection-background-color: #007acc;
- selection-color: white;
- }
- QLineEdit { /* Search bar, input fields in settings */
- background-color: #3C3C3C;
- color: #D3D3D3;
- border: 1px solid #505050;
- padding: 2px;
- selection-background-color: #007acc;
- selection-color: white;
- border-radius: 3px; /* Slight rounding */
- }
- QComboBox { /* ComboBox in settings */
- background-color: #3C3C3C;
- color: #D3D3D3;
- border: 1px solid #505050;
- padding: 2px;
- border-radius: 3px;
- }
- QComboBox::drop-down {
- border: 0px; /* Remove default arrow */
- }
- QComboBox QAbstractItemView { /* Dropdown list of ComboBox */
- background-color: #3C3C3C;
- color: #D3D3D3;
- selection-background-color: #007acc;
- border: 1px solid #505050;
- }
- QDialog { /* Settings dialog window */
- background-color: #2E2E2E;
- color: #D3D3D3;
- }
- QLabel { /* Labels in dialogs */
- color: #D3D3D3;
- }
- QDialogButtonBox QPushButton { /* Buttons in dialogs */
- background-color: #505050;
- color: #D3D3D3;
- border: 1px solid #606060;
- padding: 5px 10px;
- border-radius: 4px;
- }
- QDialogButtonBox QPushButton:hover {
- background-color: #606060;
- }
- QDialogButtonBox QPushButton:pressed {
- background-color: #404040;
- }
- QSpinBox { /* Spinbox in settings */
- background-color: #3C3C3C;
- color: #D3D3D3;
- border: 1px solid #505050;
- padding: 2px;
- selection-background-color: #007acc;
- selection-color: white;
- border-radius: 3px;
- }
- QFrame { /* Frames like the one in SettingsDialog */
- border: none;
- }
- /* Style for the console input field */
- QLineEdit[placeholderText="Wpisz polecenie..."] {
- background-color: #3C3C3C;
- color: #D3D3D3;
- border: 1px solid #505050;
- padding: 2px;
- margin: 0px; /* Remove default margins */
- border-radius: 0px; /* No rounding for console input */
- }
- """)
- elif theme_name == "light":
- self.setStyleSheet("")
- self.statusBar().showMessage(f"Zmieniono motyw na: {theme_name.capitalize()}")
- if __name__ == '__main__':
- app = QApplication(sys.argv)
- QLocale.setDefault(QLocale(QLocale.Language.Polish, QLocale.Country.Poland))
- main_window = IDEWindow()
- main_window.show()
- sys.exit(app.exec())
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement