From fcd285ced38f9879a39f58dde3a96a86ef1a9748 Mon Sep 17 00:00:00 2001 From: overcuriousity Date: Wed, 3 Sep 2025 13:53:05 +0200 Subject: [PATCH] minor fixes --- logline_leviathan/__main__.py | 17 +- .../database/database_manager.py | 10 +- .../database/database_operations.py | 2 +- logline_leviathan/database/query copy.py | 816 ------------------ logline_leviathan/gui/versionvars.py | 4 +- run.py | 4 +- 6 files changed, 26 insertions(+), 827 deletions(-) delete mode 100644 logline_leviathan/database/query copy.py diff --git a/logline_leviathan/__main__.py b/logline_leviathan/__main__.py index f07c620..d3c5b21 100644 --- a/logline_leviathan/__main__.py +++ b/logline_leviathan/__main__.py @@ -2,27 +2,40 @@ You're welcome! I'm glad you like the name "Logline Leviathan". It's a fitting name for a program that can delve into the depths of unstructured text data like a leviathan, extracting valuable insights from the chaotic ocean of information. I hope your program is successful in its mission to help investigators navigate the dark, digital realm of cyberpunk.""" import sys +import os from PyQt5.QtWidgets import QApplication from pathlib import Path import argparse +from logline_leviathan.gui.mainwindow import MainWindow +from logline_leviathan.database.database_manager import create_database + # Add the parent directory of 'logline_leviathan' to sys.path parent_dir = str(Path(__file__).resolve().parent.parent) if parent_dir not in sys.path: sys.path.append(parent_dir) -from logline_leviathan.gui.mainwindow import MainWindow -from logline_leviathan.database.database_manager import create_database + def initialize_database(): create_database() +def ensure_directories(): + required_dirs = [ + 'data/parser', + 'data/wordlist', + 'output/entities_export/log' + ] + for dir_path in required_dirs: + os.makedirs(dir_path, exist_ok=True) def main(): parser = argparse.ArgumentParser(description='Analyze Export') parser.add_argument('directory', nargs='?', default='', help='Directory to analyze') args = parser.parse_args() + ensure_directories() + app = QApplication(sys.argv) main_window = MainWindow(app, initialize_database, args.directory) # Pass the function as an argument main_window.show() diff --git a/logline_leviathan/database/database_manager.py b/logline_leviathan/database/database_manager.py index 9a3a22c..001ac41 100644 --- a/logline_leviathan/database/database_manager.py +++ b/logline_leviathan/database/database_manager.py @@ -66,20 +66,20 @@ class EntityTypesTable(Base): def create_database(db_path='sqlite:///entities.db'): engine = create_engine(db_path) - logging.debug(f"Create Database Engine") + logging.debug("Create Database Engine") Base.metadata.create_all(engine) - logging.debug(f"Created all Metadata") + logging.debug("Created all Metadata") engine.dispose() - logging.debug(f"Disposed Engine") + logging.debug("Disposed Engine") # Start a new session session = SessionFactory() - logging.debug(f"Started new session with session factory") + logging.debug("Started new session with session factory") # Check if EntityTypesTable is empty if not session.query(EntityTypesTable).first(): # Populate EntityTypesTable from the YAML file - logging.debug(f"Didnt find the EntityTypesTable, running populate_entity_types_table") + logging.debug("Didnt find the EntityTypesTable, running populate_entity_types_table") #populate_entity_types_table(session) session.close() diff --git a/logline_leviathan/database/database_operations.py b/logline_leviathan/database/database_operations.py index dc5be76..bb0875a 100644 --- a/logline_leviathan/database/database_operations.py +++ b/logline_leviathan/database/database_operations.py @@ -4,7 +4,7 @@ import yaml from PyQt5.QtWidgets import QDialog, QVBoxLayout, QMessageBox, QLabel, QRadioButton, QPushButton from logline_leviathan.gui.ui_helper import UIHelper -from logline_leviathan.database.database_manager import * +from logline_leviathan.database.database_manager import session_scope, EntityTypesTable, DistinctEntitiesTable, EntitiesTable, ContextTable diff --git a/logline_leviathan/database/query copy.py b/logline_leviathan/database/query copy.py deleted file mode 100644 index 9b59db9..0000000 --- a/logline_leviathan/database/query copy.py +++ /dev/null @@ -1,816 +0,0 @@ -from sqlalchemy import or_, and_, not_, String -from PyQt5.QtWidgets import QProgressBar, QMainWindow, QTableWidget, QTableWidgetItem, QLineEdit, QStyledItemDelegate, QTextEdit, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QStyle, QLabel -from logline_leviathan.database.database_manager import get_db_session, EntitiesTable, DistinctEntitiesTable, EntityTypesTable, ContextTable, FileMetadata, session_scope -from PyQt5.QtCore import pyqtSignal, Qt, QThread, pyqtSignal, QTimer -from PyQt5.QtGui import QTextDocument, QTextOption -from fuzzywuzzy import fuzz -import re -import logging -import html - - -class QueryThread(QThread): - queryCompleted = pyqtSignal(list, list) # Signal to indicate completion - - def __init__(self, db_query_instance, query_text): - super(QueryThread, self).__init__() - self.db_query_instance = db_query_instance - self.query_text = query_text - - def run(self): - base_query, search_terms = self.db_query_instance.prepare_query(self.query_text) - query_lambda = self.db_query_instance.parse_query(self.query_text) - - # Pass the lambda function directly to filter - results = base_query.filter(query_lambda).all() - - # Calculate scored results - scored_results = [(result, self.db_query_instance.calculate_match_score(result, self.query_text)) for result in results] - self.queryCompleted.emit(scored_results, search_terms) - - -class DatabaseGUIQuery: - def __init__(self): - self.db_session = get_db_session() - self.entity_types = EntityTypesTable - self.entities = EntitiesTable - self.distinct_entities = DistinctEntitiesTable - self.context = ContextTable - self.file_metadata = FileMetadata - - def parse_query(self, query): - if not query.strip(): - return lambda _: False - - # Split and strip special characters for database query - tokens = re.findall(r'"[^"]+"|\S+', query) - stripped_tokens = [token.strip('+-"') for token in tokens] - - filters = [] - for token in stripped_tokens: - search_condition = f'%{token.replace("*", "%")}%' - - condition = or_( - self.distinct_entities.distinct_entity.like(search_condition), - self.entity_types.entity_type.like(search_condition), - self.entity_types.gui_name.like(search_condition), - self.entity_types.gui_tooltip.like(search_condition), - self.entity_types.script_parser.like(search_condition), - self.file_metadata.file_name.like(search_condition), - self.file_metadata.file_path.like(search_condition), - self.file_metadata.file_mimetype.like(search_condition), - self.entities.line_number.cast(String).like(search_condition), - self.context.context_large.like(search_condition) - # Add other fields as needed - ) - filters.append(condition) - - return lambda: or_(*filters) - - def parse_search_terms(self, query): - tokens = query.split() - search_terms = [token.lstrip('+-') for token in tokens if not token.startswith('-') and not token.startswith('+')] - return search_terms - - def prepare_query(self, query): - search_terms = self.parse_search_terms(query) - - # Construct the base query with proper joins - base_query = self.db_session.query( - self.distinct_entities.distinct_entity, - self.entity_types.gui_name, - self.file_metadata.file_name, - self.entities.line_number, - self.entities.entry_timestamp, - self.context.context_large - ).join( - self.entities, self.distinct_entities.distinct_entities_id == self.entities.distinct_entities_id - ).join( - self.file_metadata, self.entities.file_id == self.file_metadata.file_id - ).join( - self.context, self.entities.entities_id == self.context.entities_id - ).join( - self.entity_types, self.entities.entity_types_id == self.entity_types.entity_type_id - ).distinct() - - # Apply filters and return results - return base_query, search_terms - - - def display_results(self, results, search_terms): - self.results_window = ResultsWindow(results, search_terms) - self.results_window.show() - - def calculate_match_score(self, result, query): - # Adjusted weights and thresholds - distinct_entity_weight = 4 - file_name_weight = 4 - timestamp_weight = 1 - line_number_weight = 1 - context_weight = 5 - multiple_term_weight = 1 - order_weight = 8 # Increased weight for exact order of terms - fuzzy_match_weight = 0.3 # More discerning fuzzy match - threshold_for_fuzzy = 90 # Higher threshold for fuzzy matches - proximity_weight = 2 # Increased weight for proximity - - positive_operand_weight = 10 # Weight for terms with '+' - negative_operand_penalty = -5 # Penalty for terms with '-' - exact_match_weight = 10 # Increased weight for exact sequence match - - score = 0 - - # Extracting operands and terms - tokens = re.findall(r'"[^"]+"|\S+', query) - processed_terms = [(token.startswith('+'), token.startswith('-'), token.strip('+-"').lower()) for token in tokens] - - # Normalize result fields - lower_distinct_entity = result.distinct_entity.lower() - lower_file_name = result.file_name.lower() - timestamp_str = str(result.entry_timestamp).lower() - line_number_str = str(result.line_number).lower() - words_in_context = result.context_large.lower().split() - - # Check matches in various fields with operand consideration - for is_positive, is_negative, term in processed_terms: - if term in lower_distinct_entity: - score += positive_operand_weight if is_positive else (negative_operand_penalty if is_negative else distinct_entity_weight) - if term in lower_file_name: - score += positive_operand_weight if is_positive else (negative_operand_penalty if is_negative else file_name_weight) - if term in timestamp_str: - score += positive_operand_weight if is_positive else (negative_operand_penalty if is_negative else timestamp_weight) - if term in line_number_str: - score += positive_operand_weight if is_positive else (negative_operand_penalty if is_negative else line_number_weight) - if term in words_in_context: - score += positive_operand_weight if is_positive else (negative_operand_penalty if is_negative else context_weight) - - # Creating a cleaned substring of search terms in the exact order they appear in the query - exact_terms_substring = ' '.join([token.strip('+-"').lower() for token in tokens]) - - # Check for exact order of terms in the context - if exact_terms_substring and exact_terms_substring in ' '.join(words_in_context): - score += exact_match_weight - - # Check for exact order of terms - if '"' in query: - exact_query = ' '.join(term for _, _, term in processed_terms) - if exact_query in ' '.join(words_in_context): - score += order_weight - - # Additional weight for multiple different terms - unique_terms = set(term for _, _, term in processed_terms) - score += len(unique_terms) * multiple_term_weight - - # Proximity score calculation - for _, _, term in processed_terms: - if term in words_in_context: - # Find the positions of the term and the entity in the context - term_pos = words_in_context.index(term) - entity_pos = words_in_context.index(lower_distinct_entity) if lower_distinct_entity in words_in_context else 0 - - # Calculate the distance and adjust the score - distance = abs(term_pos - entity_pos) - proximity_score = max(0, proximity_weight - distance * 0.01) # Reduce score based on distance - score += proximity_score - - # Fuzzy matching - all_text = f"{result.distinct_entity} {result.file_name} {result.entry_timestamp} {result.line_number} {result.context_large}".lower() - for _, _, term in processed_terms: - fuzzy_score = max(fuzz.partial_ratio(term, word) for word in all_text.split()) - if fuzzy_score > threshold_for_fuzzy: - score += (fuzzy_score / 100) * fuzzy_match_weight - - # Normalize the score - max_possible_positive_score = ( - distinct_entity_weight + file_name_weight + - timestamp_weight + line_number_weight + - context_weight * len(processed_terms) + # Assuming each term can match in the context - order_weight + exact_match_weight + - len(processed_terms) * multiple_term_weight + # Each term contributes to multiple_term_weight - len(processed_terms) * positive_operand_weight # Each term could have a positive operand - ) - - # Considering the negative operand penalty - max_possible_negative_score = len(processed_terms) * negative_operand_penalty - - # The maximum score is the sum of the possible positive score and the absolute value of the possible negative score - max_possible_score = max_possible_positive_score + abs(max_possible_negative_score) - - # Normalizing the score to a scale of 100 - score = (score / max_possible_score) * 100 - - return score - - - - def get_entity_types(self): - with session_scope() as session: - # Query to filter entity types that have either regex_pattern or script_parser - return [entity_type.gui_name for entity_type in session.query(EntityTypesTable) - .filter(or_(EntityTypesTable.regex_pattern.isnot(None), - EntityTypesTable.script_parser.isnot(None))) - .all()] - - - - -COLUMN_WIDTHS = [200, 100, 250, 100, 120, 600, 80] # Adjust these values as needed -COLUMN_NAMES = ['Distinct Entity', 'Entity Type', 'File Name', 'Line Number', 'Timestamp', 'Context', 'Match Score'] -DEFAULT_ROW_HEIGHT = 120 -FILTER_EDIT_WIDTH = 150 - -class ResultsWindow(QMainWindow): - def __init__(self, db_query_instance, parent=None): - super(ResultsWindow, self).__init__(parent) - self.db_query_instance = db_query_instance - self.loaded_data_count = 0 - self.total_data = [] - self.current_filters = {} - self.setWindowTitle("Suchergebnis") - self.setGeometry(800, 600, 1500, 600) # Adjust size as needed - - # Create central widget and set layout - centralWidget = QWidget(self) - self.setCentralWidget(centralWidget) - mainLayout = QVBoxLayout(centralWidget) - - queryFieldLayout = QHBoxLayout() - - self.databaseQueryLineEdit = QueryLineEdit(self) - self.databaseQueryLineEdit.setPlaceholderText(" Suchbegriff eingeben...") - self.databaseQueryLineEdit.returnPressed.connect(self.execute_query_from_results_window) - self.databaseQueryLineEdit.setStyleSheet(""" - QLineEdit { - background-color: #3C4043; - color: white; - min-height: 20px; - } - """) - queryFieldLayout.addWidget(self.databaseQueryLineEdit) - # Create a progress bar for query in progress - self.queryProgressBar = QProgressBar(self) - self.queryProgressBar.setRange(0, 1) # Indeterminate mode - self.queryProgressBar.setFixedWidth(100) # Initially hidden - queryFieldLayout.addWidget(self.queryProgressBar) - executeQueryButton = QPushButton("Suche ausführen", self) - executeQueryButton.clicked.connect(self.execute_query_from_results_window) - queryFieldLayout.addWidget(executeQueryButton) - - mainLayout.addLayout(queryFieldLayout) - - # Create a horizontal layout for filter options - filterLayout = QHBoxLayout() - mainLayout.addLayout(filterLayout) - - # Add the table widget to the main layout - self.tableWidget = QTableWidget() - mainLayout.addWidget(self.tableWidget) - - # Updated stylesheet for the entire ResultsWindow - stylesheet = """ - /* Styles for QTableWidget and headers */ - QTableWidget, QHeaderView::section { - background-color: #2A2F35; - color: white; - border: 1px solid #4A4A4A; - } - - /* Style for QLineEdit */ - QLineEdit { - background-color: #3A3F44; - color: white; - border: 1px solid #4A4A4A; - } - - /* Style for QPushButton */ - QPushButton { - background-color: #4B5563; - color: white; - border-radius: 4px; - padding: 5px; - margin: 5px; - } - - QPushButton:hover { - background-color: #5C677D; - } - - QPushButton:pressed { - background-color: #2A2F35; - } - - /* Style for empty rows and other areas */ - QWidget { - background-color: #2A2F35; - color: white; - } - """ - self.setStyleSheet(stylesheet) - - - # Apply default row height after setting up the table - self.tableWidget.verticalHeader().setDefaultSectionSize(DEFAULT_ROW_HEIGHT) - - self.clearAllButton = QPushButton("Alle Filteroptionen loeschen", self) - self.clearAllButton.clicked.connect(self.clear_all_filters) - filterLayout.addWidget(self.clearAllButton) - # Adding filter options after table setup - self.entityTypeComboBox = QComboBox() - filterLayout.addWidget(self.entityTypeComboBox) - - # Initialize filterWidgets before calling setup_table - self.filterWidgets = [] - - # Create and add QLineEdit widgets to the filter layout - for i, column_name in enumerate(COLUMN_NAMES): - # Skipping the filter creation for certain columns - if column_name in ['Entity Type', 'Context']: - continue - - filter_edit = QLineEdit(self) - filter_edit.setFixedWidth(FILTER_EDIT_WIDTH) - filter_edit.setPlaceholderText(f"Filtern nach {column_name}") - filter_edit.textChanged.connect(lambda text, col=i: self.apply_filter(text, col)) - - self.filterWidgets.append(filter_edit) - filterLayout.addWidget(filter_edit) - self.dataLoadTimer = QTimer(self) - self.dataLoadTimer.timeout.connect(self.load_more_data) - - # Create and add the Dismiss button - self.dismissButton = QPushButton("Schließen", self) - self.dismissButton.clicked.connect(self.dataLoadTimer.stop) - self.dismissButton.clicked.connect(self.close) - mainLayout.addWidget(self.dismissButton) - - self.populate_entity_type_combobox() - - # Adjust column widths and filter widgets' widths - self.adjust_column_widths() - - #self.tableWidget.verticalScrollBar().valueChanged.connect(self.check_scroll) - - - def populate_entity_type_combobox(self): - entity_types = DatabaseGUIQuery().get_entity_types() - self.entityTypeComboBox.addItem("Alle verfügbaren Typen", None) # Default option - for entity_type in entity_types: - self.entityTypeComboBox.addItem(entity_type, entity_type) - self.entityTypeComboBox.currentIndexChanged.connect(self.filter_by_entity_type) - - def clear_table(self): - self.tableWidget.clear() - self.tableWidget.setRowCount(0) - self.tableWidget.setColumnCount(0) - - def adjust_column_widths(self): - for column, width in enumerate(COLUMN_WIDTHS): - self.tableWidget.setColumnWidth(column, width) - - - def execute_query_from_results_window(self): - self.dataLoadTimer.start(2000) - query_text = self.databaseQueryLineEdit.text() - if not query_text: - return - self.clear_table() - self.queryProgressBar.setRange(0, 0) - self.query_thread = QueryThread(self.db_query_instance, query_text) - self.query_thread.queryCompleted.connect(self.on_query_completed) - self.query_thread.start() - - def set_query_and_execute(self, query_text): - self.databaseQueryLineEdit.setText(query_text) - self.execute_query_from_results_window() - - - def on_query_completed(self, results, search_terms): - logging.debug(f"Query completed with {len(results)} results") # Debug statementself.queryProgressBar.setRange(0, 1) - self.total_data = results - self.search_terms = search_terms - self.loaded_data_count = 0 - self.setup_table(search_terms) - self.apply_all_filters() - - - def setup_table(self, search_terms=[]): - # Set up the table columns and headers - self.tableWidget.setColumnCount(7) - self.tableWidget.setHorizontalHeaderLabels(['Distinct Entity', 'Entity Type', 'File Name', 'Line Number', 'Timestamp', 'Context', 'Match Score']) - highlight_delegate = HighlightDelegate(self, search_terms) - self.tableWidget.setItemDelegateForColumn(0, highlight_delegate) - self.tableWidget.setItemDelegateForColumn(1, highlight_delegate) - self.tableWidget.setItemDelegateForColumn(3, highlight_delegate) - # Apply column widths - self.adjust_column_widths() - # Disable sorting when initially populating data - self.tableWidget.setSortingEnabled(False) - # Load initial subset of data - self.load_more_data() - # Enable sorting by 'Match Score' after data is populated - self.tableWidget.setSortingEnabled(True) - self.tableWidget.sortItems(6, Qt.DescendingOrder) - - def add_table_row(self, row_index, result, score): - self.tableWidget.insertRow(row_index) - # Distinct Entity with highlighting - distinct_entity_item = QTableWidgetItem(str(result[0])) - self.tableWidget.setItem(row_index, 0, distinct_entity_item) - # Entity Type - entity_type_item = QTableWidgetItem(str(result[1])) - self.tableWidget.setItem(row_index, 1, entity_type_item) - # File Name - using CellWidget - file_name_widget = CellWidget(str(result[2]), self.filterWidgets[1], self.search_terms) - self.tableWidget.setCellWidget(row_index, 2, file_name_widget) - file_name_item = QTableWidgetItem() - file_name_item.setData(Qt.UserRole, str(result[2])) - self.tableWidget.setItem(row_index, 2, file_name_item) - # Line Number - line_number_item = QTableWidgetItem(str(result[3])) - self.tableWidget.setItem(row_index, 3, line_number_item) - # Timestamp - using CellWidget - timestamp_widget = CellWidget(str(result[4]), self.filterWidgets[3], self.search_terms) - self.tableWidget.setCellWidget(row_index, 4, timestamp_widget) - timestamp_item = QTableWidgetItem() - timestamp_item.setData(Qt.UserRole, str(result[4])) - self.tableWidget.setItem(row_index, 4, timestamp_item) - # Context - using ScrollableTextWidget - scrollable_widget = ScrollableTextWidget(result[5], self.search_terms, str(result[0])) - self.tableWidget.setCellWidget(row_index, 5, scrollable_widget) - # Match Score - match_score_item = NumericTableWidgetItem("{:.4f}".format(float(score))) - self.tableWidget.setItem(row_index, 6, match_score_item) - # Apply highlight delegate if needed - highlight_delegate = HighlightDelegate(self, self.search_terms) - self.tableWidget.setItemDelegateForRow(row_index, highlight_delegate) - # Restore sorting, if it was enabled - self.tableWidget.setSortingEnabled(True) - # Check if total rows exceed 100 and remove the lowest 20% if so - if self.tableWidget.rowCount() > 500: - self.remove_lowest_scoring_rows(10) # 20% to be removed - - def load_more_data(self): - if not self.is_new_data_available(): - return # No new data available, just return - - start_index = self.loaded_data_count - chunk_size = 50 # Adjust this number based on performance - end_index = min(start_index + chunk_size, len(self.total_data)) - - # Calculate the average match score of the current items - average_score = self.calculate_average_score() - - # Sort the chunk by match score in descending order - sorted_chunk = sorted(self.total_data[start_index:end_index], key=lambda x: x[1], reverse=True) - - for row_data in sorted_chunk: - score = row_data[1] - if score > average_score: - row_index = start_index + len(sorted_chunk) # Adjust index based on the sorted chunk - if self.matches_current_filters(row_index, row_data): - self.insert_row_in_sorted_order(row_data) - - # Reapply filters after loading new data - self.apply_all_filters() - # Update loaded_data_count or other mechanism to keep track of processed data - self.update_data_tracking(end_index) - - self.tableWidget.update() # Refresh the table - - def remove_lowest_scoring_rows(self, percentage): - total_rows = self.tableWidget.rowCount() - rows_to_remove = total_rows * percentage // 100 - - # Collect scores and associated row indices - score_rows = [] - for row in range(total_rows): - score_item = self.tableWidget.item(row, 6) # Assuming column 6 is Match Score - if score_item: - score_rows.append((float(score_item.text()), row)) - - # Sort by scores (ascending) and select the lowest ones - score_rows.sort(key=lambda x: x[0]) - lowest_score_rows = score_rows[:rows_to_remove] - - # Remove rows with the lowest scores - for _, row in sorted(lowest_score_rows, key=lambda x: x[1], reverse=True): - self.tableWidget.removeRow(row) - - - def is_new_data_available(self): - return self.loaded_data_count < len(self.total_data) - - def calculate_average_score(self): - total_score = 0 - row_count = self.tableWidget.rowCount() - for row_index in range(row_count): - score_item = self.tableWidget.item(row_index, 6) # Assuming column 6 is Match Score - total_score += float(score_item.text()) if score_item else 0 - return total_score / row_count if row_count > 0 else 0 - - - def update_data_tracking(self, end_index): - # Update loaded_data_count or implement other mechanism to keep track of processed data - self.loaded_data_count = end_index - - def insert_row_in_sorted_order(self, row_data): - row_index = 0 - score = row_data[1] - # Find the correct position based on match score - while row_index < self.tableWidget.rowCount(): - current_score_item = self.tableWidget.item(row_index, 6) # Assuming column 6 is Match Score - current_score = float(current_score_item.text()) if current_score_item else 0 - if score > current_score: - break - row_index += 1 - - self.add_table_row(row_index, row_data[0], score) - - - def matches_current_filters(self, row_index, row_data): - for column, filter_text in self.current_filters.items(): - if not self.is_match(row_index, column, filter_text, row_data): - return False - return True - - def is_match(self, row_index, column, filter_text, row_data): - # Extract text from the cell or widget - widget = self.tableWidget.cellWidget(row_index, column) - if isinstance(widget, CellWidget): - # CellWidget contains a QLabel with HTML-formatted text - document = QTextDocument() - document.setHtml(widget.label.text()) - text = document.toPlainText() - elif isinstance(widget, ScrollableTextWidget): - # ScrollableTextWidget contains a QTextEdit with HTML-formatted text - text = widget.text_edit.toPlainText() - else: - # Standard QTableWidgetItem - item = self.tableWidget.item(row_index, column) - text = item.text() if item else "" - - # Compare the extracted plain text with the filter text - return filter_text.lower() in text.lower() - - - def apply_filter(self, text, column): - self.current_filters[column] = text.lower() - self.apply_all_filters() - - - def extract_row_data(self, row_index): - # Construct row_data from the table content - row_data = [] - for column in range(self.tableWidget.columnCount()): - cell_data = self.get_cell_data(row_index, column) - row_data.append(cell_data) - return row_data - - def get_cell_data(self, row_index, column): - widget = self.tableWidget.cellWidget(row_index, column) - if isinstance(widget, CellWidget): - document = QTextDocument() - document.setHtml(widget.label.text()) - return document.toPlainText() - elif isinstance(widget, ScrollableTextWidget): - return widget.text_edit.toPlainText() - else: - item = self.tableWidget.item(row_index, column) - return item.text() if item else "" - - def apply_all_filters(self): - for row_index in range(self.tableWidget.rowCount()): - row_data = self.extract_row_data(row_index) - if self.matches_current_filters(row_index, row_data): - self.tableWidget.showRow(row_index) - else: - self.tableWidget.hideRow(row_index) - - - def filter_by_entity_type(self): - selected_type = self.entityTypeComboBox.currentData() - #logging.debug(f"Filtering by entity type: {selected_type}") - - # Update the current filters dictionary - entity_type_column = COLUMN_NAMES.index('Entity Type') # Assuming 'Entity Type' is one of the column names - if selected_type is None: - # Clear the filter for entity type if 'All Entity Types' is selected - if entity_type_column in self.current_filters: - del self.current_filters[entity_type_column] - else: - # Set the filter for entity type - self.current_filters[entity_type_column] = selected_type.lower() - - # Reapply all filters including the entity type filter - self.apply_all_filters() - - - def on_filter_change(self): - # Reapply all filters - self.apply_all_filters() - - def clear_all_filters(self): - for filter_widget in self.filterWidgets: - filter_widget.clear() - - self.current_filters.clear() # Clear all filters - #logging.debug("All filters cleared") - - for row in range(self.tableWidget.rowCount()): - self.tableWidget.showRow(row) # Show all rows - - # Optionally reapply entity type filter if it should be independent - self.filter_by_entity_type() - - @staticmethod - def strip_html_tags(text): - return re.sub('<[^<]+?>', '', text) - - - - -class QueryLineEdit(QLineEdit): - returnPressed = pyqtSignal() - - def keyPressEvent(self, event): - if event.key() == Qt.Key_Return: - self.returnPressed.emit() - else: - super().keyPressEvent(event) - - -class HighlightDelegate(QStyledItemDelegate): - def __init__(self, parent=None, search_terms=None): - super().__init__(parent) - self.search_terms = search_terms or [] - - def paint(self, painter, option, index): - painter.save() - - # Set text color and other options - options = QTextOption() - options.setWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) - document = QTextDocument() - document.setDefaultTextOption(options) - document.setDefaultFont(option.font) - - # Prepare highlighted text - text = index.model().data(index) - highlighted_text = self.get_highlighted_text(text) - document.setHtml(highlighted_text) - - # Set the width of the document to the cell width - document.setTextWidth(option.rect.width()) - - # Draw the contents - painter.translate(option.rect.topLeft()) - document.drawContents(painter) - painter.restore() - - def get_highlighted_text(self, text): - if text is None: - text = "" - - text_with_color = f"{text}" - for term in self.search_terms: - # Retain the '+' at the beginning and strip other special characters - is_positive = term.startswith('+') - clean_term = re.sub(r'[^\w\s]', '', term.lstrip('+-')).lower() - - if is_positive and clean_term.lower() in text.lower(): - # Use regex for case-insensitive search and replace - regex = re.compile(re.escape(clean_term), re.IGNORECASE) - highlighted_term = f"{clean_term}" - text_with_color = regex.sub(highlighted_term, text_with_color) - - return text_with_color.replace("\n", "
") - - - -class ScrollableTextWidget(QWidget): - def __init__(self, text, search_terms, distinct_entity, parent=None): - super().__init__(parent) - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - self.text_edit = CustomTextEdit(self) - self.text_edit.setReadOnly(True) - - # Apply styles including scrollbar styles - self.text_edit.setStyleSheet(""" - QTextEdit { - background-color: #2A2F35; /* Dark blue-ish background */ - color: white; /* White text */ - } - QTextEdit QScrollBar:vertical { - border: none; - background-color: #3A3F44; /* Dark scrollbar background */ - width: 8px; /* Width of the scrollbar */ - } - QTextEdit QScrollBar::handle:vertical { - background-color: #6E6E6E; /* Scroll handle color */ - border-radius: 4px; /* Rounded corners for the handle */ - } - QTextEdit QScrollBar::add-line:vertical, QTextEdit QScrollBar::sub-line:vertical { - background: none; - } - """) - - # Set the text with highlighting - self.setHighlightedText(text, search_terms, distinct_entity) - layout.addWidget(self.text_edit) - - # Scroll to the distinct entity - self.scroll_to_text(distinct_entity) - - def setHighlightedText(self, text, search_terms, distinct_entity): - if text is None: - text = "" - - # Wrap the original text in a span to maintain color - text_with_color = f"{text}" - - # Highlight distinct entity in a different color - if distinct_entity: - distinct_entity_escaped = html.escape(distinct_entity) - text_with_color = re.sub( - re.escape(distinct_entity_escaped), - lambda match: f"{match.group()}", - text_with_color, - flags=re.IGNORECASE - ) - - - for term in search_terms: - # Check if the term starts with '+' - is_positive = term.startswith('+') - clean_term = re.sub(r'[^\w\s]', '', term.lstrip('+-')) - - # If the term starts with '+', highlight all matches regardless of case - if is_positive or clean_term.lower() in text.lower(): - regex = re.compile(re.escape(clean_term), re.IGNORECASE) - highlighted_term = f"{clean_term}" - text_with_color = regex.sub(highlighted_term, text_with_color) - - self.text_edit.setHtml(text_with_color.replace("\n", "
")) - - - - def scroll_to_text(self, text): - if text: - cursor = self.text_edit.document().find(text) - self.text_edit.setTextCursor(cursor) - -class CustomTextEdit(QTextEdit): - def __init__(self, parent=None): - super().__init__(parent) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Enable vertical scrollbar as needed - - def wheelEvent(self, event): - # Always handle the wheel event within QTextEdit - super().wheelEvent(event) - - # Stop propagation of the event to parent - if self.verticalScrollBar().isVisible(): - event.accept() - else: - event.ignore() - - -class CellWidget(QWidget): - def __init__(self, text, filter_edit, search_terms, parent=None): - super(CellWidget, self).__init__(parent) - self.layout = QHBoxLayout(self) - self.label = QLabel(text) - self.setHighlightedText(text, search_terms) - self.button = QPushButton() - icon = self.button.style().standardIcon(QStyle.SP_CommandLink) # Example of a standard icon - self.button.setIcon(icon) - self.button.setFixedSize(20, 20) # Adjust size as needed - self.button.clicked.connect(lambda: filter_edit.setText(text)) - self.layout.addWidget(self.label) - self.layout.addWidget(self.button) - self.layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.layout) - - def setHighlightedText(self, text, search_terms): - if text is None: - text = "" - - # Wrap the original text in a span to maintain color - text_with_color = f"{text}" - - for term in search_terms: - # Strip leading operands (+ or -) and special characters - clean_term = re.sub(r'[^\w\s]', '', term.lstrip('+-')) - - # Use regex for case-insensitive search and replace - regex = re.compile(re.escape(clean_term), re.IGNORECASE) - highlighted_term = f"{clean_term}" - text_with_color = regex.sub(highlighted_term, text_with_color) - - self.label.setText(text_with_color) - - -class NumericTableWidgetItem(QTableWidgetItem): - def __lt__(self, other): - return float(self.text()) < float(other.text()) diff --git a/logline_leviathan/gui/versionvars.py b/logline_leviathan/gui/versionvars.py index 8565094..61105e3 100644 --- a/logline_leviathan/gui/versionvars.py +++ b/logline_leviathan/gui/versionvars.py @@ -1,4 +1,4 @@ -repo_link = "https://cloud.mikoshi.de/call/qhtkcnmn#/" +repo_link = "https://git.cc24.dev/mstoeck3/LoglineLeviathan" repo_link_text = "Feedback // Support (öffnet externen Link)" -version_string = "2024-02-08 - Version: 0.4.4 // TESTING // UPDATE REGULARLY" +version_string = "2024-02-08 - Version: 0.5.0r // ROLLING" loglevel = "INFO" diff --git a/run.py b/run.py index 7d7a82d..fed283a 100644 --- a/run.py +++ b/run.py @@ -1,8 +1,10 @@ from logline_leviathan import main import multiprocessing +import sys if __name__ == "__main__": multiprocessing.freeze_support() - #multiprocessing.set_start_method('spawn') + if sys.platform != 'linux': + multiprocessing.set_start_method('spawn') main() \ No newline at end of file