From f7b83faf8c826a1b65b26bf062ddbd82673bc01c Mon Sep 17 00:00:00 2001 From: mstoeck3 Date: Tue, 23 Dec 2025 15:11:16 +0100 Subject: [PATCH] database schema & basic GUI implementation --- src/factum/services/db_service.py | 66 ++ src/factum/ui/main_window.py | 401 ++++++++- src/factum/ui/stylesheet.py | 1251 +++++++++++++++++++++++++++-- 3 files changed, 1651 insertions(+), 67 deletions(-) diff --git a/src/factum/services/db_service.py b/src/factum/services/db_service.py index de349aa..093367e 100644 --- a/src/factum/services/db_service.py +++ b/src/factum/services/db_service.py @@ -12,6 +12,7 @@ class DBService: def __init__(self): logging.debug("Initializing DBService") self.conn = self.connect_db() + self.initialize_schema() def connect_db(self): logging.debug(f"Trying to connect to database at {DB_FILE}") @@ -24,4 +25,69 @@ class DBService: return conn except sqlite3.Error as e: logging.error(f"Database connection failed: {e}") + raise + + def initialize_schema(self): + try: + with self.conn: + cursor = self.conn.cursor() + cursor.executescript(""" + CREATE TABLE IF NOT EXISTS cases( + id INTEGER PRIMARY KEY, + case_file TEXT, + case_name_pretty TEXT, + case_description TEXT, + creation_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS evidence( + id INTEGER PRIMARY KEY, + parent_case_id INTEGER, + evid_descr TEXT, + evidence_hash TEXT, + creation_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(parent_case_id) REFERENCES cases(id) + ); + CREATE TABLE IF NOT EXISTS notes( + id INTEGER PRIMARY KEY, + note_text TEXT, + creation_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + note_hash TEXT, + note_hash_sig TEXT, + parent_evidence_id INTEGER, + parent_case_id INTEGER, + FOREIGN KEY(parent_evidence_id) REFERENCES evidence(id), + FOREIGN KEY(parent_case_id) REFERENCES cases(id) + ); + CREATE TABLE IF NOT EXISTS ioc( + id INTEGER PRIMARY KEY, + ioc_type TEXT CHECK(ioc_type IN ('ip', 'domain', 'url', 'hash')), + ioc_value TEXT + ); + CREATE TABLE IF NOT EXISTS note_ioc_map( + note_id INTEGER, + ioc_id INTEGER, + PRIMARY KEY(note_id, ioc_id), + FOREIGN KEY(note_id) REFERENCES notes(id), + FOREIGN KEY(ioc_id) REFERENCES ioc(id) + ); + CREATE TABLE IF NOT EXISTS tags( + id INTEGER PRIMARY KEY, + tag_text TEXT + ); + CREATE TABLE IF NOT EXISTS note_tag_map( + note_id INTEGER, + tag_id INTEGER, + PRIMARY KEY(note_id, tag_id), + FOREIGN KEY(note_id) REFERENCES notes(id), + FOREIGN KEY(tag_id) REFERENCES tags(id) + ); + CREATE TABLE IF NOT EXISTS settings( + id INTEGER PRIMARY KEY, + setting_key TEXT UNIQUE, + setting_value TEXT + ); + """) + logging.info("Database schema initialized.") + except sqlite3.Error as e: + logging.error(f"Failed to initialize database schema: {e}") raise \ No newline at end of file diff --git a/src/factum/ui/main_window.py b/src/factum/ui/main_window.py index c612fc9..fe0413a 100644 --- a/src/factum/ui/main_window.py +++ b/src/factum/ui/main_window.py @@ -1,7 +1,7 @@ -from PySide6.QtWidgets import QMainWindow, QApplication, QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit, QMenu, QHBoxLayout +from PySide6.QtWidgets import QMainWindow, QToolBar, QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit, QMenu, QHBoxLayout, QFrame, QSizePolicy, QTextEdit, QSplitter, QScrollArea, QWidgetAction from PySide6.QtCore import Qt, Signal from factum import config -import factum.ui.stylesheet as ss +from factum.ui.stylesheet import get_stylesheet class FactumWindow(QMainWindow): def __init__(self, db_service): @@ -14,32 +14,391 @@ class FactumWindow(QMainWindow): def create_ui(self): self.setWindowTitle("Factum") self.setGeometry(100, 100, 800, 600) - self.setStyleSheet(ss.main_window_style) - + # load ai-generated stylesheet + # we use setObjectName throughout to reference the stylesheet + self.setStyleSheet(get_stylesheet()) + central_widget = QWidget() self.setCentralWidget(central_widget) - self.main_layout = QHBoxLayout() - self.case_tree_layout = QVBoxLayout() - self.note_area_layout = QVBoxLayout() - self.info_widget_layout = QVBoxLayout() - - self.main_layout.addLayout(self.case_tree_layout) - self.main_layout.addLayout(self.note_area_layout) - self.main_layout.addLayout(self.info_widget_layout) - - central_widget.setLayout(self.main_layout) - + # Create menu bar with branding toolbar self.create_menu_bar() + + self.main_layout = QHBoxLayout() + # remove any margings and spacing to create a cohesive ui + self.set_zero_spacing(self.main_layout) + + # add the left sidebar which has a QFrame as border + left_sidebar = QFrame() + left_sidebar.setObjectName("case_sidebar") + left_sidebar.setProperty("class", "sidebar") + left_sidebar.setLayout(self.create_casetree_layout()) + self.main_layout.addWidget(left_sidebar, 0) + + + # add the center area which has a QFrame as border + center_area = QFrame() + center_area.setObjectName("note_area") + center_area.setProperty("class", "centerArea") + center_area.setLayout(self.create_central_area_layout()) + self.main_layout.addWidget(center_area, 1) + + # add the right sidebar which has a QFrame as border + right_sidebar = QFrame() + right_sidebar.setObjectName("right_sidebar") + right_sidebar.setProperty("class", "sidebar") + right_sidebar.setLayout(self.create_info_widget_layout()) + self.main_layout.addWidget(right_sidebar, 0) + + central_widget.setLayout(self.main_layout) + self.create_bottom_toolbar() + + def create_casetree_layout(self): + tree_layout = QVBoxLayout() + self.set_zero_spacing(tree_layout) + + # Header (might change) + header_toolbar = QToolBar() + header_toolbar.setMovable(False) + header_toolbar.setFloatable(False) + header_toolbar.setContextMenuPolicy(Qt.ContextMenuPolicy.PreventContextMenu) + header_toolbar.setObjectName("sidebarHeader") + header_label = QLabel(" CASE STRUCTURE") + header_label.setObjectName("sidebarHeaderTitle") + header_toolbar.addWidget(header_label) + tree_layout.addWidget(header_toolbar) + + # tree content area + tree_content = QLabel("Case Tree Placeholder") + tree_layout.addWidget(tree_content, 1) + + # button area with margings + button_frame = QFrame() + button_frame.setObjectName("sidebarButtonArea") + button_frame.setContentsMargins(10, 10, 10, 10) + button_layout = QVBoxLayout() + self.set_zero_spacing(button_layout) + + # and space between buttons + button_layout.setSpacing(8) + new_case_button = QPushButton("+ New Case") + new_case_button.setProperty("buttonStyle", "primary") + delete_case_button = QPushButton("- Delete Case") + delete_case_button.setProperty("buttonStyle", "danger") + button_layout.addWidget(new_case_button) + button_layout.addWidget(delete_case_button) + button_frame.setLayout(button_layout) + tree_layout.addWidget(button_frame) + + return tree_layout + + def create_central_area_layout(self): + note_area_layout = QVBoxLayout() + self.set_zero_spacing(note_area_layout) + + # filter toolbar setup + filter_toolbar = QToolBar() + filter_toolbar.setMovable(False) + filter_toolbar.setFloatable(False) + filter_toolbar.setContextMenuPolicy(Qt.ContextMenuPolicy.PreventContextMenu) + filter_toolbar.setObjectName("filterBar") + filter_label = QLabel(" Filter by tag:") + filter_toolbar.addWidget(filter_label) + note_area_layout.addWidget(filter_toolbar) + + # the area where notes are displayed (placeholder) + note_display = QLabel("Notes display area") + note_area_layout.addWidget(note_display, 1) + + # note input area with frame + note_input_frame = QFrame() + note_input_frame.setObjectName("noteInputArea") + note_input_layout = QVBoxLayout() + # text input is multiline + note_input_text = QTextEdit() + note_input_text.setObjectName("note_input") + note_input_text.setPlaceholderText("Notes will be time-stamped. Add #tags if needed.") + note_input_text.setMaximumHeight(96) + note_input_layout.addWidget(note_input_text) + + # submit button toolbar with status text + bottom_row = QHBoxLayout() + bottom_row.setSpacing(8) + bottom_row.setContentsMargins(0,16,0,4) + status_label = QLabel("IoCs will be auto-extracted | GPG signing enabled") #TODO real status from current settings + status_label.setObjectName("noteInputStatus") + add_button = QPushButton("Add Note") + add_button.setProperty("buttonStyle", "primary") + bottom_row.addWidget(status_label) + bottom_row.addStretch() + bottom_row.addWidget(add_button) + note_input_layout.addLayout(bottom_row) + note_input_frame.setLayout(note_input_layout) + note_area_layout.addWidget(note_input_frame) + + return note_area_layout + + def create_info_widget_layout(self): + info_widget_layout = QVBoxLayout() + self.set_zero_spacing(info_widget_layout) + + # the resize_separator enables the drag-resize + self.info_splitter = QSplitter(Qt.Orientation.Vertical) + self.info_splitter.setObjectName("infoSplitter") + self.info_splitter.setChildrenCollapsible(False) # this keeps the sections from disappearing + + # the investigative goals section can be collapsed + goals_widget = self.create_collapsible_section("Investigative Goals", self.create_goals_content()) + self.info_splitter.addWidget(goals_widget) + + # pinned notes are always visible + pinned_widget = self.create_section("Pinned Notes", self.create_pinned_content()) + self.info_splitter.addWidget(pinned_widget) + + # extracted iocs can be collapsed + iocs_widget = self.create_collapsible_section("Extracted IoCs", self.create_iocs_content()) + self.info_splitter.addWidget(iocs_widget) + + # Set initial sizes (in pixels) for each section + self.info_splitter.setSizes([150, 150, 150]) + + # TODO: add a section for the ai assistant if we choose to implement it + + info_widget_layout.addWidget(self.info_splitter, 1) + + return info_widget_layout + + def create_collapsible_section(self, title, content_widget): + section_frame = QFrame() + section_frame.setObjectName("infoSection") + section_layout = QVBoxLayout() + self.set_zero_spacing(section_layout) + + # header has toggle button + header = QFrame() + header.setObjectName("infoSectionHeader") + header_layout = QHBoxLayout() + header_layout.setContentsMargins(8, 4, 8, 4) + + toggle_button = QPushButton("▼") + toggle_button.setObjectName("toggleButton") + toggle_button.setFixedWidth(24) + toggle_button.setProperty("collapsed", False) + # store the id references (python memory addresses) to retrieve later + toggle_button.setProperty("content_widget", id(content_widget)) + toggle_button.setProperty("section_frame", id(section_frame)) + + title_label = QLabel(title) + title_label.setObjectName("infoSectionTitle") + + header_layout.addWidget(toggle_button) + header_layout.addWidget(title_label) + header_layout.addStretch() + header.setLayout(header_layout) + + # Store widget references in instance dict and connect toggle function + if not hasattr(self, 'toggle_widgets'): + self.toggle_widgets = {} + if not hasattr(self, 'section_frames'): + self.section_frames = {} + self.toggle_widgets[id(content_widget)] = content_widget + self.section_frames[id(section_frame)] = section_frame + toggle_button.clicked.connect(self.toggle_section) + + section_layout.addWidget(header) + section_layout.addWidget(content_widget, 1) + section_frame.setLayout(section_layout) + + return section_frame + + def create_section(self, title, content_widget): + section_frame = QFrame() + section_frame.setObjectName("infoSection") + section_layout = QVBoxLayout() + self.set_zero_spacing(section_layout) + + header = QFrame() + header.setObjectName("infoSectionHeader") + header_layout = QHBoxLayout() + header_layout.setContentsMargins(8, 4, 8, 4) + + spacer = QWidget() + spacer.setFixedWidth(24) + # this height needs to be set to align with the toggle button height + spacer.setFixedHeight(20) + + title_label = QLabel(title) + title_label.setObjectName("infoSectionTitle") + + header_layout.addWidget(spacer) + header_layout.addWidget(title_label) + header_layout.addStretch() + header.setLayout(header_layout) + + section_layout.addWidget(header) + section_layout.addWidget(content_widget, 1) + section_frame.setLayout(section_layout) + + return section_frame + + def create_goals_content(self): + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setObjectName("infoContent") + + content = QLabel() + content.setObjectName("goalsContent") + content.setText("will be implemented later") #TODO fetch from case data + + scroll_area.setWidget(content) + return scroll_area + + def create_pinned_content(self): + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setObjectName("infoContent") + + content = QLabel("No pinned notes") #TODO fetch from database, notes with flag + content.setObjectName("pinnedContent") + content.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft) + content.setWordWrap(True) + content.setContentsMargins(8, 8, 8, 8) + + scroll_area.setWidget(content) + return scroll_area + + def create_iocs_content(self): + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setObjectName("infoContent") + + content = QLabel("No IoCs extracted yet") #TODO fetch from database + content.setObjectName("iocsContent") + content.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft) + content.setWordWrap(True) + content.setContentsMargins(8, 8, 8, 8) + + scroll_area.setWidget(content) + return scroll_area + + def toggle_section(self): + # if the function is called, sender() returns the button which was clicked + button = self.sender() + # this is necessary to satisfy type checker + if not isinstance(button, QPushButton): + return + + widget_id = button.property("content_widget") + section_frame_id = button.property("section_frame") + # these were stored previously in create_collapsible_section + content_widget = self.toggle_widgets.get(widget_id) + section_frame = self.section_frames.get(section_frame_id) + + # this check is necessary to satisfy type checker + if content_widget and section_frame: + is_visible = content_widget.isVisible() + # invert visibility + new_visibility = not is_visible + # set inverted visibility + content_widget.setVisible(new_visibility) + button.setText("▼" if new_visibility else "▶") + button.setProperty("collapsed", not new_visibility) + + # this is proposed by AI to resize the section properly - seems functional + if new_visibility: # Expanding + section_frame.setMinimumHeight(100) + section_frame.setMaximumHeight(16777215) # QWIDGETSIZE_MAX + section_frame.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding) + else: # Collapsing + header_height = 32 + section_frame.setMinimumHeight(header_height) + section_frame.setMaximumHeight(header_height) + section_frame.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + def connect_ui_elements(self): pass def create_menu_bar(self): - menu_bar = self.menuBar() - menu_bar.setStyleSheet(ss.toolbar_style) + # the menu is actually a toolbar with nested menus + menu_toolbar = QToolBar("Menu Bar", self) + menu_toolbar.setMovable(False) + menu_toolbar.setFloatable(False) + # by default there is an unnecessary context menu, we have to explicitly prevent it + menu_toolbar.setContextMenuPolicy(Qt.ContextMenuPolicy.PreventContextMenu) + menu_toolbar.setObjectName("menuToolbar") - settings_menu = QMenu("&Settings", self) - menu_bar.addMenu(settings_menu) + brand_label = QLabel(" FACTUM ") + brand_label.setObjectName("appBrand") + menu_toolbar.addWidget(brand_label) - about_menu = QMenu("&About", self) - menu_bar.addMenu(about_menu) \ No newline at end of file + file_menu = QMenu("File", self) + file_button = QPushButton("File") + file_button.setObjectName("menuButton") + file_button.setMenu(file_menu) + menu_toolbar.addWidget(file_button) + + view_menu = QMenu("View", self) + view_button = QPushButton("View") + view_button.setObjectName("menuButton") + view_button.setMenu(view_menu) + menu_toolbar.addWidget(view_button) + + settings_menu = QMenu("Settings", self) + settings_button = QPushButton("Settings") + settings_button.setObjectName("menuButton") + settings_button.setMenu(settings_menu) + menu_toolbar.addWidget(settings_button) + + help_menu = QMenu("Help", self) + help_button = QPushButton("Help") + help_button.setObjectName("menuButton") + help_button.setMenu(help_menu) + menu_toolbar.addWidget(help_button) + + self.addToolBar(Qt.ToolBarArea.TopToolBarArea, menu_toolbar) + + def create_bottom_toolbar(self): + bottom_toolbar = QToolBar("Bottom Toolbar", self) + bottom_toolbar.setMovable(False) + bottom_toolbar.setFloatable(False) + # by default there is an unnecessary context menu, we have to explicitly prevent it + bottom_toolbar.setContextMenuPolicy(Qt.ContextMenuPolicy.PreventContextMenu) + + viewing_label = QLabel(" Viewing: ") + viewing_label.setProperty("statusLabel", "true") + viewing_value = QLabel("No case selected") + viewing_value.setProperty("statusValue", "true") + + bottom_toolbar.addWidget(viewing_label) + bottom_toolbar.addWidget(viewing_value) + bottom_toolbar.addSeparator() + + cli_label = QLabel("CLI Active: ") + cli_label.setProperty("statusLabel", "true") + cli_value = QLabel("None") + cli_value.setProperty("statusValue", "true") + + bottom_toolbar.addWidget(cli_label) + bottom_toolbar.addWidget(cli_value) + + # this adds a spacer to push the gpg status to the right + spacer = QWidget() + spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) + bottom_toolbar.addWidget(spacer) + + # TODO: real GPG status from settings + gpg_label = QLabel("GPG Key: ") + gpg_label.setProperty("statusLabel", "true") + gpg_value = QLabel("Not configured") + gpg_value.setProperty("statusValue", "true") + + bottom_toolbar.addWidget(gpg_label) + bottom_toolbar.addWidget(gpg_value) + + bottom_toolbar.setContentsMargins(4, 0, 4, 0) + + self.addToolBar(Qt.ToolBarArea.BottomToolBarArea, bottom_toolbar) + + def set_zero_spacing(self, layout): + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) \ No newline at end of file diff --git a/src/factum/ui/stylesheet.py b/src/factum/ui/stylesheet.py index 41414ba..58f8a75 100644 --- a/src/factum/ui/stylesheet.py +++ b/src/factum/ui/stylesheet.py @@ -1,65 +1,1224 @@ -main_window_style = """ +""" +Factum-Notes Stylesheet +Comprehensive QSS styling for the forensic logging application +Color scheme: Amber/Stone with forensic-grade aesthetics +DISCLAIMER: This stylesheet was completely AI-generated in order to save time for development of the business logic. +""" + +def get_stylesheet(): + """ + Returns the complete QSS stylesheet for Factum-Notes application. + Implements the color scheme and design from the GUI mockup. + """ + return """ +/* ============================================ + COLOR PALETTE + ============================================ */ +/* + Amber Tones (Primary): + - amber-900: #78350f (dark brown) + - amber-800: #92400e + - amber-700: #b45309 + - amber-100: #fef3c7 + - amber-50: #fffbeb + + Stone Tones (Neutral): + - stone-800: #292524 + - stone-700: #44403c + - stone-600: #57534e + - stone-500: #78716c + - stone-400: #a8a29e + - stone-300: #d6d3d1 + - stone-200: #e7e5e4 + - stone-100: #f5f5f4 + - stone-50: #fafaf9 + + Accent Colors: + - green-600: #16a34a (active indicators) + - blue-800: #1e40af (tags) + - red-700: #b91c1c (delete actions) +*/ + +/* ============================================ + GLOBAL STYLES + ============================================ */ QWidget { - background-color: #f0f0f0; - font-family: Arial, sans-serif; - font-size: 14px; - color: #333; + font-family: "Courier New", "Consolas", monospace; + font-size: 13px; + color: #292524; } -""" -button_style = """ -QPushButton { - padding: 8px 16px; +QMainWindow { + background-color: #fafaf9; +} + +/* ============================================ + MENU BAR / TOOLBAR + ============================================ */ +#menuToolbar { + background-color: #78350f; + border-bottom: 1px solid #92400e; + padding: 4px 8px; + spacing: 8px; +} + +#appBrand { + color: #fffbeb; + font-weight: bold; font-size: 16px; - border-radius: 12px; - background-color: #808080; - color: white; - border: 1px solid #6e6e6e; + background: transparent; + padding-right: 8px; } -QPushButton:hover { - background-color: #6e6e6e; -} -QPushButton:pressed { - background-color: #5e5e5e; -} -""" -input_style = """ -QLineEdit { - border: 1px solid #ddd; - border-radius: 24px; - padding: 10px 20px; - font-size: 16px; -} -QLineEdit:focus { - border: 1px solid #aaa; - outline: none; -} -""" - -toolbar_style = """ -QMenuBar::item { +QPushButton#menuButton { + color: #fef3c7; + background: transparent; + border: none; padding: 6px 12px; + font-size: 13px; +} + +QPushButton#menuButton:hover { + color: #fffbeb; + background-color: #92400e; border-radius: 4px; } -QMenuBar::item:selected { - background-color: #e8e8e8; + +QPushButton#menuButton:pressed { + background-color: #b45309; } + +QPushButton#menuButton::menu-indicator { + image: none; +} + QMenu { - border: 2px solid #c0c0c0; - border-radius: 6px; - padding: 6px 4px; + background-color: #fafaf9; + border: 1px solid #d6d3d1; + padding: 4px; } + QMenu::item { - padding: 8px 24px 8px 12px; + padding: 8px 32px 8px 16px; + color: #292524; +} + +QMenu::item:selected { + background-color: #fef3c7; + color: #78350f; +} + +/* ============================================ + STATUS BAR + ============================================ */ +QStatusBar { + background-color: #78350f; + border-top: 1px solid #92400e; + color: #fef3c7; + padding: 8px 16px; + font-size: 11px; +} + +QStatusBar QLabel { + color: #fef3c7; + background: transparent; +} + +/* ============================================ + SPLITTER + ============================================ */ +QSplitter::handle { + background-color: #d6d3d1; +} + +QSplitter::handle:horizontal { + width: 1px; +} + +QSplitter::handle:vertical { + height: 1px; +} + +QSplitter::handle:hover { + background-color: #b45309; +} + +/* ============================================ + LEFT SIDEBAR - CASE TREE + ============================================ */ +#case_sidebar { + background-color: #f5f5f4; + border-right: 1px solid #d6d3d1; +} + +#case_tree_header { + background-color: #e7e5e4; + border-bottom: 1px solid #d6d3d1; + padding: 12px; +} + +#case_tree_header QLabel { + color: #57534e; + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; +} + +QTreeWidget { + background-color: #f5f5f4; + border: none; + outline: none; + padding: 8px; +} + +QTreeWidget::item { + padding: 8px; + border-radius: 4px; + margin: 2px 0; +} + +QTreeWidget::item:hover { + background-color: #e7e5e4; +} + +QTreeWidget::item:selected { + background-color: #fef3c7; + color: #78350f; + border: 1px solid #f59e0b; +} + +QTreeWidget::item:selected:active { + background-color: #fef3c7; +} + +QTreeWidget::branch { + background-color: #f5f5f4; +} + +QTreeWidget::branch:has-children:closed { + image: url(:/icons/chevron-right.png); +} + +QTreeWidget::branch:has-children:open { + image: url(:/icons/chevron-down.png); +} + +/* Active indicator dot */ +#active_indicator { + min-width: 8px; + min-height: 8px; + max-width: 8px; + max-height: 8px; + border-radius: 4px; + background-color: #a8a29e; +} + +#active_indicator:hover { + background-color: #10b981; +} + +#active_indicator[active="true"] { + background-color: #16a34a; +} + +/* ============================================ + BUTTONS + ============================================ */ +QPushButton { + padding: 8px 16px; + border-radius: 4px; + font-size: 13px; + border: none; +} + +/* Primary Button (Add Note, etc.) */ +QPushButton#primary_button, +QPushButton[buttonStyle="primary"] { + background-color: #b45309; + color: #fffbeb; +} + +QPushButton#primary_button:hover, +QPushButton[buttonStyle="primary"]:hover { + background-color: #92400e; +} + +QPushButton#primary_button:pressed, +QPushButton[buttonStyle="primary"]:pressed { + background-color: #78350f; +} + +QPushButton#primary_button:disabled, +QPushButton[buttonStyle="primary"]:disabled { + background-color: #e7e5e4; + color: #a8a29e; +} + +/* New Case Button */ +QPushButton#new_case_button { + background-color: #b45309; + color: #fffbeb; +} + +QPushButton#new_case_button:hover { + background-color: #92400e; +} + +/* Delete Button */ +QPushButton#delete_button, +QPushButton[buttonStyle="danger"] { + background-color: #fee2e2; + color: #b91c1c; + border: 1px solid #fca5a5; +} + +QPushButton#delete_button:hover, +QPushButton[buttonStyle="danger"]:hover { + background-color: #fecaca; +} + +QPushButton#delete_button:pressed, +QPushButton[buttonStyle="danger"]:pressed { + background-color: #fca5a5; +} + +/* Secondary Button */ +QPushButton#secondary_button, +QPushButton[buttonStyle="secondary"] { + background-color: #e7e5e4; + color: #292524; + border: 1px solid #d6d3d1; +} + +QPushButton#secondary_button:hover, +QPushButton[buttonStyle="secondary"]:hover { + background-color: #d6d3d1; +} + +/* AI Assistant Toggle */ +QPushButton#ai_toggle { + padding: 4px 12px; + font-size: 11px; +} + +QPushButton#ai_toggle[active="false"] { + background-color: #d6d3d1; + color: #57534e; +} + +QPushButton#ai_toggle[active="true"] { + background-color: #d1fae5; + color: #065f46; + border: 1px solid #6ee7b7; +} + +/* Action Buttons in AI Panel */ +QPushButton#ai_action_button { + background-color: #dbeafe; + color: #1e40af; + border: 1px solid #93c5fd; + text-align: left; + padding: 8px 12px; +} + +QPushButton#ai_action_button:hover { + background-color: #bfdbfe; +} + +/* Icon Buttons (Pin, Delete in notes) */ +QPushButton#icon_button { + background: transparent; + border: none; + padding: 4px; + min-width: 24px; + max-width: 24px; + min-height: 24px; + max-height: 24px; +} + +QPushButton#icon_button:hover { + background-color: #e7e5e4; + border-radius: 4px; +} + +/* ============================================ + INPUT FIELDS + ============================================ */ +QLineEdit, QTextEdit, QPlainTextEdit { + background-color: white; + border: 1px solid #d6d3d1; + border-radius: 4px; + padding: 8px 12px; + color: #292524; + selection-background-color: #fef3c7; + selection-color: #78350f; +} + +QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus { + border: 1px solid #b45309; + outline: none; +} + +QLineEdit::placeholder, QTextEdit::placeholder, QPlainTextEdit::placeholder { + color: #78716c; +} + +/* Note Input Area */ +#note_input { + background-color: white; + border: 1px solid #d6d3d1; + border-radius: 4px; + padding: 12px; + font-size: 13px; + min-height: 96px; +} + +#note_input:focus { + border: 1px solid #b45309; +} + +/* ============================================ + FILTER BAR + ============================================ */ +#filter_bar { + background-color: #f5f5f4; + border-bottom: 1px solid #d6d3d1; + padding: 8px 16px; +} + +#filter_bar QLabel { + color: #57534e; + font-size: 11px; +} + +QComboBox { + background-color: white; + border: 1px solid #d6d3d1; + border-radius: 4px; + padding: 4px 12px; + min-width: 150px; +} + +QComboBox:hover { + border: 1px solid #a8a29e; +} + +QComboBox:focus { + border: 1px solid #b45309; +} + +QComboBox::drop-down { + border: none; + width: 20px; +} + +QComboBox::down-arrow { + image: url(:/icons/chevron-down.png); + width: 12px; + height: 12px; +} + +QComboBox QAbstractItemView { + background-color: white; + border: 1px solid #d6d3d1; + selection-background-color: #fef3c7; + selection-color: #78350f; + outline: none; +} + +/* ============================================ + CHECKBOX + ============================================ */ +QCheckBox { + spacing: 8px; + color: #44403c; +} + +QCheckBox::indicator { + width: 16px; + height: 16px; + border-radius: 3px; + border: 1px solid #d6d3d1; + background-color: white; +} + +QCheckBox::indicator:hover { + border: 1px solid #b45309; +} + +QCheckBox::indicator:checked { + background-color: #b45309; + border: 1px solid #b45309; + image: url(:/icons/check.png); +} + +/* ============================================ + NOTES DISPLAY AREA + ============================================ */ +#notes_container { + background-color: #fafaf9; + padding: 16px; +} + +/* Individual Note Card */ +#note_card { + background-color: white; + border: 1px solid #d6d3d1; + border-radius: 8px; + padding: 16px; + margin-bottom: 12px; +} + +#note_card:hover { + border: 1px solid #f59e0b; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +#note_timestamp { + color: #92400e; + font-weight: bold; + font-size: 13px; +} + +#note_content { + color: #44403c; + font-size: 13px; + line-height: 1.6; + padding: 8px 0; +} + +/* Evidence Badge */ +#evidence_badge { + background-color: #fef3c7; + border: 1px solid #f59e0b; + border-radius: 4px; + padding: 4px 8px; + color: #44403c; + font-size: 11px; + font-weight: bold; +} + +/* Tag Label */ +#tag_label, +QLabel[tagStyle="true"] { + background-color: #dbeafe; + color: #1e40af; + border: 1px solid #93c5fd; + border-radius: 4px; + padding: 4px 8px; + font-size: 11px; +} + +#tag_label:hover, +QLabel[tagStyle="true"]:hover { + background-color: #bfdbfe; + cursor: pointer; +} + +/* IoC Highlight */ +#ioc_highlight, +QLabel[iocStyle="true"] { + background-color: #fef3c7; + color: #78350f; + border: 1px solid #f59e0b; + border-radius: 3px; + padding: 2px 4px; + font-family: "Courier New", monospace; + font-size: 12px; +} + +/* Signature Section */ +#signature_section { + border-top: 1px solid #e7e5e4; + padding-top: 8px; + margin-top: 8px; +} + +#signature_text { + color: #57534e; + font-size: 11px; +} + +/* ============================================ + RIGHT SIDEBAR PANELS + ============================================ */ +#right_sidebar { + background-color: #f5f5f4; + border-left: 1px solid #d6d3d1; +} + +/* Info Section Container */ +#infoSection { + background-color: #f5f5f4; +} + +/* Info Section Headers */ +#infoSectionHeader { + background-color: #e7e5e4; + border-bottom: 1px solid #d6d3d1; +} + +#infoSectionTitle { + color: #44403c; + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Toggle Button for Collapsible Sections */ +#toggleButton { + background-color: transparent; + border: none; + color: #78350f; + font-size: 14px; + padding: 2px; + min-width: 20px; + max-width: 20px; + min-height: 20px; + max-height: 20px; + text-align: center; +} + +#toggleButton:hover { + background-color: #d6d3d1; + border-radius: 3px; +} + +#toggleButton:pressed { + background-color: #c4c0bd; +} + +/* Info Section Content */ +#infoContent { + background-color: #f5f5f4; + border: none; +} + +/* Panel Headers */ +#panel_header { + background-color: #e7e5e4; + border-bottom: 1px solid #d6d3d1; + padding: 12px; +} + +#panel_header QLabel { + color: #44403c; + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; +} + +#panel_header QPushButton { + background: transparent; + border: none; + padding: 4px; +} + +#panel_header QPushButton:hover { + background-color: #d6d3d1; + border-radius: 4px; +} + +/* Panel Content */ +#panel_content { + background-color: #f5f5f4; + padding: 12px; +} + +/* Investigative Goals */ +#goals_list { + background-color: transparent; + border: none; +} + +#goals_list QLabel { + color: #44403c; + font-size: 11px; + line-height: 1.5; +} + +/* Pinned Note Card */ +#pinned_note { + background-color: #d1fae5; + border: 1px solid #6ee7b7; + border-radius: 4px; + padding: 12px; + margin-bottom: 8px; +} + +#pinned_note_timestamp { + color: #065f46; + font-size: 10px; +} + +#pinned_note_content { + color: #292524; + font-size: 11px; + line-height: 1.4; +} + +/* IoC Tree */ +#ioc_tree { + background-color: transparent; + border: none; + font-family: "Courier New", monospace; + font-size: 11px; +} + +#ioc_tree::item { + padding: 4px 8px; + border-radius: 4px; +} + +#ioc_tree::item:hover { + background-color: #fffbeb; + cursor: pointer; +} + +#ioc_type_label { + color: #92400e; + font-weight: bold; + font-size: 11px; +} + +#ioc_value { + color: #44403c; + font-family: "Courier New", monospace; + font-size: 11px; +} + +/* ============================================ + AI ASSISTANT PANEL + ============================================ */ +#ai_panel { + background-color: #f5f5f4; + border-top: 1px solid #d6d3d1; +} + +#ai_header { + background-color: #e7e5e4; + padding: 12px; + border-bottom: 1px solid #d6d3d1; +} + +#ai_conversation { + background-color: white; + border: 1px solid #d6d3d1; + border-radius: 4px; + padding: 12px; + font-size: 11px; +} + +#ai_message_user { + color: #1e40af; + margin-bottom: 8px; +} + +#ai_message_assistant { + color: #44403c; + margin-bottom: 8px; +} + +#ai_input { + background-color: white; + border: 1px solid #d6d3d1; + border-radius: 4px; + padding: 6px 12px; + font-size: 11px; +} + +#ai_input:focus { + border: 1px solid #b45309; +} + +/* ============================================ + SCROLLBAR + ============================================ */ +QScrollBar:vertical { + background-color: #f5f5f4; + width: 12px; + margin: 0; +} + +QScrollBar::handle:vertical { + background-color: #d6d3d1; + min-height: 30px; + border-radius: 6px; + margin: 2px; +} + +QScrollBar::handle:vertical:hover { + background-color: #a8a29e; +} + +QScrollBar::add-line:vertical, +QScrollBar::sub-line:vertical { + height: 0; + background: none; +} + +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background: none; +} + +QScrollBar:horizontal { + background-color: #f5f5f4; + height: 12px; + margin: 0; +} + +QScrollBar::handle:horizontal { + background-color: #d6d3d1; + min-width: 30px; + border-radius: 6px; + margin: 2px; +} + +QScrollBar::handle:horizontal:hover { + background-color: #a8a29e; +} + +QScrollBar::add-line:horizontal, +QScrollBar::sub-line:horizontal { + width: 0; + background: none; +} + +QScrollBar::add-page:horizontal, +QScrollBar::sub-page:horizontal { + background: none; +} + +/* ============================================ + TOOLTIPS + ============================================ */ +QToolTip { + background-color: #292524; + color: #fffbeb; + border: 1px solid #44403c; + padding: 6px 10px; + border-radius: 4px; + font-size: 11px; +} + +/* ============================================ + PROGRESS BAR + ============================================ */ +QProgressBar { + border: 1px solid #d6d3d1; + border-radius: 4px; + text-align: center; + background-color: #f5f5f4; + height: 20px; +} + +QProgressBar::chunk { + background-color: #b45309; + border-radius: 3px; +} + +/* ============================================ + TAB WIDGET (if used) + ============================================ */ +QTabWidget::pane { + border: 1px solid #d6d3d1; + background-color: white; +} + +QTabBar::tab { + background-color: #e7e5e4; + color: #44403c; + padding: 8px 16px; + margin-right: 2px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +QTabBar::tab:selected { + background-color: white; + color: #78350f; + border-bottom: 2px solid #b45309; +} + +QTabBar::tab:hover { + background-color: #d6d3d1; +} + +/* ============================================ + GROUP BOX + ============================================ */ +QGroupBox { + border: 1px solid #d6d3d1; + border-radius: 6px; + margin-top: 12px; + padding-top: 12px; + font-weight: bold; + color: #44403c; +} + +QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top left; + left: 10px; + padding: 0 5px; + background-color: #fafaf9; +} + +/* ============================================ + DIALOGS + ============================================ */ +QDialog { + background-color: #fafaf9; +} + +QDialogButtonBox { + button-layout: 0; +} + +/* ============================================ + LIST WIDGET + ============================================ */ +QListWidget { + background-color: white; + border: 1px solid #d6d3d1; + border-radius: 4px; + outline: none; +} + +QListWidget::item { + padding: 8px; border-radius: 4px; margin: 2px; } -QMenu::item:selected { - background-color: #e8e8e8; + +QListWidget::item:hover { + background-color: #f5f5f4; } -""" -status_widget_style = """ +QListWidget::item:selected { + background-color: #fef3c7; + color: #78350f; + border: 1px solid #f59e0b; +} -""" \ No newline at end of file +/* ============================================ + TABLE WIDGET (if used for IoCs) + ============================================ */ +QTableWidget { + background-color: white; + border: 1px solid #d6d3d1; + gridline-color: #e7e5e4; + font-size: 11px; +} + +QTableWidget::item { + padding: 6px; +} + +QTableWidget::item:selected { + background-color: #fef3c7; + color: #78350f; +} + +QHeaderView::section { + background-color: #e7e5e4; + color: #44403c; + padding: 8px; + border: none; + border-right: 1px solid #d6d3d1; + border-bottom: 1px solid #d6d3d1; + font-weight: bold; + text-transform: uppercase; + font-size: 10px; +} + +/* ============================================ + RESIZE HANDLE + ============================================ */ +#resize_handle { + background-color: #d6d3d1; + min-height: 4px; + max-height: 4px; +} + +#resize_handle:hover { + background-color: #b45309; + cursor: ns-resize; +} + +/* ============================================ + SIGNATURE VERIFIED INDICATOR + ============================================ */ +#signature_verified { + color: #16a34a; +} + +#signature_failed { + color: #dc2626; +} + +/* ============================================ + NESTED NOTE INDICATOR + ============================================ */ +#nested_note_tree { + color: #78716c; + font-family: "Courier New", monospace; + font-size: 12px; +} + +/* ============================================ + CUSTOM ANIMATIONS (apply via property) + ============================================ */ +*[highlighted="true"] { + background-color: #fef3c7; + animation: highlight-fade 2s; +} + +/* ============================================ + DISABLED STATES + ============================================ */ +QWidget:disabled { + color: #a8a29e; +} + +QLineEdit:disabled, +QTextEdit:disabled, +QPlainTextEdit:disabled { + background-color: #f5f5f4; + color: #a8a29e; +} + +/* ============================================ + CONTEXT MENU + ============================================ */ +QMenu::separator { + height: 1px; + background-color: #e7e5e4; + margin: 4px 0; +} + +QMenu::indicator { + width: 16px; + height: 16px; +} + +QMenu::indicator:checked { + image: url(:/icons/check.png); +} + +/* ============================================ + SPIN BOX + ============================================ */ +QSpinBox, QDoubleSpinBox { + background-color: white; + border: 1px solid #d6d3d1; + border-radius: 4px; + padding: 4px 8px; +} + +QSpinBox:focus, QDoubleSpinBox:focus { + border: 1px solid #b45309; +} + +QSpinBox::up-button, QDoubleSpinBox::up-button, +QSpinBox::down-button, QDoubleSpinBox::down-button { + background-color: #e7e5e4; + border: none; + width: 16px; +} + +QSpinBox::up-button:hover, QDoubleSpinBox::up-button:hover, +QSpinBox::down-button:hover, QDoubleSpinBox::down-button:hover { + background-color: #d6d3d1; + } + +/* ============================================ + SIDEBAR FRAMES AND SEPARATORS + ============================================ */ +#sidebarHeader { + background-color: #e7e5e4; + border-bottom: 1px solid #d6d3d1; + border: none; + padding: 4px 8px; + margin: 0px; +} + +#sidebarHeaderTitle { + color: #57534e; + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; +} + +#sidebarButtonArea { + background-color: #f5f5f4; + border-top: 1px solid #d6d3d1; + padding: 0px; + margin: 0px; +} + +/* Separator lines */ +#separator { + background-color: #d6d3d1; + border: none; + height: 1px; + margin: 0px; + padding: 0px; +} + +#filterBar { + background-color: #e7e5e4; + border-bottom: 1px solid #d6d3d1; + border: none; + padding: 4px 8px; + margin: 0px; +} + +#filterBar QLabel { + color: #57534e; + font-size: 11px; +} + +#case_sidebar { + background-color: #f5f5f4; + border-right: 1px solid #d6d3d1; + padding: 0px; + margin: 0px; +} + +#right_sidebar { + background-color: #f5f5f4; + border-left: 1px solid #d6d3d1; + padding: 0px; + margin: 0px; +} + +#note_area { + background-color: #fafaf9; + border: none; + padding: 0px; + margin: 0px; +} + +/* Note Input Area */ +#noteInputArea { + background-color: #f5f5f4; + border-top: 1px solid #d6d3d1; + padding: 0px; + margin: 0px; +} + +#note_input { + background-color: white; + border: 1px solid #d6d3d1; + border-radius: 4px; + padding: 8px 12px; + font-size: 13px; + color: #292524; +} + +#note_input:focus { + border: 1px solid #b45309; + outline: none; +} + +#noteInputStatus { + color: #57534e; + font-size: 11px; +} + +/* ============================================ + BOTTOM TOOLBAR / STATUS BAR + ============================================ */ +QToolBar { + background-color: #78350f; + border-top: 1px solid #92400e; + padding: 2px 4px; + spacing: 4px; + border: none; +} + +QToolBar QWidget { + margin: 0px; + padding: 0px; +} + +QToolBar::separator { + background-color: #92400e; + width: 1px; + margin: 0 12px; +} + +QToolBar QLabel { + color: #fef3c7; + font-size: 11px; +} + +QToolBar QLabel[statusLabel="true"] { + color: #fef3c7; +} + +QToolBar QLabel[statusValue="true"] { + color: #fffbeb; + font-weight: bold; +} + """ +def get_color_palette(): + """ + Returns a dictionary of color values for programmatic access. + Use this when you need to set colors dynamically in Python code. + """ + return { + # Amber tones (Primary) + 'amber_900': '#78350f', + 'amber_800': '#92400e', + 'amber_700': '#b45309', + 'amber_100': '#fef3c7', + 'amber_50': '#fffbeb', + + # Stone tones (Neutral) + 'stone_800': '#292524', + 'stone_700': '#44403c', + 'stone_600': '#57534e', + 'stone_500': '#78716c', + 'stone_400': '#a8a29e', + 'stone_300': '#d6d3d1', + 'stone_200': '#e7e5e4', + 'stone_100': '#f5f5f4', + 'stone_50': '#fafaf9', + + # Accent colors + 'green_600': '#16a34a', + 'green_50': '#d1fae5', + 'blue_800': '#1e40af', + 'blue_50': '#dbeafe', + 'red_700': '#b91c1c', + 'red_50': '#fee2e2', + + # Semantic colors + 'success': '#16a34a', + 'warning': '#f59e0b', + 'danger': '#dc2626', + 'info': '#3b82f6', + } + + +def get_font_config(): + """ + Returns font configuration dictionary. + """ + return { + 'primary_font': 'Courier New', + 'fallback_fonts': ['Consolas', 'Monaco', 'monospace'], + 'sizes': { + 'header': 16, + 'normal': 13, + 'small': 11, + 'tiny': 10, + } + } \ No newline at end of file