From adec642a3517630f7eaaa12c4152e7dbdab27cf2 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 9 May 2026 21:19:55 -0500 Subject: [PATCH 01/28] STYLE: Begin work on toggleable dark mode. --- faslr/style/analysis.py | 95 +++++++++++++++++++++++++++++++++++++++++ faslr/style/triangle.py | 4 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 faslr/style/analysis.py diff --git a/faslr/style/analysis.py b/faslr/style/analysis.py new file mode 100644 index 00000000..9945a6ca --- /dev/null +++ b/faslr/style/analysis.py @@ -0,0 +1,95 @@ +from PyQt6.QtCore import Qt +from PyQt6.QtGui import ( + QColor, + QPalette +) + +def qss_column_tab( + theme: Qt.ColorScheme, + bottom_border_width, + margin_top: str, + +) -> str: + + light_unselected = "rgb(230, 230, 230)" + dark_unselected = "rgb(50, 50, 50)" + + light_selected = "rgb(245, 245, 245)" + dark_selected = "rgb(55, 55, 55)" + + if theme == Qt.ColorScheme.Light: + tab_unselected_background = light_unselected + tab_selected_background = light_selected + elif theme == Qt.ColorScheme.Dark: + tab_unselected_background = dark_unselected + tab_selected_background = dark_selected + else: + raise ValueError("Invalid theme provided.") + + qss_str = """ + QTabBar::tab:first {{ + margin-top: 22px; + border-bottom: {}px solid darkgrey; + }} + + + QTabBar::tab {{ + margin-top: {}px; + background: {}; + border: 1px solid darkgrey; + border-bottom: 1px solid darkgrey; + padding: 5px; + padding-left: 10px; + height: 125px; + margin-right: 0px; + border-right: 0px; + }} + + QTabBar::tab:selected {{ + background: {}; + + }} + + QTabWidget::pane {{ + border: 1px solid darkgrey; + }} + """.format( + bottom_border_width, + margin_top, + tab_unselected_background, + tab_selected_background, + ) + + # Further refinement - remove solid darkgrey borders if in dark mode. + if theme == Qt.ColorScheme.Dark: + qss_str = qss_str.replace(' solid darkgrey', '') + + return qss_str + + +def qss_analysis_tab_palette( + theme: Qt.ColorScheme, + palette: QPalette, + role: QPalette.ColorRole +) -> None: + + if theme == Qt.ColorScheme.Light: + palette.setColor( + role, + QColor.fromRgb( + 240, + 240, + 240 + ) + ) + elif theme == Qt.ColorScheme.Dark: + palette.setColor( + role, + QColor.fromRgb( + 42, + 42, + 42 + ) + ) + else: + raise ValueError("Incorrect theme provided.") diff --git a/faslr/style/triangle.py b/faslr/style/triangle.py index 6f8f6d1e..7544058b 100644 --- a/faslr/style/triangle.py +++ b/faslr/style/triangle.py @@ -4,7 +4,9 @@ EXCL_FACTOR_COLOR = QColor(255, 230, 230) -LOWER_DIAG_COLOR = QColor(238, 237, 238) +LOWER_DIAG_COLOR_LIGHT = QColor(238, 237, 238) + +LOWER_DIAG_COLOR_DARK = QColor(75, 75, 75) MAIN_TRIANGLE_COLOR = QColor(255, 255, 255) From c468847c48cf9689a25780349f2ec018295a33d7 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 9 May 2026 21:20:26 -0500 Subject: [PATCH 02/28] STYLE: Move AnalysisTab stylings to style subpackage. --- faslr/analysis.py | 86 +++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 51 deletions(-) diff --git a/faslr/analysis.py b/faslr/analysis.py index 9dbbb528..23ce0064 100644 --- a/faslr/analysis.py +++ b/faslr/analysis.py @@ -12,13 +12,21 @@ VALUE_TYPES_COMBO_BOX_WIDTH ) +from faslr.style.analysis import ( + qss_analysis_tab_palette, + qss_column_tab +) + from faslr.utilities.accessors import get_column from PyQt6.QtCore import ( Qt ) -from PyQt6.QtGui import QColor +from PyQt6.QtGui import ( + QColor, + QGuiApplication +) from PyQt6.QtWidgets import ( QComboBox, @@ -59,6 +67,7 @@ def __init__( self.triangle = triangle self.lob = lob + self.theme = QGuiApplication.styleHints().colorScheme() self.layout = QVBoxLayout() @@ -89,11 +98,11 @@ def __init__( # Used to solve some issues with borders not appearing when there's only 1 tab. if column_count == 1: - bottom_border_width = 1 - margin_top = "22" + self.bottom_border_width = "1" + self.margin_top = "22" else: - bottom_border_width = 0 - margin_top = "0" + self.bottom_border_width = "0" + self.margin_top = "0" # For each chainladder column, we create a horizontal tab to the left. for i in self.column_list: @@ -170,55 +179,11 @@ def __init__( self.setLayout(self.layout) - self.column_tab.setStyleSheet( - """ - QTabBar::tab:first {{ - margin-top: 22px; - border-bottom: {}px solid darkgrey; - }} - - - QTabBar::tab {{ - margin-top: {}px; - background: rgb(230, 230, 230); - border: 1px solid darkgrey; - border-bottom: 1px solid darkgrey; - padding: 5px; - padding-left: 10px; - height: 125px; - margin-right: 0px; - border-right: 0px; - }} - - QTabBar::tab:selected {{ - background: rgb(245, 245, 245); - - }} - - QTabWidget::pane {{ - border: 1px solid darkgrey; - }} - """.format( - bottom_border_width, - margin_top - ) - ) - self.setAutoFillBackground(True) - palette = self.palette() - - palette.setColor( - self.backgroundRole(), - QColor.fromRgb( - 240, - 240, - 240 - ) - ) - - self.setPalette(palette) + self.apply_theme(scheme=self.theme) self.value_box.currentTextChanged.connect(self.update_value_type) # noqa + QGuiApplication.styleHints().colorSchemeChanged.connect(self.apply_theme) def resizeEvent(self, event): @@ -262,6 +227,25 @@ def update_value_type(self): else: self.analysis_containers[tab_name].setCurrentIndex(1) + def apply_theme(self, scheme: Qt.ColorScheme): + + self.column_tab.setStyleSheet( + qss_column_tab( + theme=scheme, + bottom_border_width=self.bottom_border_width, + margin_top=self.margin_top + ) + ) + + palette = self.palette() + + qss_analysis_tab_palette( + theme=scheme, + palette=palette, + role=self.backgroundRole() + ) + + self.setPalette(palette) class MackValuationModel(FAbstractTableModel): def __init__( From 2f726c74be76c095058395e1e30b80753081c750 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 9 May 2026 21:20:42 -0500 Subject: [PATCH 03/28] STYLE: Toggle lower diagonal dark mode. --- faslr/factor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/faslr/factor.py b/faslr/factor.py index 3bc5e9ca..7cf88bd2 100644 --- a/faslr/factor.py +++ b/faslr/factor.py @@ -57,7 +57,7 @@ from faslr.style.triangle import ( BLANK_TEXT, EXCL_FACTOR_COLOR, - LOWER_DIAG_COLOR, + LOWER_DIAG_COLOR_LIGHT, MAIN_TRIANGLE_COLOR, RATIO_STYLE, VALUE_STYLE @@ -84,7 +84,7 @@ def __init__( self.heatmap_checked = False self.heatmap_frame = self.triangle.to_frame(origin_as_datetime=False).astype(str) - self.heatmap_frame.loc[:] = LOWER_DIAG_COLOR.name() + self.heatmap_frame.loc[:] = LOWER_DIAG_COLOR_LIGHT.name() self.ldf_types = TEMP_LDF_LIST self.num_ldf_types = self.ldf_types[self.ldf_types["Selected"]].shape[0] @@ -189,7 +189,7 @@ def data( # Case when the index is on the lower diagonal if (index.column() >= self.n_triangle_rows - index.row()) and \ (index.row() < self.triangle_spacer_row): - return LOWER_DIAG_COLOR + return LOWER_DIAG_COLOR_LIGHT # Case when the index is on the triangle elif index.row() < self.triangle_spacer_row: if self.heatmap_checked: @@ -202,12 +202,12 @@ def data( else: return MAIN_TRIANGLE_COLOR elif (index.row() == self.selected_spacer_row) | (index.column() > self.n_triangle_columns - 1): - return LOWER_DIAG_COLOR + return LOWER_DIAG_COLOR_LIGHT else: if index.row() < self.triangle_spacer_row - 1: return MAIN_TRIANGLE_COLOR else: - return LOWER_DIAG_COLOR + return LOWER_DIAG_COLOR_LIGHT # Strike out the link ratios if double-clicked, but not the averaged factors at the bottom if (role == Qt.ItemDataRole.FontRole) and \ From 86950adb279dafaaa04e0ec32cb46b3ad873c3bd Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 9 May 2026 21:21:02 -0500 Subject: [PATCH 04/28] STYLE: Toggle lower diagonal dark mode. --- faslr/utilities/style_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/faslr/utilities/style_parser.py b/faslr/utilities/style_parser.py index b6741b47..843e04ac 100644 --- a/faslr/utilities/style_parser.py +++ b/faslr/utilities/style_parser.py @@ -2,7 +2,7 @@ from matplotlib.colors import Colormap from pandas import DataFrame -from faslr.style.triangle import LOWER_DIAG_COLOR +from faslr.style.triangle import LOWER_DIAG_COLOR_LIGHT def extract_style( @@ -48,7 +48,7 @@ def parse_styler( # Initial colors will be the FASLR lower diagonal color for the entire triangle. # Then we override the upper triangle with the heatmap colors. - color_triangle.loc[:] = LOWER_DIAG_COLOR.name() + color_triangle.loc[:] = LOWER_DIAG_COLOR_LIGHT.name() # Parse css to get the background colors. Create a list of cell-color mappings. Each element maps # all the cells that correspond to a certain color. From e80e53729060412a02eac0e37743345f6194781a Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 9 May 2026 21:21:21 -0500 Subject: [PATCH 05/28] STYLE: Toggle lower diagonal dark mode. --- faslr/tests/test_factor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/faslr/tests/test_factor.py b/faslr/tests/test_factor.py index ec005ef3..c1ad6cdb 100644 --- a/faslr/tests/test_factor.py +++ b/faslr/tests/test_factor.py @@ -5,7 +5,7 @@ from faslr.factor import AddLDFDialog from faslr.methods.development import DevelopmentTab, LDFAverageBox from faslr.style.triangle import ( - LOWER_DIAG_COLOR, + LOWER_DIAG_COLOR_LIGHT, EXCL_FACTOR_COLOR, MAIN_TRIANGLE_COLOR ) @@ -84,7 +84,7 @@ def test_factor_data(development_tab): ) assert lower_diag_blank_value == '' - assert lower_diag_blank_back == LOWER_DIAG_COLOR + assert lower_diag_blank_back == LOWER_DIAG_COLOR_LIGHT idx = development_tab.factor_model.index(0, 0) From 21b2cbfc9b4f7bd44fa6edeeece5c4e679924273 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 9 May 2026 21:21:26 -0500 Subject: [PATCH 06/28] STYLE: Toggle lower diagonal dark mode. --- faslr/tests/test_triangle_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/faslr/tests/test_triangle_model.py b/faslr/tests/test_triangle_model.py index a9e36141..1301a391 100644 --- a/faslr/tests/test_triangle_model.py +++ b/faslr/tests/test_triangle_model.py @@ -6,7 +6,7 @@ ) from faslr.style.triangle import ( - LOWER_DIAG_COLOR + LOWER_DIAG_COLOR_LIGHT ) from faslr.triangle_model import ( @@ -155,7 +155,7 @@ def test_triangle_view( ) # Check for correct color displayed in the lower diagonal of the triangle. - assert lower_diag_color_test == LOWER_DIAG_COLOR + assert lower_diag_color_test == LOWER_DIAG_COLOR_LIGHT # rect = QRect(0, 0) triangle_view.selectRow(1) From fb0d2093b7bd36ecfd68d001efb1f8745b5b63ff Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 9 May 2026 21:21:30 -0500 Subject: [PATCH 07/28] STYLE: Toggle lower diagonal dark mode. --- faslr/triangle_model.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/faslr/triangle_model.py b/faslr/triangle_model.py index 7f6e77cf..b46523c8 100644 --- a/faslr/triangle_model.py +++ b/faslr/triangle_model.py @@ -9,7 +9,8 @@ from faslr.style.triangle import ( BLANK_TEXT, - LOWER_DIAG_COLOR, + LOWER_DIAG_COLOR_DARK, + LOWER_DIAG_COLOR_LIGHT, RATIO_STYLE, VALUE_STYLE ) @@ -25,6 +26,7 @@ from PyQt6.QtGui import ( QAction, + QGuiApplication, QKeySequence ) @@ -59,6 +61,12 @@ def __init__( self.excl_frame = self._data.copy() self.excl_frame = df_set_false(df=self.excl_frame) + theme = QGuiApplication.styleHints().colorScheme() + + self.lower_diag_color = LOWER_DIAG_COLOR_LIGHT if theme == Qt.ColorScheme.Light else LOWER_DIAG_COLOR_DARK + + QGuiApplication.styleHints().colorSchemeChanged.connect(self.on_color_scheme_changed) + def data( self, index, @@ -102,7 +110,7 @@ def data( if role == Qt.ItemDataRole.BackgroundRole and (index.column() >= self.n_rows - index.row()): - return LOWER_DIAG_COLOR + return self.lower_diag_color # if (role == Qt.ItemDataRole.FontRole) and (self.value_type == "ratio"): # @@ -129,6 +137,12 @@ def headerData( if qt_orientation == Qt.Orientation.Vertical: return str(self._data.index[p_int]) + def on_color_scheme_changed(self, scheme): + self.lower_diag_color = ( + LOWER_DIAG_COLOR_DARK if scheme == Qt.ColorScheme.Dark else LOWER_DIAG_COLOR_LIGHT + ) + self.layoutChanged.emit() # noqa + class TriangleView(FTableView): def __init__(self): From 57435e162cbcb08c9d5999f1dc8429c4dcd80b91 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 9 May 2026 21:21:51 -0500 Subject: [PATCH 08/28] STYLE: Toggle lower table corner button dark mode. --- faslr/common/table.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/faslr/common/table.py b/faslr/common/table.py index ae6277a7..4225c639 100644 --- a/faslr/common/table.py +++ b/faslr/common/table.py @@ -10,6 +10,8 @@ Qt ) +from PyQt6.QtGui import QGuiApplication + from PyQt6.QtWidgets import ( QAbstractButton, QLabel, @@ -34,13 +36,17 @@ def make_corner_button( btn.setLayout(btn_layout) opt = QStyleOptionHeader() - parent.setStyleSheet( - """ - QTableCornerButton::section { - border: 1px outset darkgrey; - } - """ - ) + def apply_style(theme: Qt.ColorScheme): + + if theme == Qt.ColorScheme.Dark: + color = "rgb(60, 60, 60)" + else: + color = "darkgrey" + + btn.setStyleSheet(f"border: 1px outset {color};") + + apply_style(QGuiApplication.styleHints().colorScheme()) + QGuiApplication.styleHints().colorSchemeChanged.connect(apply_style) # noqa s = QSize(btn.style().sizeFromContents( QStyle.ContentsType.CT_HeaderSection, opt, QSize(), btn). From 6de2399a3e5165d9b71083670ac8e2a1562286bb Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 9 May 2026 21:22:08 -0500 Subject: [PATCH 09/28] STYLE: Begin work on moving light/dark stylings out of the ProjectItem. --- faslr/project_item.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/faslr/project_item.py b/faslr/project_item.py index cdf6ef44..c99ccd79 100644 --- a/faslr/project_item.py +++ b/faslr/project_item.py @@ -1,8 +1,11 @@ from faslr.style.project import DEFAULT_PROJECT_FONT +from PyQt6.QtCore import Qt + from PyQt6.QtGui import ( QColor, QFont, + QGuiApplication, QStandardItem ) @@ -32,7 +35,7 @@ def __init__( segment_level: str, font_size: int = 12, set_bold: bool = False, - text_color: QColor = QColor(0, 0, 0) + text_color: QColor = None ): super().__init__() @@ -42,7 +45,24 @@ def __init__( ) project_font.setBold(set_bold) + # Set the text color based on theme. + if not text_color: + theme = QGuiApplication.styleHints().colorScheme() + self.text_color = QColor(0, 0, 0) if theme == Qt.ColorScheme.Light else QColor(255, 255, 255) + else: + self.text_color = text_color + self.segment_level: str = segment_level - self.setForeground(text_color) + self.text_color = self.text_color + self.setForeground(self.text_color) self.setFont(project_font) self.setText(text) + + QGuiApplication.styleHints().colorSchemeChanged.connect(self.toggle_light_dark_text) + + def toggle_light_dark_text(self, theme: Qt.ColorScheme) -> None: + color = QColor(255, 255, 255) if theme == Qt.ColorScheme.Dark else QColor(0, 0, 0) + self.setForeground(color) + if self.model(): + idx = self.index() + self.model().dataChanged.emit(idx, idx) \ No newline at end of file From d391702a0cebbcb99f6cbbbbfddab0dcd185147e Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sun, 10 May 2026 06:43:56 -0500 Subject: [PATCH 10/28] FIX: Set default qss_column_tab to light if operating with an unknown theme, such as in a headless environment. --- faslr/style/analysis.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/faslr/style/analysis.py b/faslr/style/analysis.py index 9945a6ca..bae2f3e4 100644 --- a/faslr/style/analysis.py +++ b/faslr/style/analysis.py @@ -17,14 +17,13 @@ def qss_column_tab( light_selected = "rgb(245, 245, 245)" dark_selected = "rgb(55, 55, 55)" - if theme == Qt.ColorScheme.Light: - tab_unselected_background = light_unselected - tab_selected_background = light_selected - elif theme == Qt.ColorScheme.Dark: + if theme == Qt.ColorScheme.Dark: tab_unselected_background = dark_unselected tab_selected_background = dark_selected + # Else, default to light, including headless environments like in GitHub Actions. else: - raise ValueError("Invalid theme provided.") + tab_unselected_background = light_unselected + tab_selected_background = light_selected qss_str = """ QTabBar::tab:first {{ From c9028b846cc3e068dcf5989c69175a8898efc89b Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sun, 10 May 2026 07:10:41 -0500 Subject: [PATCH 11/28] FIX: Set default qss_column_tab to light if operating with an unknown theme, such as in a headless environment. --- faslr/style/analysis.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/faslr/style/analysis.py b/faslr/style/analysis.py index bae2f3e4..e280ea62 100644 --- a/faslr/style/analysis.py +++ b/faslr/style/analysis.py @@ -72,23 +72,21 @@ def qss_analysis_tab_palette( role: QPalette.ColorRole ) -> None: - if theme == Qt.ColorScheme.Light: + if theme == Qt.ColorScheme.Dark: palette.setColor( role, QColor.fromRgb( - 240, - 240, - 240 + 42, + 42, + 42 ) ) - elif theme == Qt.ColorScheme.Dark: + else: palette.setColor( role, QColor.fromRgb( - 42, - 42, - 42 + 240, + 240, + 240 ) ) - else: - raise ValueError("Incorrect theme provided.") From a6ee7349352fb2fc89cade104c47b90da738d857 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sun, 10 May 2026 07:27:34 -0500 Subject: [PATCH 12/28] FIX: Fix diag color test to extract theme of running system. --- faslr/tests/test_triangle_model.py | 19 ++++++++++++++++++- faslr/triangle_model.py | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/faslr/tests/test_triangle_model.py b/faslr/tests/test_triangle_model.py index 1301a391..e02e3a05 100644 --- a/faslr/tests/test_triangle_model.py +++ b/faslr/tests/test_triangle_model.py @@ -6,6 +6,7 @@ ) from faslr.style.triangle import ( + LOWER_DIAG_COLOR_DARK, LOWER_DIAG_COLOR_LIGHT ) @@ -24,6 +25,10 @@ QTimer ) +from PyQt6.QtGui import ( + QGuiApplication +) + from PyQt6.QtWidgets import ( QApplication ) @@ -155,7 +160,19 @@ def test_triangle_view( ) # Check for correct color displayed in the lower diagonal of the triangle. - assert lower_diag_color_test == LOWER_DIAG_COLOR_LIGHT + + # Extract the theme. + theme = QGuiApplication.styleHints().colorScheme() + + diag_color_dict = { + Qt.ColorScheme.Dark: LOWER_DIAG_COLOR_DARK, + Qt.ColorScheme.Light: LOWER_DIAG_COLOR_LIGHT + } + + # Default to light if theme is unknown. + lower_diag_color_expectation = diag_color_dict.get(theme, LOWER_DIAG_COLOR_LIGHT) + + assert lower_diag_color_test == lower_diag_color_expectation # rect = QRect(0, 0) triangle_view.selectRow(1) diff --git a/faslr/triangle_model.py b/faslr/triangle_model.py index b46523c8..a40558e6 100644 --- a/faslr/triangle_model.py +++ b/faslr/triangle_model.py @@ -63,7 +63,7 @@ def __init__( theme = QGuiApplication.styleHints().colorScheme() - self.lower_diag_color = LOWER_DIAG_COLOR_LIGHT if theme == Qt.ColorScheme.Light else LOWER_DIAG_COLOR_DARK + self.lower_diag_color = LOWER_DIAG_COLOR_DARK if theme == Qt.ColorScheme.Dark else LOWER_DIAG_COLOR_LIGHT QGuiApplication.styleHints().colorSchemeChanged.connect(self.on_color_scheme_changed) From 96dd5cbb31f86197f83bb38e7faebb21a13b46b1 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sun, 10 May 2026 21:51:20 -0500 Subject: [PATCH 13/28] FEAT: Toggle light/dark for main window background color. --- faslr/__main__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/faslr/__main__.py b/faslr/__main__.py index 65980b16..c217f045 100644 --- a/faslr/__main__.py +++ b/faslr/__main__.py @@ -30,6 +30,8 @@ ) from faslr.style.main import ( + MAIN_WINDOW_BACKGROUND_COLOR_DARK, + MAIN_WINDOW_BACKGROUND_COLOR_LIGHT, MAIN_WINDOW_HEIGHT, MAIN_WINDOW_WIDTH, MAIN_WINDOW_TITLE @@ -41,6 +43,11 @@ QThreadPool ) +from PyQt6.QtGui import ( + QGuiApplication, + QPalette +) + from PyQt6.QtWidgets import ( QApplication, QMainWindow, @@ -80,6 +87,8 @@ def __init__( MAIN_WINDOW_HEIGHT ) + self.set_background_color(scheme=QGuiApplication.styleHints().colorScheme()) + self.setWindowTitle(MAIN_WINDOW_TITLE) self.layout = QVBoxLayout() @@ -118,6 +127,7 @@ def __init__( ) self.analysis_pane = QTabWidget() + self.analysis_pane.setTabsClosable(True) self.analysis_pane.setMovable(True) self.analysis_pane.addTab(self.auto_tab, "Auto") @@ -153,6 +163,18 @@ def __init__( main_window=self ) + QGuiApplication.styleHints().colorSchemeChanged.connect(self.set_background_color) + + def set_background_color(self, scheme: Qt.ColorScheme) -> None: + + palette = self.palette() + if scheme == Qt.ColorScheme.Dark: + color = MAIN_WINDOW_BACKGROUND_COLOR_DARK + else: + color = MAIN_WINDOW_BACKGROUND_COLOR_LIGHT + palette.setColor(self.backgroundRole(), color) + self.setPalette(palette) + def remove_tab( self, index: int From b32b59f8d370c4a58b00d5cc5de1b24db932400f Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sun, 10 May 2026 21:52:10 -0500 Subject: [PATCH 14/28] FEAT: Work on toggling the scrollbar. --- faslr/analysis.py | 2 +- faslr/style/analysis.py | 119 +++++++++++++++++++++++++++------------- faslr/style/main.py | 5 ++ 3 files changed, 86 insertions(+), 40 deletions(-) diff --git a/faslr/analysis.py b/faslr/analysis.py index 23ce0064..22c64243 100644 --- a/faslr/analysis.py +++ b/faslr/analysis.py @@ -231,7 +231,7 @@ def apply_theme(self, scheme: Qt.ColorScheme): self.column_tab.setStyleSheet( qss_column_tab( - theme=scheme, + scheme=scheme, bottom_border_width=self.bottom_border_width, margin_top=self.margin_top ) diff --git a/faslr/style/analysis.py b/faslr/style/analysis.py index e280ea62..2da3d764 100644 --- a/faslr/style/analysis.py +++ b/faslr/style/analysis.py @@ -5,11 +5,23 @@ ) def qss_column_tab( - theme: Qt.ColorScheme, - bottom_border_width, + scheme: Qt.ColorScheme, + bottom_border_width: str, margin_top: str, ) -> str: + """ + + Parameters + ---------- + scheme + bottom_border_width + margin_top + + Returns + ------- + + """ light_unselected = "rgb(230, 230, 230)" dark_unselected = "rgb(50, 50, 50)" @@ -17,50 +29,79 @@ def qss_column_tab( light_selected = "rgb(245, 245, 245)" dark_selected = "rgb(55, 55, 55)" - if theme == Qt.ColorScheme.Dark: + if scheme == Qt.ColorScheme.Dark: tab_unselected_background = dark_unselected tab_selected_background = dark_selected + scrollbar_horizontal_background = dark_selected + scrollbar_horizontal_groove_background = "rgb(42, 42, 42)" # Else, default to light, including headless environments like in GitHub Actions. else: tab_unselected_background = light_unselected tab_selected_background = light_selected + scrollbar_horizontal_background = "rgb(217, 217, 217)" + scrollbar_horizontal_groove_background = light_selected qss_str = """ - QTabBar::tab:first {{ - margin-top: 22px; - border-bottom: {}px solid darkgrey; - }} - - - QTabBar::tab {{ - margin-top: {}px; - background: {}; - border: 1px solid darkgrey; - border-bottom: 1px solid darkgrey; - padding: 5px; - padding-left: 10px; - height: 125px; - margin-right: 0px; - border-right: 0px; - }} - - QTabBar::tab:selected {{ - background: {}; - - }} - - QTabWidget::pane {{ - border: 1px solid darkgrey; - }} - """.format( - bottom_border_width, - margin_top, - tab_unselected_background, - tab_selected_background, - ) + QTabBar::tab:first {{ + margin-top: 22px; + border-bottom: {}px solid darkgrey; + }} + + + QTabBar::tab {{ + margin-top: {}px; + background: {}; + border: 1px solid darkgrey; + border-bottom: 1px solid darkgrey; + padding: 5px; + padding-left: 10px; + height: 125px; + margin-right: 0px; + border-right: 0px; + }} + + QTabBar::tab:selected {{ + background: {}; + + }} + + QTabWidget::pane {{ + border: 1px solid darkgrey; + }} + + QScrollBar:horizontal {{ + background: {}; + height: 14px; + }} + + QScrollBar::handle:horizontal {{ + background: {}; + border: none; + border-radius: 7px; + margin: 2px 0px; + min-width: 10px; + }} + + QScrollBar::sub-line:horizontal {{ + width: 0px; + }} + + QScrollBar::add-line:horizontal {{ + width: 0px; + }} + + """.format( + bottom_border_width, + margin_top, + tab_unselected_background, + tab_selected_background, + scrollbar_horizontal_groove_background, + scrollbar_horizontal_background, + scrollbar_horizontal_background + ) # Further refinement - remove solid darkgrey borders if in dark mode. - if theme == Qt.ColorScheme.Dark: + if scheme == Qt.ColorScheme.Dark: qss_str = qss_str.replace(' solid darkgrey', '') return qss_str @@ -85,8 +126,8 @@ def qss_analysis_tab_palette( palette.setColor( role, QColor.fromRgb( - 240, - 240, - 240 + 245, + 245, + 245 ) ) diff --git a/faslr/style/main.py b/faslr/style/main.py index 1ae07551..db4db24b 100644 --- a/faslr/style/main.py +++ b/faslr/style/main.py @@ -1,4 +1,9 @@ +from PyQt6.QtGui import QColor + MAIN_WINDOW_WIDTH = 2500 MAIN_WINDOW_HEIGHT = 900 MAIN_WINDOW_TITLE = "FASLR - Free Actuarial System for Loss Reserving" + +MAIN_WINDOW_BACKGROUND_COLOR_LIGHT = QColor.fromRgb(245, 245, 245) +MAIN_WINDOW_BACKGROUND_COLOR_DARK = QColor.fromRgb(42, 42, 42) From 06f5d237ed1f090723cdd36c2fccba82333a6f7c Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Thu, 14 May 2026 08:38:02 -0500 Subject: [PATCH 15/28] CHORE: Remove commented blocks. --- faslr/triangle_model.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/faslr/triangle_model.py b/faslr/triangle_model.py index a40558e6..285ae713 100644 --- a/faslr/triangle_model.py +++ b/faslr/triangle_model.py @@ -112,16 +112,6 @@ def data( return self.lower_diag_color - # if (role == Qt.ItemDataRole.FontRole) and (self.value_type == "ratio"): - # - # font = QFont() - # exclude = self.excl_frame.iloc[[index.row()], [index.column()]].squeeze() - # if exclude: - # font.setStrikeOut(True) - # else: - # font.setStrikeOut(False) - # return font - def headerData( self, p_int, @@ -164,19 +154,6 @@ def __init__(self): self.corner_button = make_corner_button(parent=self) - # Set the styling for the table corner so that it matches the rest of the headers. - # self.setStyleSheet( - # """ - # QTableCornerButton::section{ - # border-right: 1px; - # border-bottom: 1px; - # border-style: solid; - # border-color:none darkgrey darkgrey none; - # margin-right: 0px; - # } - # """ - # ) - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self.contextMenuEvent) # noqa From fbde05c1affeafcea2cc4c2292d3507125145990 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Thu, 14 May 2026 08:38:27 -0500 Subject: [PATCH 16/28] FEAT: Add dark mode to settings dialog. --- faslr/constants/settings.py | 3 ++- faslr/settings.py | 43 ++++++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/faslr/constants/settings.py b/faslr/constants/settings.py index bc7405f2..a25ccbca 100644 --- a/faslr/constants/settings.py +++ b/faslr/constants/settings.py @@ -1,5 +1,6 @@ SETTINGS_LIST = [ "Startup", "User", - "Plots" + "Plots", + "Display" ] diff --git a/faslr/settings.py b/faslr/settings.py index 6202c7ad..402ee110 100644 --- a/faslr/settings.py +++ b/faslr/settings.py @@ -23,15 +23,18 @@ ) from PyQt6.QtGui import ( - QCloseEvent + QCloseEvent, + QGuiApplication ) from PyQt6.QtWidgets import ( QButtonGroup, + QComboBox, QDialog, QDialogButtonBox, QFileDialog, QGroupBox, + QHBoxLayout, QLabel, QListView, QPushButton, @@ -72,6 +75,8 @@ def data( if role == Qt.ItemDataRole.DisplayRole: return self.setting_items[index.row()] + else: + return None def rowCount( self, @@ -133,17 +138,20 @@ def __init__( self.startup_unconnected_container = QWidget() self.user_container = QWidget() self.plot_container = QWidget() + self.display_container = QWidget() self.startup_unconnected_layout() self.startup_connected_layout() self.user_layout() self.plot_layout() + self.display_layout() for widget in [ self.startup_connected_container, self.startup_unconnected_container, self.user_container, - self.plot_container + self.plot_container, + self.display_container ]: self.configuration_layout.addWidget(widget) @@ -184,6 +192,8 @@ def update_config_layout( self.configuration_layout.setCurrentIndex(2) elif index.data() == "Plots": self.configuration_layout.setCurrentIndex(3) + elif index.data() == "Display": + self.configuration_layout.setCurrentIndex(4) def startup_unconnected_layout(self) -> None: """ @@ -253,6 +263,31 @@ def plot_layout(self) -> None: self.plot_container.setLayout(layout) + def display_layout(self) -> None: + + theme_toggle = { + "System": Qt.ColorScheme.Unknown, + "Light": Qt.ColorScheme.Light, + "Dark": Qt.ColorScheme.Dark + } + theme_layout = QHBoxLayout() + theme_label = QLabel("Theme: ") + self.theme_combobox = QComboBox() + self.theme_combobox.addItems(["System", "Light", "Dark"]) + self.theme_combobox.setCurrentText(self.config['DISPLAY']['theme']) + + theme_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + theme_layout.addWidget(theme_label) + theme_layout.addWidget(self.theme_combobox) + theme_layout.addStretch() + + self.theme_combobox.currentTextChanged.connect( + lambda text: QGuiApplication.styleHints().setColorScheme(theme_toggle[text]) # noqa + ) + + self.display_container.setLayout(theme_layout) + def reset_connection(self) -> None: """ @@ -310,7 +345,9 @@ def accept(self) -> None: :return: None """ logging.info("Settings accepted.") - + self.config['DISPLAY']['theme'] = self.theme_combobox.currentText() + with open(self.config_path, 'w') as configfile: + self.config.write(configfile) self.close() def closeEvent( From 2650d6e78aa77a3a791b2d4c5de54833e3eb1464 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Thu, 14 May 2026 08:38:30 -0500 Subject: [PATCH 17/28] FEAT: Add dark mode to settings dialog. --- faslr/templates/config_template.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/faslr/templates/config_template.ini b/faslr/templates/config_template.ini index eec8e018..5a21f5ab 100644 --- a/faslr/templates/config_template.ini +++ b/faslr/templates/config_template.ini @@ -3,3 +3,6 @@ startup_db = None [PLOTTING_STYLE] plotting_style = Regular + +[DISPLAY] +theme = System \ No newline at end of file From 5d44f3e4bf0150a9dc852abad940a8bfd5d8881a Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Thu, 14 May 2026 08:38:56 -0500 Subject: [PATCH 18/28] DOCS: Add annotations to AnalysisTab qss. --- faslr/analysis.py | 11 +--- faslr/style/analysis.py | 143 +++++++++++++++++++++++++++------------- 2 files changed, 98 insertions(+), 56 deletions(-) diff --git a/faslr/analysis.py b/faslr/analysis.py index 22c64243..84513d0a 100644 --- a/faslr/analysis.py +++ b/faslr/analysis.py @@ -160,15 +160,6 @@ def __init__( triangle_model = TriangleModel(triangle_column, 'value') self.triangle_views[i].setModel(triangle_model) - # self.analysis_containers[i].setStyleSheet( - # """ - # DiagnosticWidget { - # border: 2px solid darkgrey; - # background: rgb(230, 230, 230); - # } - # """ - # ) - self.column_tab.addTab(self.analysis_containers[i], i) self.layout.addWidget( @@ -240,7 +231,7 @@ def apply_theme(self, scheme: Qt.ColorScheme): palette = self.palette() qss_analysis_tab_palette( - theme=scheme, + scheme=scheme, palette=palette, role=self.backgroundRole() ) diff --git a/faslr/style/analysis.py b/faslr/style/analysis.py index 2da3d764..0dca17c7 100644 --- a/faslr/style/analysis.py +++ b/faslr/style/analysis.py @@ -9,17 +9,23 @@ def qss_column_tab( bottom_border_width: str, margin_top: str, -) -> str: + ) -> str: """ - + Styles the column tab of the AnalysisPane. ColumnTab is the tab containing column information, with column + referring to a Triangle column in Chainladder. + Parameters ---------- - scheme - bottom_border_width - margin_top + scheme: Qt.ColorScheme + The current color scheme (Light, Dark, Unknown). + bottom_border_width: str + The bottom border width of the tab, in pixels. + margin_top: str + Top margin adjustment for when there's only one column tab open. Returns ------- + The QSS string used to style the ColumnTab. """ @@ -32,22 +38,30 @@ def qss_column_tab( if scheme == Qt.ColorScheme.Dark: tab_unselected_background = dark_unselected tab_selected_background = dark_selected - scrollbar_horizontal_background = dark_selected - scrollbar_horizontal_groove_background = "rgb(42, 42, 42)" + # scrollbar_horizontal_background = dark_selected + qtabbar_border = 'darkgrey' + # scrollbar_horizontal_groove_background = "rgb(42, 42, 42)" + # scrollbar_handle_background = scrollbar_horizontal_background # Else, default to light, including headless environments like in GitHub Actions. else: tab_unselected_background = light_unselected tab_selected_background = light_selected - scrollbar_horizontal_background = "rgb(217, 217, 217)" - scrollbar_horizontal_groove_background = light_selected + # scrollbar_horizontal_background = "rgb(217, 217, 217)" + qtabbar_border = light_selected + # scrollbar_horizontal_groove_background = light_selected + # scrollbar_handle_background = scrollbar_horizontal_background + - qss_str = """ + # Adjust issue with border not showing up when there's only 1 tab. + qtabbar_tab_first = """ QTabBar::tab:first {{ margin-top: 22px; border-bottom: {}px solid darkgrey; }} + """.format(bottom_border_width) - + # Some stylings on the tab, particularly adjusting the background color when tab is not in focus. + qttabbar_tab = """ QTabBar::tab {{ margin-top: {}px; background: {}; @@ -59,46 +73,67 @@ def qss_column_tab( margin-right: 0px; border-right: 0px; }} + """.format( + margin_top, + tab_unselected_background + ) + # Tab background color when it is selected. + qttabbar_selected = """ QTabBar::tab:selected {{ background: {}; - }} + """.format(tab_selected_background) + # Actually used to suppress the border around the table to keep it from being too prominent. + qtabwidget_pane = """ QTabWidget::pane {{ - border: 1px solid darkgrey; - }} - - QScrollBar:horizontal {{ - background: {}; - height: 14px; - }} - - QScrollBar::handle:horizontal {{ - background: {}; - border: none; - border-radius: 7px; - margin: 2px 0px; - min-width: 10px; - }} - - QScrollBar::sub-line:horizontal {{ - width: 0px; - }} - - QScrollBar::add-line:horizontal {{ - width: 0px; + border: 1px solid {}; }} - - """.format( - bottom_border_width, - margin_top, - tab_unselected_background, - tab_selected_background, - scrollbar_horizontal_groove_background, - scrollbar_horizontal_background, - scrollbar_horizontal_background - ) + """.format(qtabbar_border) + + # Scrollbar corrections when there were issues with color retention toggling between light/dark mode. + # Fixed when Palette is redrawn via __main__.py, might look later if issue ever resurfaces. + + # qscrollbar_horizontal = """ + # QScrollBar:horizontal {{ + # background: {}; + # height: 14px; + # }} + # """.format(scrollbar_horizontal_groove_background) + # + # qscrollbar_handle_horizontal = """ + # QScrollBar::handle:horizontal {{ + # background: {}; + # border: none; + # border-radius: 7px; + # margin: 2px 0px; + # min-width: 10px; + # }} + # """.format(scrollbar_handle_background) + # + # qscrollbar_sub_line_horizontal = """ + # QScrollBar::sub-line:horizontal { + # width: 0px; + # } + # """ + # + # qscrollbar = """ + # QScrollBar::add-line:horizontal { + # width: 0px; + # } + #""" + + qss_str = "\n".join([ + qtabbar_tab_first, + qttabbar_tab, + qttabbar_selected, + qtabwidget_pane, + # qscrollbar_horizontal, + # qscrollbar_handle_horizontal, + # qscrollbar_sub_line_horizontal, + # qscrollbar + ]) # Further refinement - remove solid darkgrey borders if in dark mode. if scheme == Qt.ColorScheme.Dark: @@ -108,12 +143,28 @@ def qss_column_tab( def qss_analysis_tab_palette( - theme: Qt.ColorScheme, + scheme: Qt.ColorScheme, palette: QPalette, role: QPalette.ColorRole ) -> None: + """ + Adjusts the color surrounding the TriangleView but within the top-level tabs of the AnalysisTab. + + Parameters + ---------- + scheme: Qt.ColorScheme + The color scheme, light or dark mode. + palette: QPalette + The palette of the AnalysisTab. + role: QPalette.ColorRole + The backgroundRole of the AnalysisTab. - if theme == Qt.ColorScheme.Dark: + Returns + ------- + None + + """ + if scheme == Qt.ColorScheme.Dark: palette.setColor( role, QColor.fromRgb( From 0c10c6695d7e523428ec7d5af831fbf4b3b0882e Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Thu, 14 May 2026 08:39:27 -0500 Subject: [PATCH 19/28] FEAT: Add function to reset palette after OS theme changes. --- faslr/__main__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/faslr/__main__.py b/faslr/__main__.py index c217f045..c06a6e1a 100644 --- a/faslr/__main__.py +++ b/faslr/__main__.py @@ -119,6 +119,8 @@ def __init__( self.auto_triangle = load_sample('us_industry_auto') self.xyz_triangle = load_sample('uspp_incr_case') + + QGuiApplication.styleHints().colorSchemeChanged.connect(self.set_background_color) self.auto_tab = AnalysisTab( triangle=self.auto_triangle ) @@ -163,9 +165,10 @@ def __init__( main_window=self ) - QGuiApplication.styleHints().colorSchemeChanged.connect(self.set_background_color) + def set_background_color(self, scheme: Qt.ColorScheme) -> None: + QApplication.setPalette(QApplication.style().standardPalette()) palette = self.palette() if scheme == Qt.ColorScheme.Dark: From 0af74cfafa3e4241a2e147793fd5c85bab828d18 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Thu, 14 May 2026 08:40:17 -0500 Subject: [PATCH 20/28] DOCS: Update .gitignore. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4a0034b4..6417c670 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ scratch*.py *.log coverage.xml docs/_build/ -debian/files \ No newline at end of file +debian/files +*.egg-info \ No newline at end of file From 8f8ca85ca7e469474cee44f28d16fdfd50063bcc Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 15 May 2026 21:44:57 -0500 Subject: [PATCH 21/28] FEAT: Add light/dark mode to about dialog. --- faslr/about.py | 86 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/faslr/about.py b/faslr/about.py index bc87ab26..f24f7525 100644 --- a/faslr/about.py +++ b/faslr/about.py @@ -1,3 +1,5 @@ +from faslr.common.icon import InfoIcon + from faslr.constants import ( BUILD_VERSION, CURRENT_BRANCH, @@ -11,8 +13,8 @@ Qt ) -from PyQt6.QtSvgWidgets import ( - QSvgWidget +from PyQt6.QtGui import ( + QGuiApplication, QPalette, QColor ) from PyQt6.QtWidgets import ( @@ -40,30 +42,44 @@ def __init__( faslr_version = "FASLR v" + BUILD_VERSION - faslr_version_link = '' + \ - faslr_version + '' + faslr_version_link = "https://github.com/casact/faslr" - branch_link = '' + \ - CURRENT_BRANCH + '' + branch_link = "https://github.com/casact/faslr/tree/" + CURRENT_BRANCH - commit_link = '' + \ - CURRENT_COMMIT + '' + commit_link = "https://github.com/casact/faslr/commit/" + CURRENT_COMMIT_LONG - version_label = LinkLabel(faslr_version_link) + version_label = LinkLabel( + url=faslr_version_link, + label_text=faslr_version + ) - branch_label = LinkLabel(branch_link) - commit_label = LinkLabel(commit_link) + branch_label = LinkLabel( + url=branch_link, + label_text=CURRENT_BRANCH + ) + + commit_label = LinkLabel( + url=commit_link, + label_text=CURRENT_COMMIT + ) - gh_svg = InfoIcon(ICONS_PATH + "github.svg") + gh_svg = InfoIcon( + svg_path=ICONS_PATH + "github.svg", + width=24, + height=24 + ) - branch_svg = InfoIcon(OCTICONS_PATH + "git-branch-24.svg") + branch_svg = InfoIcon( + svg_path=OCTICONS_PATH + "git-branch-24.svg", + width=24, + height=24 + ) - commit_svg = InfoIcon(OCTICONS_PATH + "commit-24.svg") + commit_svg = InfoIcon( + OCTICONS_PATH + "commit-24.svg", + width=24, + height=24 + ) layout = QVBoxLayout() @@ -99,22 +115,18 @@ def ok(self) -> None: self.close() -class InfoIcon(QSvgWidget): - def __init__( - self, - svg_path: str - ): - super().__init__(svg_path) - - self.setFixedSize(24, 24) class LinkLabel(QLabel): def __init__( self, - url: str + url: str, + label_text: str ): - super().__init__(url) + super().__init__() + + self.url = url + self.label_text = label_text self.setTextFormat(Qt.TextFormat.RichText) @@ -123,3 +135,19 @@ def __init__( ) self.setOpenExternalLinks(True) + self.apply_theme( + scheme=QGuiApplication.styleHints().colorScheme() # noqa + ) + + QGuiApplication.styleHints().colorSchemeChanged.connect(self.apply_theme) # noqa + + + def apply_theme(self, scheme: Qt.ColorScheme): + + if scheme == Qt.ColorScheme.Dark: + color = "#FFFFFF" + else: + color = "#000000" + + self.setText(f'{self.label_text}') + From 0f0ff52cf9282a32dde1db1a1383d0c6b827f8f7 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 15 May 2026 21:45:54 -0500 Subject: [PATCH 22/28] FEAT: Move stylings to style folder. --- faslr/common/table.py | 13 ++++++------- faslr/connection.py | 4 +--- faslr/factor.py | 12 +++++------- faslr/project.py | 7 +------ faslr/project_item.py | 14 +++++++++----- faslr/style/project.py | 6 ++++++ 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/faslr/common/table.py b/faslr/common/table.py index 4225c639..8c61b292 100644 --- a/faslr/common/table.py +++ b/faslr/common/table.py @@ -1,5 +1,9 @@ from __future__ import annotations +from faslr.style.table import ( + corner_button_qss +) + from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -36,14 +40,9 @@ def make_corner_button( btn.setLayout(btn_layout) opt = QStyleOptionHeader() - def apply_style(theme: Qt.ColorScheme): - - if theme == Qt.ColorScheme.Dark: - color = "rgb(60, 60, 60)" - else: - color = "darkgrey" + def apply_style(scheme: Qt.ColorScheme): - btn.setStyleSheet(f"border: 1px outset {color};") + btn.setStyleSheet(corner_button_qss(scheme=scheme)) apply_style(QGuiApplication.styleHints().colorScheme()) QGuiApplication.styleHints().colorSchemeChanged.connect(apply_style) # noqa diff --git a/faslr/connection.py b/faslr/connection.py index c56cb0d0..8a7f4f75 100644 --- a/faslr/connection.py +++ b/faslr/connection.py @@ -6,7 +6,6 @@ import sqlalchemy as sa from faslr.constants import ( - CONFIG_PATH, DB_NOT_FOUND_TEXT, DEFAULT_DIALOG_PATH, QT_FILEPATH_OPTION @@ -274,8 +273,7 @@ def populate_project_tree( for lob, lob_uuid in lobs: lob_item = ProjectItem( lob, - segment_level='lob', - text_color=QColor(0, 77, 122) + segment_level='lob' ) lob_row = [lob_item, QStandardItem(lob_uuid)] diff --git a/faslr/factor.py b/faslr/factor.py index 7cf88bd2..e2063659 100644 --- a/faslr/factor.py +++ b/faslr/factor.py @@ -16,6 +16,10 @@ TEMP_LDF_LIST ) +from faslr.style.factor import ( + FACTOR_VIEW_QSS +) + from faslr.utilities import df_set_false from pandas import DataFrame @@ -482,13 +486,7 @@ def __init__(self): # Set the styling for the table corner so that it matches the rest of the headers. self.setStyleSheet( - """ - QTableCornerButton::section{ - border-width: 1px; - border-style: solid; - border-color:none darkgrey darkgrey none; - } - """ + FACTOR_VIEW_QSS ) s = QSize(btn.style().sizeFromContents( diff --git a/faslr/project.py b/faslr/project.py index 6ba3feba..00de0fc8 100644 --- a/faslr/project.py +++ b/faslr/project.py @@ -138,12 +138,7 @@ def make_project( lob = ProjectItem( text=lob_text, - segment_level="lob", - text_color=QColor( - 155, - 0, - 0 - ) + segment_level="lob" ) # Check if the country is already in the database diff --git a/faslr/project_item.py b/faslr/project_item.py index c99ccd79..ae911637 100644 --- a/faslr/project_item.py +++ b/faslr/project_item.py @@ -1,4 +1,8 @@ -from faslr.style.project import DEFAULT_PROJECT_FONT +from faslr.style.project import ( + DEFAULT_PROJECT_FONT, + PROJECT_ITEM_TEXT_DARK, + PROJECT_ITEM_TEXT_LIGHT +) from PyQt6.QtCore import Qt @@ -47,8 +51,8 @@ def __init__( # Set the text color based on theme. if not text_color: - theme = QGuiApplication.styleHints().colorScheme() - self.text_color = QColor(0, 0, 0) if theme == Qt.ColorScheme.Light else QColor(255, 255, 255) + theme = QGuiApplication.styleHints().colorScheme() # noqa + self.text_color = PROJECT_ITEM_TEXT_DARK if theme == Qt.ColorScheme.Dark else PROJECT_ITEM_TEXT_LIGHT else: self.text_color = text_color @@ -58,11 +62,11 @@ def __init__( self.setFont(project_font) self.setText(text) - QGuiApplication.styleHints().colorSchemeChanged.connect(self.toggle_light_dark_text) + QGuiApplication.styleHints().colorSchemeChanged.connect(self.toggle_light_dark_text) # noqa def toggle_light_dark_text(self, theme: Qt.ColorScheme) -> None: color = QColor(255, 255, 255) if theme == Qt.ColorScheme.Dark else QColor(0, 0, 0) self.setForeground(color) if self.model(): idx = self.index() - self.model().dataChanged.emit(idx, idx) \ No newline at end of file + self.model().dataChanged.emit(idx, idx) # noqa diff --git a/faslr/style/project.py b/faslr/style/project.py index c8f458ff..da029ba1 100644 --- a/faslr/style/project.py +++ b/faslr/style/project.py @@ -1 +1,7 @@ +from PyQt6.QtGui import QColor + DEFAULT_PROJECT_FONT = 'Ubuntu Regular' + +PROJECT_ITEM_TEXT_LIGHT = QColor(0, 0, 0) + +PROJECT_ITEM_TEXT_DARK = QColor(255, 255, 255) \ No newline at end of file From 2d52c0b2be1c9f5f73a094436dd9f499a229dec9 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 15 May 2026 21:46:21 -0500 Subject: [PATCH 23/28] FEAT: Create dark mode icons. --- faslr/common/icon.py | 65 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 faslr/common/icon.py diff --git a/faslr/common/icon.py b/faslr/common/icon.py new file mode 100644 index 00000000..d99e70c2 --- /dev/null +++ b/faslr/common/icon.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import os + +from PyQt6.QtCore import ( + QObject, + Qt +) + +from PyQt6.QtGui import ( + QGuiApplication, + QIcon +) + +from PyQt6.QtSvgWidgets import ( + QSvgWidget +) + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + from PyQt6.QtGui import QAction + from PyQt6.QtWidgets import QAbstractButton + +class InfoIcon(QSvgWidget): + def __init__( + self, + svg_path: str, + width: int = None, + height: int = None + ): + super().__init__(svg_path) + root, ext = os.path.splitext(svg_path) + self.light_path = svg_path + self.dark_path = root + '_white' + ext + self.apply_theme(QGuiApplication.styleHints().colorScheme()) # noqa + QGuiApplication.styleHints().colorSchemeChanged.connect(self.apply_theme) # noqa + + if width and height: + self.setFixedSize(width, height) + + def apply_theme(self, scheme:Qt.ColorScheme) -> None: + path = self.dark_path if scheme == Qt.ColorScheme.Dark else self.light_path + self.load(path) + + +class MenuIcon(QObject): + def __init__( + self, + icon_path: str, + widget: Union[QAction, QAbstractButton] + ): + super().__init__() + + root, ext = os.path.splitext(icon_path) + self.light_path = icon_path + self.dark_path = root + '_white' + ext + self._widget = widget + self.apply_theme(QGuiApplication.styleHints().colorScheme()) # noqa + QGuiApplication.styleHints().colorSchemeChanged.connect(self.apply_theme) # noqa + + def apply_theme(self, scheme: Qt.ColorScheme) -> None: + path = self.dark_path if scheme == Qt.ColorScheme.Dark else self.light_path + self._widget.setIcon(QIcon(path)) From de495bb7eb69957e20d57398f0fb3db4eaf9ff96 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 15 May 2026 21:46:42 -0500 Subject: [PATCH 24/28] FEAT: Enable dark mode icons in menus. --- faslr/menu.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/faslr/menu.py b/faslr/menu.py index 556e131d..b8de8d48 100644 --- a/faslr/menu.py +++ b/faslr/menu.py @@ -19,6 +19,8 @@ OCTICONS_PATH ) +from faslr.common.menu import MenuAction + import faslr.core as core from faslr.engine import EngineDialog @@ -53,7 +55,7 @@ def __init__( self.parent = parent - self.connection_action = QAction(QIcon(ICONS_PATH + "db.svg"), "&Connection", self) + self.connection_action = MenuAction(ICONS_PATH + "db.svg", "&Connection", self) self.connection_action.setShortcut(QKeySequence("Ctrl+Shift+c")) self.connection_action.setStatusTip("Edit database connection.") # noinspection PyUnresolvedReferences @@ -85,20 +87,24 @@ def __init__( # noinspection PyUnresolvedReferences self.about_action.triggered.connect(self.display_about) - self.documentation_action = QAction(QIcon(ICONS_PATH + "open-in-browser.svg"), "&Documentation", self) + self.documentation_action = MenuAction(ICONS_PATH + "open-in-browser.svg", "&Documentation", self) self.documentation_action.setStatusTip("Go to the documentation website.") self.documentation_action.setShortcut("F1") self.documentation_action.triggered.connect(open_documentation) # noqa - self.github_action = QAction(QIcon(ICONS_PATH + "github.svg"), "&GitHub Repo", self) + self.github_action = MenuAction( + icon_path=ICONS_PATH + "github.svg", + text="&GitHub Repo", + parent=self + ) self.github_action.setStatusTip("Go to the GitHub Repo.") self.github_action.triggered.connect(open_github) # noqa - self.discussions_action = QAction(QIcon(OCTICONS_PATH + "comment-discussion-24.svg"), "&Discussion Board") + self.discussions_action = MenuAction(OCTICONS_PATH + "comment-discussion-24.svg", "&Discussion Board") self.discussions_action.setStatusTip("Go to the discussion board.") self.discussions_action.triggered.connect(open_discussions) # noqa - self.issues_action = QAction(QIcon(ICONS_PATH + "kanban-board.svg"), "&Open an Issue") + self.issues_action = MenuAction(ICONS_PATH + "kanban-board.svg", "&Open an Issue") self.issues_action.setStatusTip("Open an issue on GitHub.") self.issues_action.triggered.connect(open_issue) # noqa From 344c3cd7e2a7e07f9dd8fa5ff56d233ea806d89c Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 15 May 2026 21:47:05 -0500 Subject: [PATCH 25/28] FEAT: Add dark mode icons. --- faslr/style/icons/db_white.svg | 1 + faslr/style/icons/github_white.svg | 1 + faslr/style/icons/kanban-board_white.svg | 1 + faslr/style/icons/octicons/comment-discussion-24_white.svg | 1 + faslr/style/icons/octicons/commit-24_white.svg | 1 + faslr/style/icons/octicons/git-branch-24_white.svg | 1 + faslr/style/icons/open-in-browser_white.svg | 1 + 7 files changed, 7 insertions(+) create mode 100644 faslr/style/icons/db_white.svg create mode 100644 faslr/style/icons/github_white.svg create mode 100644 faslr/style/icons/kanban-board_white.svg create mode 100644 faslr/style/icons/octicons/comment-discussion-24_white.svg create mode 100644 faslr/style/icons/octicons/commit-24_white.svg create mode 100644 faslr/style/icons/octicons/git-branch-24_white.svg create mode 100644 faslr/style/icons/open-in-browser_white.svg diff --git a/faslr/style/icons/db_white.svg b/faslr/style/icons/db_white.svg new file mode 100644 index 00000000..d9a4bf01 --- /dev/null +++ b/faslr/style/icons/db_white.svg @@ -0,0 +1 @@ + diff --git a/faslr/style/icons/github_white.svg b/faslr/style/icons/github_white.svg new file mode 100644 index 00000000..0e1d5c9a --- /dev/null +++ b/faslr/style/icons/github_white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/faslr/style/icons/kanban-board_white.svg b/faslr/style/icons/kanban-board_white.svg new file mode 100644 index 00000000..385f5558 --- /dev/null +++ b/faslr/style/icons/kanban-board_white.svg @@ -0,0 +1 @@ + diff --git a/faslr/style/icons/octicons/comment-discussion-24_white.svg b/faslr/style/icons/octicons/comment-discussion-24_white.svg new file mode 100644 index 00000000..f7cd50e1 --- /dev/null +++ b/faslr/style/icons/octicons/comment-discussion-24_white.svg @@ -0,0 +1 @@ + diff --git a/faslr/style/icons/octicons/commit-24_white.svg b/faslr/style/icons/octicons/commit-24_white.svg new file mode 100644 index 00000000..3e1249f6 --- /dev/null +++ b/faslr/style/icons/octicons/commit-24_white.svg @@ -0,0 +1 @@ + diff --git a/faslr/style/icons/octicons/git-branch-24_white.svg b/faslr/style/icons/octicons/git-branch-24_white.svg new file mode 100644 index 00000000..b8103ee3 --- /dev/null +++ b/faslr/style/icons/octicons/git-branch-24_white.svg @@ -0,0 +1 @@ + diff --git a/faslr/style/icons/open-in-browser_white.svg b/faslr/style/icons/open-in-browser_white.svg new file mode 100644 index 00000000..023e0889 --- /dev/null +++ b/faslr/style/icons/open-in-browser_white.svg @@ -0,0 +1 @@ + From ef23f9d98e3a30d7ca1bbdc2a077b6bcbccc4e1f Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 15 May 2026 21:47:20 -0500 Subject: [PATCH 26/28] DOCS: Add octicons license. --- faslr/style/icons/octicons/LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 faslr/style/icons/octicons/LICENSE diff --git a/faslr/style/icons/octicons/LICENSE b/faslr/style/icons/octicons/LICENSE new file mode 100644 index 00000000..d3b88f85 --- /dev/null +++ b/faslr/style/icons/octicons/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file From bbd2b674de28acda7a8535926f609b88514db52d Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 15 May 2026 21:47:36 -0500 Subject: [PATCH 27/28] FEAT: Move stylings to style folder. --- faslr/style/about.py | 3 +++ faslr/style/factor.py | 7 +++++++ faslr/style/table.py | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 faslr/style/about.py create mode 100644 faslr/style/factor.py create mode 100644 faslr/style/table.py diff --git a/faslr/style/about.py b/faslr/style/about.py new file mode 100644 index 00000000..21ab0fc4 --- /dev/null +++ b/faslr/style/about.py @@ -0,0 +1,3 @@ +ABOUT_DIALOG_LINK_COLOR_DARK = '#FFFFFF' + +ABOUT_DIALOG_LINK_COLOR_LIGHT = '#000000' \ No newline at end of file diff --git a/faslr/style/factor.py b/faslr/style/factor.py new file mode 100644 index 00000000..13f6048d --- /dev/null +++ b/faslr/style/factor.py @@ -0,0 +1,7 @@ +FACTOR_VIEW_QSS = """ + QTableCornerButton::section{ + border-width: 1px; + border-style: solid; + border-color:none darkgrey darkgrey none; + } +""" \ No newline at end of file diff --git a/faslr/style/table.py b/faslr/style/table.py new file mode 100644 index 00000000..666107c2 --- /dev/null +++ b/faslr/style/table.py @@ -0,0 +1,18 @@ +from PyQt6.QtCore import Qt + +CORNER_BUTTON_BORDER_COLOR_DARK = "rgb(60, 60, 60)" + +CORNER_BUTTON_BORDER_COLOR_LIGHT = "darkgrey" + +def corner_button_qss( + scheme: Qt.ColorScheme +) -> str: + + if scheme == Qt.ColorScheme.Dark: + color = CORNER_BUTTON_BORDER_COLOR_DARK + else: + color = CORNER_BUTTON_BORDER_COLOR_LIGHT + + qss_str = f"border: 1px outset {color};" + + return qss_str \ No newline at end of file From 9569cdd35f13001aa9560f49ca639530b76d5ae9 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Fri, 15 May 2026 21:47:58 -0500 Subject: [PATCH 28/28] FEAT: Create dedicated MenuAction class. --- faslr/common/menu.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 faslr/common/menu.py diff --git a/faslr/common/menu.py b/faslr/common/menu.py new file mode 100644 index 00000000..33414114 --- /dev/null +++ b/faslr/common/menu.py @@ -0,0 +1,28 @@ +import os + +from PyQt6.QtCore import Qt + +from PyQt6.QtGui import ( + QAction, + QGuiApplication, + QIcon +) + + +class MenuAction(QAction): + def __init__( + self, + icon_path: str, + text: str, + parent=None + ): + super().__init__(text, parent) + root, ext = os.path.splitext(icon_path) + self.light_path = icon_path + self.dark_path = root + '_white' + ext + self.apply_theme(QGuiApplication.styleHints().colorScheme()) # noqa + QGuiApplication.styleHints().colorSchemeChanged.connect(self.apply_theme) # noqa + + def apply_theme(self, scheme: Qt.ColorScheme) -> None: + path = self.dark_path if scheme == Qt.ColorScheme.Dark else self.light_path + self.setIcon(QIcon(path))