diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9354e..61cb289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.3 +* Added line numbers to the code views +* Refactoring of classes and modules + ## 1.2.1 * Added versioner that makes a back up of the settings file on startup. Back ups are saved in a .local_history folder next to the settings file diff --git a/dcs_code_injector/code_editor.py b/dcs_code_injector/code_editor.py new file mode 100644 index 0000000..1f86c11 --- /dev/null +++ b/dcs_code_injector/code_editor.py @@ -0,0 +1,221 @@ +from .lua_syntax_highlighter import SimpleLuaHighlighter +from PySide6.QtWidgets import * +from PySide6.QtGui import * +from PySide6.QtCore import * +from PySide6.QtCore import Qt + +class CodeTextEdit(QPlainTextEdit): + def __init__(self): + """ + Constructor for the CodeTextEdit class. + Initializes the text edit and sets up the syntax highlighter. + """ + + super().__init__() + + self.font_size = 10 + self.line_numbers_padding = 5 + self.highlight_color = QColor(29, 233, 182) + self.update_document_size() + + self.line_number_area = LineNumberArea(self) + + self.blockCountChanged.connect(self.update_line_number_area_width) + self.updateRequest.connect(self.update_line_number_area) + self.cursorPositionChanged.connect(self.highlight_current_line) + + self.update_line_number_area_width() + + SimpleLuaHighlighter(self.document()) + + def get_line_number_area_width(self): + """ + Returns the space needed for the line number area based number of lines + :return: int + """ + digits = 1 + max_value = max(1, self.blockCount()) + while max_value >= 10: + max_value /= 10 + digits += 1 + + space = 3 + self.fontMetrics().horizontalAdvance('9') * digits + self.line_numbers_padding + return space + + def update_line_number_area_width(self): + """ + Update the viewport margins based on the space needed by the line number widget + + :return: + """ + self.setViewportMargins(self.get_line_number_area_width(), 0, 0, 0) + + def update_line_number_area(self, rect, dy): + """ + + :param rect: + :param dy: + :return: + """ + if dy: + self.line_number_area.scroll(0, dy) + else: + self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height()) + + if rect.contains(self.viewport().rect()): + self.update_line_number_area_width() + + def line_number_area_paint_event(self, event): + """ + Paint the numbers widget + + :param event: The QPaintEvent that triggered this function + :return: None + """ + # grab the painter of the line number area widget + painter = QPainter(self.line_number_area) + painter.fillRect(event.rect(), QColor(49, 54, 59)) + + block = self.firstVisibleBlock() + block_number = block.blockNumber() + + # Get the top and bottom y-coordinates of the first visible block + top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top() + bottom = top + self.blockBoundingRect(block).height() + + # Loop through all visible blocks + while block.isValid() and (top <= event.rect().bottom()): + if block.isVisible() and (bottom >= event.rect().top()): + number = str(block_number + 1) + + # If this block is the current line, set the pen color to the highlight color + if block_number == self.textCursor().blockNumber(): + painter.setPen(self.highlight_color) + else: + # Otherwise, set the pen color to a darker color + painter.setPen(self.highlight_color.darker(150)) + + # Draw the line number text at the correct position + painter.drawText(0, top, self.line_number_area.width() - self.line_numbers_padding, + self.fontMetrics().height(), + Qt.AlignRight, number) + + # Move to the next block and update the top and bottom y-coordinates + block = block.next() + top = bottom + bottom = top + self.blockBoundingRect(block).height() + block_number += 1 + + # Draw a vertical line to separate line numbers and code + painter.setPen(self.highlight_color) + painter.drawLine(self.line_number_area.width() - 1, event.rect().top(), + self.line_number_area.width() - 1, event.rect().bottom()) + + def highlight_current_line(self): + """ + Does what it says on the box + + :return: + """ + extra_selections = [] + + if not self.isReadOnly(): + selection = QTextEdit.ExtraSelection() + + lineColor = QColor(49, 54, 59).lighter(110) + + selection.format.setBackground(lineColor) + selection.format.setProperty(QTextFormat.FullWidthSelection, True) + selection.cursor = self.textCursor() + selection.cursor.clearSelection() + extra_selections.append(selection) + + self.setExtraSelections(extra_selections) + + def update_document_size(self): + """ + Updates the document size based on the font size. + """ + + self.setStyleSheet(f"font: {self.font_size}pt 'Courier New';") + + def get_selected_text(self): + """ + Returns the selected text in the text edit. + + :return: the selected text + """ + + return self.textCursor().selectedText() + + def __insert_code(self, text, move_back_pos): + """ + Inserts the given text at the current cursor position. + + :param text: the text to be inserted + :param move_back_pos: the number of positions to move the cursor back after inserting the text + """ + + cursor = self.textCursor() + selected_text = cursor.selection().toPlainText() + self.insertPlainText(text) + pos = cursor.position() + move_back_pos + cursor.setPosition(pos) + self.setTextCursor(cursor) + self.insertPlainText(selected_text) + + def keyPressEvent(self, event: QKeyEvent) -> None: + """ + Handles key press events. + + :param event: the key press event + """ + + if event.key() == Qt.Key_Slash and event.modifiers() == Qt.ControlModifier: + cursor = self.textCursor() + selected_text = cursor.selection().toPlainText() + lines = selected_text.split("\n") + commented_lines = [] + for line in lines: + if line.startswith("-- "): + line = line.replace("-- ", "") + else: + line = "-- " + line + commented_lines.append(line) + + self.insertPlainText("\n".join(commented_lines)) + if event.key() == Qt.Key_Up and event.modifiers() == Qt.ControlModifier: + self.font_size += 1 + self.update_document_size() + if event.key() == Qt.Key_Down and event.modifiers() == Qt.ControlModifier: + self.font_size -= 1 + self.update_document_size() + if event.key() == Qt.Key_P and event.modifiers() == Qt.ControlModifier: + self.__insert_code("BASE:I()", -1) + if event.key() == Qt.Key_M and event.modifiers() == Qt.ControlModifier: + self.__insert_code("MessageToAll()", -1) + if event.key() == Qt.Key_QuoteDbl: + self.__insert_code('"', -1) + if event.key() == Qt.Key_BraceLeft: + self.__insert_code("}", -1) + if event.key() == Qt.Key_BracketLeft: + self.__insert_code("]", -1) + if event.key() == Qt.Key_ParenLeft: + self.__insert_code(")", -1) + + super().keyPressEvent(event) + + def resizeEvent(self, event): + super().resizeEvent(event) + + cr = self.contentsRect() + self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.get_line_number_area_width(), cr.height())) + + +class LineNumberArea(QWidget): + def __init__(self, editor): + super().__init__(editor) + self.codeEditor = editor + + def paintEvent(self, event): + self.codeEditor.line_number_area_paint_event(event) \ No newline at end of file diff --git a/dcs_code_injector/dcs_code_injector_window.py b/dcs_code_injector/dcs_code_injector_window.py index f115192..381bd0c 100644 --- a/dcs_code_injector/dcs_code_injector_window.py +++ b/dcs_code_injector/dcs_code_injector_window.py @@ -13,10 +13,12 @@ from .settings_dialog import SettingsDialog from .server import Server -from .lua_syntax_highlighter import SimpleLuaHighlighter +from .code_editor import CodeTextEdit +from .favorites import FavoritesWidget +from .log_view import LogView from .log_highlighter import LogHighlighter from .ui.dcs_code_injector_window_ui import Ui_MainWindow -from .ui.dcs_code_injector_search_ui import Ui_Form + from .constants import sk from . import versioner @@ -406,316 +408,7 @@ def closeEvent(self, event): super().closeEvent(event) -class CodeTextEdit(QPlainTextEdit): - def __init__(self): - """ - Constructor for the CodeTextEdit class. - Initializes the text edit and sets up the syntax highlighter. - """ - - super().__init__() - - self.font_size = 10 - self.update_document_size() - SimpleLuaHighlighter(self.document()) - - def update_document_size(self): - """ - Updates the document size based on the font size. - """ - - self.setStyleSheet(f"font: {self.font_size}pt 'Courier New';") - - def get_selected_text(self): - """ - Returns the selected text in the text edit. - - :return: the selected text - """ - - return self.textCursor().selectedText() - - def __insert_code(self, text, move_back_pos): - """ - Inserts the given text at the current cursor position. - - :param text: the text to be inserted - :param move_back_pos: the number of positions to move the cursor back after inserting the text - """ - - cursor = self.textCursor() - selected_text = cursor.selection().toPlainText() - self.insertPlainText(text) - pos = cursor.position() + move_back_pos - cursor.setPosition(pos) - self.setTextCursor(cursor) - self.insertPlainText(selected_text) - - def keyPressEvent(self, event: QKeyEvent) -> None: - """ - Handles key press events. - - :param event: the key press event - """ - - if event.key() == Qt.Key_Slash and event.modifiers() == Qt.ControlModifier: - cursor = self.textCursor() - selected_text = cursor.selection().toPlainText() - lines = selected_text.split("\n") - commented_lines = [] - for line in lines: - if line.startswith("-- "): - line = line.replace("-- ", "") - else: - line = "-- " + line - commented_lines.append(line) - - self.insertPlainText("\n".join(commented_lines)) - if event.key() == Qt.Key_Up and event.modifiers() == Qt.ControlModifier: - self.font_size += 1 - self.update_document_size() - if event.key() == Qt.Key_Down and event.modifiers() == Qt.ControlModifier: - self.font_size -= 1 - self.update_document_size() - if event.key() == Qt.Key_P and event.modifiers() == Qt.ControlModifier: - self.__insert_code("BASE:I()", -1) - if event.key() == Qt.Key_M and event.modifiers() == Qt.ControlModifier: - self.__insert_code("MessageToAll()", -1) - if event.key() == Qt.Key_QuoteDbl: - self.__insert_code('"', -1) - if event.key() == Qt.Key_BraceLeft: - self.__insert_code("}", -1) - if event.key() == Qt.Key_BracketLeft: - self.__insert_code("]", -1) - if event.key() == Qt.Key_ParenLeft: - self.__insert_code(")", -1) - - super().keyPressEvent(event) - - -class FavoritesButton(QPushButton): - exec_code = Signal(str) - delete = Signal() - open_tab_with_code = Signal(str, str) - def __init__(self, label, code): - """ - Constructor for the FavoritesButton class. - - :param label: the label of the button - :param code: the code associated with the button - """ - - super().__init__() - self.label = label - self.code = code - self.setText(label) - self.setMaximumWidth(150) - self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.show_menu) - - def show_menu(self): - """ - Shows the context menu for the button. - """ - - build_menu_from_action_list( - [ - {"Delete": self.remove}, - {"Open as tab": self.open_as_tab} - ]) - - def remove(self): - """ - Emits the delete signal that will delete the button in the main window - """ - - self.delete.emit() - - def open_as_tab(self): - """ - Emits the open_tab_with_code signal with the button's label and code to open a tab in the main window - """ - - self.open_tab_with_code.emit(self.label, self.code) - - -class FavoritesWidget(QWidget): - new_button_added = Signal(FavoritesButton) - def __init__(self): - """ - Constructor for the FavoritesWidget class. - """ - - super().__init__() - self.setAcceptDrops(True) - self.lay = QHBoxLayout() - self.lay.setSpacing(2) - self.lay.setContentsMargins(0, 0, 10, 10) - self.setLayout(self.lay) - self.layout().setAlignment(Qt.AlignLeft) - self.setMaximumHeight(35) - self.setAttribute(Qt.WA_StyledBackground, True) - # self.setStyleSheet("background-color: white;") - - def add_new_button(self, label, code): - """ - Adds a new button to the widget. - - :param label: the label of the button - :param code: the code associated with the button - """ - - button = FavoritesButton(label, code) - self.layout().addWidget(button) - EZSettings().set(f"btn_{label}", code) - - self.new_button_added.emit(button) - button.delete.connect(partial(self.delete_button, button)) - - def delete_button(self, button): - """ - Deletes the given button from the widget. - - :param button: the button to delete - """ - - button.setParent(None) - # self.lay.removeWidget(button) - button.deleteLater() - EZSettings().remove(f"btn_{button.text()}") - - def dragEnterEvent(self, event): - """ - Handles the drag enter event. - - :param event: the drag enter event - """ - - event.accept() if event.mimeData().hasText() else event.ignore() - - def dragMoveEvent(self, event): - """ - Handles the drag move event. - - :param event: the drag move event - """ - - event.accept() if event.mimeData().hasText() else event.ignore() - - def dropEvent(self, event): - """ - Handles the drop event. - - :param event: the drop event - """ - - if event.mimeData().hasText(): - event.setDropAction(Qt.CopyAction) - - label, accepted = QInputDialog().getText(self.parent(), "DCS Code Injector", "Name:") - if accepted: - self.add_new_button(label, event.mimeData().text()) - event.accept() - else: - event.ignore() - - -class LogView(QPlainTextEdit): - def __init__(self): - """ - Constructor for the LogView class. - """ - - super().__init__() - self.search_widget = SearchBox() - self.search_widget.txt_search.returnPressed.connect(self.search_text) - - self.last_search_position = 0 - - self.grid_layout = QGridLayout(self) - self.grid_layout.setContentsMargins(0, 10 , 15, 0) - self.grid_layout.addWidget(self.search_widget, 0, 0, 0, 0, Qt.AlignTop | Qt.AlignRight) - self.setReadOnly(True) - - - def toggle_search(self): - """ - Toggles the visibility of the search widget. - """ - - self.search_widget.setVisible(not self.search_widget.isVisible()) - self.search_widget.txt_search.clear() - self.search_widget.txt_search.setFocus() - - def search_text(self): - """ - Searches the text in the log view based on the query in the search widget. Highlights the text when found - """ - - search_query = self.search_widget.txt_search.text() - - if self.search_widget.btn_case_sensitive.isChecked(): - cursor = self.document().find(search_query, self.last_search_position, QTextDocument.FindFlag.FindCaseSensitively) - else: - cursor = self.document().find(search_query, self.last_search_position) - - if not cursor.isNull(): - self.last_search_position = cursor.position() - self.setTextCursor(cursor) - else: - self.last_search_position = 0 - - -class SearchBox(QWidget, Ui_Form): - def __init__(self): - """ - Constructor for the SearchBox class. - """ - - super().__init__() - self.setupUi(self) - self.setVisible(False) - - def keyPressEvent(self, event: QKeyEvent) -> None: - """ - Handles key press events. - - :param event: the key press event - """ - - if event.key() == Qt.Key_Escape: - self.setVisible(False) - - -def build_menu_from_action_list(actions, menu=None, is_sub_menu=False): - """ - Builds a menu from a list of actions. - - :param actions: the list of actions - :param menu: the menu to add the actions to - :param is_sub_menu: whether the menu is a sub-menu - :return: the menu with the added actions - """ - - if not menu: - menu = QMenu() - - for action in actions: - if action == "-": - menu.addSeparator() - continue - for action_title, action_command in action.items(): - if isinstance(action_command, list): - sub_menu = menu.addMenu(action_title) - build_menu_from_action_list(action_command, menu=sub_menu, is_sub_menu=True) - continue - atn = menu.addAction(action_title) - atn.triggered.connect(action_command) - if not is_sub_menu: - cursor = QCursor() - menu.exec_(cursor.pos()) - return menu \ No newline at end of file diff --git a/dcs_code_injector/favorites.py b/dcs_code_injector/favorites.py new file mode 100644 index 0000000..d85c40e --- /dev/null +++ b/dcs_code_injector/favorites.py @@ -0,0 +1,135 @@ +from PySide6.QtWidgets import * +from PySide6.QtGui import * +from PySide6.QtCore import * +from PySide6.QtCore import Qt + +from ez_settings import EZSettings +from functools import partial + +from .utils import build_menu_from_action_list + +class FavoritesButton(QPushButton): + exec_code = Signal(str) + delete = Signal() + open_tab_with_code = Signal(str, str) + def __init__(self, label, code): + """ + Constructor for the FavoritesButton class. + + :param label: the label of the button + :param code: the code associated with the button + """ + + super().__init__() + self.label = label + self.code = code + self.setText(label) + self.setMaximumWidth(150) + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_menu) + + def show_menu(self): + """ + Shows the context menu for the button. + """ + + build_menu_from_action_list( + [ + {"Delete": self.remove}, + {"Open as tab": self.open_as_tab} + ]) + + def remove(self): + """ + Emits the delete signal that will delete the button in the main window + """ + + self.delete.emit() + + def open_as_tab(self): + """ + Emits the open_tab_with_code signal with the button's label and code to open a tab in the main window + """ + + self.open_tab_with_code.emit(self.label, self.code) + + +class FavoritesWidget(QWidget): + new_button_added = Signal(FavoritesButton) + def __init__(self): + """ + Constructor for the FavoritesWidget class. + """ + + super().__init__() + self.setAcceptDrops(True) + self.lay = QHBoxLayout() + self.lay.setSpacing(2) + self.lay.setContentsMargins(0, 0, 10, 10) + self.setLayout(self.lay) + self.layout().setAlignment(Qt.AlignLeft) + self.setMaximumHeight(35) + self.setAttribute(Qt.WA_StyledBackground, True) + # self.setStyleSheet("background-color: white;") + + def add_new_button(self, label, code): + """ + Adds a new button to the widget. + + :param label: the label of the button + :param code: the code associated with the button + """ + + button = FavoritesButton(label, code) + self.layout().addWidget(button) + EZSettings().set(f"btn_{label}", code) + + self.new_button_added.emit(button) + button.delete.connect(partial(self.delete_button, button)) + + def delete_button(self, button): + """ + Deletes the given button from the widget. + + :param button: the button to delete + """ + + button.setParent(None) + # self.lay.removeWidget(button) + button.deleteLater() + EZSettings().remove(f"btn_{button.text()}") + + def dragEnterEvent(self, event): + """ + Handles the drag enter event. + + :param event: the drag enter event + """ + + event.accept() if event.mimeData().hasText() else event.ignore() + + def dragMoveEvent(self, event): + """ + Handles the drag move event. + + :param event: the drag move event + """ + + event.accept() if event.mimeData().hasText() else event.ignore() + + def dropEvent(self, event): + """ + Handles the drop event. + + :param event: the drop event + """ + + if event.mimeData().hasText(): + event.setDropAction(Qt.CopyAction) + + label, accepted = QInputDialog().getText(self.parent(), "DCS Code Injector", "Name:") + if accepted: + self.add_new_button(label, event.mimeData().text()) + event.accept() + else: + event.ignore() \ No newline at end of file diff --git a/dcs_code_injector/log_view.py b/dcs_code_injector/log_view.py new file mode 100644 index 0000000..55cb16e --- /dev/null +++ b/dcs_code_injector/log_view.py @@ -0,0 +1,72 @@ +from PySide6.QtWidgets import * +from PySide6.QtGui import * +from PySide6.QtCore import * +from PySide6.QtCore import Qt + +from .ui.dcs_code_injector_search_ui import Ui_Form + +class LogView(QPlainTextEdit): + def __init__(self): + """ + Constructor for the LogView class. + """ + + super().__init__() + self.search_widget = SearchBox() + self.search_widget.txt_search.returnPressed.connect(self.search_text) + + self.last_search_position = 0 + + self.grid_layout = QGridLayout(self) + self.grid_layout.setContentsMargins(0, 10 , 15, 0) + self.grid_layout.addWidget(self.search_widget, 0, 0, 0, 0, Qt.AlignTop | Qt.AlignRight) + self.setReadOnly(True) + + + def toggle_search(self): + """ + Toggles the visibility of the search widget. + """ + + self.search_widget.setVisible(not self.search_widget.isVisible()) + self.search_widget.txt_search.clear() + self.search_widget.txt_search.setFocus() + + def search_text(self): + """ + Searches the text in the log view based on the query in the search widget. Highlights the text when found + """ + + search_query = self.search_widget.txt_search.text() + + if self.search_widget.btn_case_sensitive.isChecked(): + cursor = self.document().find(search_query, self.last_search_position, QTextDocument.FindFlag.FindCaseSensitively) + else: + cursor = self.document().find(search_query, self.last_search_position) + + if not cursor.isNull(): + self.last_search_position = cursor.position() + self.setTextCursor(cursor) + else: + self.last_search_position = 0 + + +class SearchBox(QWidget, Ui_Form): + def __init__(self): + """ + Constructor for the SearchBox class. + """ + + super().__init__() + self.setupUi(self) + self.setVisible(False) + + def keyPressEvent(self, event: QKeyEvent) -> None: + """ + Handles key press events. + + :param event: the key press event + """ + + if event.key() == Qt.Key_Escape: + self.setVisible(False) \ No newline at end of file diff --git a/dcs_code_injector/utils.py b/dcs_code_injector/utils.py index 827a0f6..c0b0a89 100644 --- a/dcs_code_injector/utils.py +++ b/dcs_code_injector/utils.py @@ -1,8 +1,44 @@ import re +from PySide6.QtWidgets import QMenu +from PySide6.QtGui import QCursor + def check_regex(s): try: re.compile(s) except re.error: return False - return True \ No newline at end of file + return True + +def build_menu_from_action_list(actions, menu=None, is_sub_menu=False): + """ + Builds a menu from a list of actions. + + :param actions: the list of actions + :param menu: the menu to add the actions to + :param is_sub_menu: whether the menu is a sub-menu + :return: the menu with the added actions + """ + + if not menu: + menu = QMenu() + + for action in actions: + if action == "-": + menu.addSeparator() + continue + + for action_title, action_command in action.items(): + if isinstance(action_command, list): + sub_menu = menu.addMenu(action_title) + build_menu_from_action_list(action_command, menu=sub_menu, is_sub_menu=True) + continue + + atn = menu.addAction(action_title) + atn.triggered.connect(action_command) + + if not is_sub_menu: + cursor = QCursor() + menu.exec_(cursor.pos()) + + return menu \ No newline at end of file diff --git a/setup.py b/setup.py index e81b271..0f070df 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='dcs-code-injector', - version='1.2.1', + version='1.2.3', packages=find_packages(), package_data={ "": data_files_to_include,