\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x02\xfe\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\x00\x00\x02\x07\
+<\
+svg width=\x2240\x22 h\
+eight=\x2240\x22 viewB\
+ox=\x220 0 40 40\x22 f\
+ill=\x22none\x22 xmlns\
+=\x22http://www.w3.\
+org/2000/svg\x22>\x0d\x0a\
+\x0d\x0a\
+svg>\x0d\x0a\
+\x00\x00\x021\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#FFFFFF\x22>\
+\
+\x00\x00\x01\xa4\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\x00\x00\x02\xd3\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#FFFFFF\x22>\
+\x00\x00\x05O\
+<\
+svg width=\x2240\x22 h\
+eight=\x2240\x22 viewB\
+ox=\x220 0 40 40\x22 f\
+ill=\x22none\x22 xmlns\
+=\x22http://www.w3.\
+org/2000/svg\x22>\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x08+\
+<\
+svg width=\x2236\x22 h\
+eight=\x22922\x22 view\
+Box=\x220 0 36 922\x22\
+ fill=\x22none\x22 xml\
+ns=\x22http://www.w\
+3.org/2000/svg\x22>\
+\x0d\x0a\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x02\xde\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\x00\x00\x021\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\
+\x00\x00\x0aV\
+<\
+svg width=\x2236\x22 h\
+eight=\x22918\x22 view\
+Box=\x220 0 36 918\x22\
+ fill=\x22none\x22 xml\
+ns=\x22http://www.w\
+3.org/2000/svg\x22>\
+\x0d\x0a\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x05O\
+<\
+svg width=\x2240\x22 h\
+eight=\x2240\x22 viewB\
+ox=\x220 0 40 40\x22 f\
+ill=\x22none\x22 xmlns\
+=\x22http://www.w3.\
+org/2000/svg\x22>\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x02\xbb\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#FFFFFF\x22>\
+\x00\x00\x02/\
+<\
+svg width=\x2240\x22 h\
+eight=\x2240\x22 viewB\
+ox=\x220 0 40 40\x22 f\
+ill=\x22none\x22 xmlns\
+=\x22http://www.w3.\
+org/2000/svg\x22>\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x01\x9d\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\x00\x00\x01\xa2\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\x00\x00\x025\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#FFFFFF\x22>\
+svg>\
+"
+
+qt_resource_name = b"\
+\x00\x07\
+\x0c\xba\xb6s\
+\x00v\
+\x00e\x00c\x00t\x00o\x00r\x00s\
+\x00\x06\
+\x07\xae\xc3\xc3\
+\x00t\
+\x00h\x00e\x00m\x00e\x00s\
+\x00\x0e\
+\x03\x9b1c\
+\x00l\
+\x00i\x00g\x00h\x00t\x00s\x00t\x00y\x00l\x00e\x00.\x00q\x00s\x00s\
+\x00\x0b\
+\x01d\x8d\x87\
+\x00c\
+\x00h\x00e\x00c\x00k\x00e\x00d\x00.\x00s\x00v\x00g\
+\x00\x1b\
+\x0aa\xaa'\
+\x00i\
+\x00n\x00p\x00u\x00t\x00_\x00d\x00o\x00c\x00k\x00_\x00a\x00c\x00t\x00i\x00v\x00e\
+\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x0e\
+\x06\xb1\x9b\x07\
+\x00O\
+\x00s\x00d\x00a\x00g\x00_\x00l\x00o\x00g\x00o\x00.\x00s\x00v\x00g\
+\x00\x0e\
+\x0d\xd6\xeag\
+\x00l\
+\x00o\x00c\x00k\x00_\x00c\x00l\x00o\x00s\x00e\x00.\x00s\x00v\x00g\
+\x00\x1a\
+\x0b}\x5cG\
+\x00l\
+\x00o\x00g\x00s\x00_\x00d\x00o\x00c\x00k\x00_\x00a\x00c\x00t\x00i\x00v\x00e\x00_\
+\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x13\
+\x0cPg\xa7\
+\x00a\
+\x00r\x00r\x00o\x00w\x00_\x00d\x00o\x00w\x00n\x00_\x00d\x00a\x00r\x00k\x00.\x00s\
+\x00v\x00g\
+\x00\x1e\
+\x08a\xb9\xc7\
+\x00o\
+\x00u\x00t\x00p\x00u\x00t\x00_\x00d\x00o\x00c\x00k\x00_\x00i\x00n\x00a\x00c\x00t\
+\x00i\x00v\x00e\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x0a\
+\x0f\xec\x03\xe7\
+\x00d\
+\x00e\x00s\x00i\x00g\x00n\x00.\x00s\x00v\x00g\
+\x00\x12\
+\x0aDDG\
+\x00a\
+\x00r\x00r\x00o\x00w\x00_\x00u\x00p\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\
+\x00g\
+\x00\x16\
+\x0e\x16='\
+\x00i\
+\x00n\x00p\x00u\x00t\x00s\x00_\x00l\x00a\x00b\x00e\x00l\x00_\x00l\x00i\x00g\x00h\
+\x00t\x00.\x00s\x00v\x00g\
+\x00\x0d\
+\x005w\xc7\
+\x00l\
+\x00o\x00c\x00k\x00_\x00o\x00p\x00e\x00n\x00.\x00s\x00v\x00g\
+\x00\x14\
+\x01\xd3}\xa7\
+\x00a\
+\x00r\x00r\x00o\x00w\x00_\x00d\x00o\x00w\x00n\x00_\x00l\x00i\x00g\x00h\x00t\x00.\
+\x00s\x00v\x00g\
+\x00\x17\
+\x0d\xa0\xcd'\
+\x00o\
+\x00u\x00t\x00p\x00u\x00t\x00s\x00_\x00l\x00a\x00b\x00e\x00l\x00_\x00l\x00i\x00g\
+\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x11\
+\x03\xf9t'\
+\x00a\
+\x00r\x00r\x00o\x00w\x00_\x00u\x00p\x00_\x00d\x00a\x00r\x00k\x00.\x00s\x00v\x00g\
+\
+\x00\x11\
+\x08\xb1e\xc7\
+\x00d\
+\x00e\x00s\x00i\x00g\x00n\x00_\x00r\x00e\x00p\x00o\x00r\x00t\x00.\x00s\x00v\x00g\
+\
+\x00\x1c\
+\x0ap\xe4'\
+\x00o\
+\x00u\x00t\x00p\x00u\x00t\x00_\x00d\x00o\x00c\x00k\x00_\x00a\x00c\x00t\x00i\x00v\
+\x00e\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x1c\
+\x01\xd8[\xc7\
+\x00l\
+\x00o\x00g\x00s\x00_\x00d\x00o\x00c\x00k\x00_\x00i\x00n\x00a\x00c\x00t\x00i\x00v\
+\x00e\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x1d\
+\x0e\xaf\xb9\xe7\
+\x00i\
+\x00n\x00p\x00u\x00t\x00_\x00d\x00o\x00c\x00k\x00_\x00i\x00n\x00a\x00c\x00t\x00i\
+\x00v\x00e\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x08\
+\x08\xc8U\xe7\
+\x00s\
+\x00a\x00v\x00e\x00.\x00s\x00v\x00g\
+"
+
+qt_resource_struct = b"\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\
+\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x16\
+\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\
+\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x00/T\
+\x00\x00\x01\x9b\x13E\x17;\
+\x00\x00\x00H\x00\x00\x00\x00\x00\x01\x00\x00\x04\xeb\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x0026\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x03\x12\x00\x00\x00\x00\x00\x01\x00\x00I\x0a\
+\x00\x00\x01\x9a\xfc\x9f}A\
+\x00\x00\x02\x84\x00\x00\x00\x00\x00\x01\x00\x00>\xc5\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x01\x00\x00\x07\xd2\
+\x00\x00\x01\x9b\x13\xd0_\xa8\
+\x00\x00\x01J\x00\x00\x00\x00\x00\x01\x00\x00\x1dS\
+\x00\x00\x01\x9a\xfc\x9f}M\
+\x00\x00\x02\xac\x00\x00\x00\x00\x00\x01\x00\x00D\x18\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x03\x90\x00\x00\x00\x00\x00\x01\x00\x00LQ\
+\x00\x00\x01\x9b\x13E\x17;\
+\x00\x00\x01\xa6\x00\x00\x00\x00\x00\x01\x00\x00!\xd2\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x00d\x00\x00\x00\x00\x00\x01\x00\x00\x05\xa1\
+\x00\x00\x01\x9a\xfc\x9f};\
+\x00\x00\x02\xd4\x00\x00\x00\x00\x00\x01\x00\x00F\xd7\
+\x00\x00\x01\x9a\xfc\x9f}K\
+\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x01\x00\x00\x19\x13\
+\x00\x00\x01\x9a\xfc\x9f}A\
+\x00\x00\x01\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x1b\x1e\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x004k\
+\x00\x00\x01\x9b\x07\x002e\
+\x00\x00\x00\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x16\x11\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00'%\
+\x00\x00\x01\x9b\x07\x002a\
+\x00\x00\x03P\x00\x00\x00\x00\x00\x01\x00\x00J\xab\
+\x00\x00\x01\x9a\xfc\x9f}<\
+\x00\x00\x01\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x1e\xfb\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x00&\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\
+\x00\x00\x01\x9b\x13\xd50\xdc\
+"
+
+def qInitResources():
+ QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+def qCleanupResources():
+ QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+qInitResources()
diff --git a/src/osdagbridge/desktop/resources/themes/darkstyle.qss b/src/osdagbridge/desktop/resources/themes/darkstyle.qss
new file mode 100644
index 00000000..f9c6f9ac
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/themes/darkstyle.qss
@@ -0,0 +1,32 @@
+/* ==============================================
+ OSDAG GUI STYLESHEET - ORGANIZED BY SPECIFICITY
+ ============================================== */
+
+/* ==============================================
+ 1. GLOBAL STYLES (Least Specific)
+ ============================================== */
+* {
+ font-family: "Ubuntu Sans";
+}
+
+QMainWindow {
+ background-color: #282828;
+ border: 1px solid #6B7D20;
+ margin: 0px;
+ padding: 0px;
+}
+
+QTabWidget {
+ background-color: #333333;
+ border: 0px;
+}
+
+QToolTip {
+ background-color: #2B2B2B;
+ color: #D0D0D0;
+ border: 1px solid #6B7D20;
+ padding: 2px 2px;
+ font-size: 12px;
+ border-radius: 0px;
+ qproperty-alignment: AlignVCenter;
+}
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/themes/lightstyle.qss b/src/osdagbridge/desktop/resources/themes/lightstyle.qss
new file mode 100644
index 00000000..452eeeb6
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/themes/lightstyle.qss
@@ -0,0 +1,243 @@
+/* ==============================================
+ OSDAG GUI STYLESHEET - ORGANIZED BY SPECIFICITY
+ ============================================== */
+
+/* ==============================================
+ 1. GLOBAL STYLES (Least Specific)
+ ============================================== */
+QMainWindow {
+ background-color: #f4f4f4;
+ border: 1px solid #90af13;
+ margin: 0px;
+ padding: 0px;
+}
+
+QToolTip {
+ background-color: #FFFFFF;
+ color: #000000;
+ border: 1px solid #90AF13;
+ padding: 2px 2px;
+ font-size: 12px;
+ border-radius: 0px;
+ qproperty-alignment: AlignVCenter;
+}
+QSplitter::handle {
+ background-color: #D0D0D0;
+}
+
+/* Global Checkbox Style */
+QCheckBox {
+ font-size: 10px;
+ color: #333;
+ spacing: 6px;
+}
+QCheckBox::indicator {
+ width: 16px;
+ height: 16px;
+ border: 1px solid #333333;
+ border-radius: 3px;
+ background-color: #ffffff;
+}
+QCheckBox::indicator:hover {
+ border: 1px solid #555555;
+}
+QCheckBox::indicator:checked {
+ background-color: #ffffff;
+ border: 1px solid #333333;
+ image: url(:/vectors/checked.svg);
+}
+
+QMenuBar#template_page_menu_bar {
+ background-color: #F4F4F4;
+ color: #000000;
+ padding: 0px;
+}
+QMenuBar#template_page_menu_bar::item {
+ padding: 5px 10px;
+ background: transparent;
+ border-radius: 0px;
+}
+QMenuBar#template_page_menu_bar::item:selected {
+ background: #FFFFFF;
+}
+QMenuBar#template_page_menu_bar::item:pressed {
+ background: #E8E8E8;
+}
+QMenuBar#template_page_menu_bar QMenu {
+ background-color: #FFFFFF;
+ border: 1px solid #D0D0D0;
+ border-radius: 4px;
+ padding: 0px;
+}
+QMenuBar#template_page_menu_bar QMenu::item {
+ padding: 5px;
+ color: #000000;
+ font-size: 11px;
+}
+QMenuBar#template_page_menu_bar QMenu::item:selected {
+ background-color: #E6F0FF;
+ border-radius: 3px;
+}
+QMenuBar#template_page_menu_bar QMenu::separator {
+ height: 1px;
+ background: #F0F0F0;
+ margin-left: 2px;
+ margin-right: 2px;
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+QMenuBar#template_page_menu_bar QMenu::right-arrow {
+ width: 8px;
+ height: 8px;
+}
+
+/* Dropdown menu style */
+QMenu {
+ background: #fff;
+ border: 1px solid #90AF13;
+ font-size: 14px;
+ padding: 0px;
+}
+
+QMenu::item {
+ padding: 8px 16px;
+ color: #333;
+ border: none;
+ margin: 1px;
+}
+
+QMenu::item:selected {
+ background: #90AF13;
+ color: #fff;
+ border-radius: 2px;
+}
+
+/* ==============================================
+ 3. GENERAL BUTTON STYLES (Base Level)
+ ============================================== */
+QPushButton {
+ background-color: white;
+ color: black;
+ font-weight: bold;
+ border-radius: 5px;
+ border: 1px solid black;
+ padding: 5px 14px;
+ text-align: center;
+}
+
+QPushButton:hover {
+ background-color: #90AF13;
+ border: 1px solid #90AF13;
+ color: white;
+}
+
+QPushButton:pressed {
+ color: black;
+ background-color: white;
+ border: 1px solid black;
+}
+
+QWidget#CustomTitleBar {
+ background-color: #f4f4f4;
+}
+QLabel#TitleLabel {
+ color: #000000;
+ padding: 0px;
+ background: transparent;
+}
+QLabel#LogoLabel {
+ background: transparent;
+ color: #ffffff;
+ font-size: 14px;
+}
+
+QToolButton#MinimizeButton {
+ background-color: transparent;
+ color: #000000;
+ border: 0px;
+ border-radius: 0px;
+ font-size: 16px;
+ border-radius: 0px;
+}
+
+QToolButton#MinimizeButton:hover {
+ background-color: #f1f1f1;
+}
+
+QToolButton#MinimizeButton:pressed {
+ background-color: #a6a6a6;
+}
+
+QToolButton#MaxRestoreButton {
+ background-color: transparent;
+ color: #000000;
+ border: 0px;
+ border-radius: 0px;
+ font-size: 16px;
+ border-radius: 0px;
+}
+
+QToolButton#MaxRestoreButton:hover {
+ background-color: #f1f1f1;
+}
+
+QToolButton#MaxRestoreButton:pressed {
+ background-color: #a6a6a6;
+}
+
+QToolButton#CloseButton {
+ background-color: transparent;
+ color: #000000;
+ border: 0px;
+ border-radius: 0px;
+ font-size: 16px;
+ border-radius: 0px;
+}
+
+QToolButton#CloseButton:hover {
+ background-color: #e74c3c;
+ color: #ffffff;
+}
+
+QToolButton#CloseButton:pressed {
+ background-color: #c0392b;
+}
+QWidget#BottomLine {
+ background-color: #90AF13;
+}
+/*=======================Logs-Dock===========================*/
+QWidget#logs_dock QLabel {
+ background-color: #F2F2F2;
+ color: #000000;
+ padding: 3px;
+ font-weight: bold;
+ font-size: 12px;
+}
+QWidget#logs_dock QTextEdit {
+ background-color: #F8F8F8;
+ border: 1px solid #D0D0D0;
+ font-family: 'Courier New', monospace;
+ font-size: 12px;
+ padding: 5px;
+ color: #000000;
+}
+QWidget#logs_dock QScrollBar:vertical {
+ background: #E0E0E0; /* Light grey for the scrollbar track */
+ width: 8px;
+ margin: 0px 0px 0px 3px;
+ border-radius: 2px;
+}
+QWidget#logs_dock QScrollBar::handle:vertical {
+ background: #A0A0A0; /* Medium grey for the scrollbar handle */
+ min-height: 30px;
+ border-radius: 2px;
+}
+QWidget#logs_dock QScrollBar::handle:vertical:hover {
+ background: #707070; /* Darker grey on hover for the handle */
+}
+QWidget#logs_dock QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
+ height: 0px; /* Hides the up/down arrows */
+}
+QWidget#logs_dock QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
+ background: none; /* Hides the area between handle and arrows */
+}
diff --git a/src/osdagbridge/desktop/resources/vectors/Osdag_logo.svg b/src/osdagbridge/desktop/resources/vectors/Osdag_logo.svg
new file mode 100644
index 00000000..1ee3a590
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/Osdag_logo.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/osdagbridge/desktop/resources/vectors/arrow_down_dark.svg b/src/osdagbridge/desktop/resources/vectors/arrow_down_dark.svg
new file mode 100644
index 00000000..2c10661b
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/arrow_down_dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/vectors/arrow_down_light.svg b/src/osdagbridge/desktop/resources/vectors/arrow_down_light.svg
new file mode 100644
index 00000000..52e93659
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/arrow_down_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/vectors/arrow_up_dark.svg b/src/osdagbridge/desktop/resources/vectors/arrow_up_dark.svg
new file mode 100644
index 00000000..34f7193e
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/arrow_up_dark.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/osdagbridge/desktop/resources/vectors/arrow_up_light.svg b/src/osdagbridge/desktop/resources/vectors/arrow_up_light.svg
new file mode 100644
index 00000000..494d7624
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/arrow_up_light.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/osdagbridge/desktop/resources/vectors/checked.svg b/src/osdagbridge/desktop/resources/vectors/checked.svg
new file mode 100644
index 00000000..829f9eda
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/checked.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/vectors/design.svg b/src/osdagbridge/desktop/resources/vectors/design.svg
new file mode 100644
index 00000000..a5085160
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/design.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/vectors/design_report.svg b/src/osdagbridge/desktop/resources/vectors/design_report.svg
new file mode 100644
index 00000000..5c224e91
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/design_report.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/vectors/input_dock_active_light.svg b/src/osdagbridge/desktop/resources/vectors/input_dock_active_light.svg
new file mode 100644
index 00000000..70151046
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/input_dock_active_light.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/osdagbridge/desktop/resources/vectors/input_dock_inactive_light.svg b/src/osdagbridge/desktop/resources/vectors/input_dock_inactive_light.svg
new file mode 100644
index 00000000..32bfa48d
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/input_dock_inactive_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/vectors/inputs_label_light.svg b/src/osdagbridge/desktop/resources/vectors/inputs_label_light.svg
new file mode 100644
index 00000000..4306a92a
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/inputs_label_light.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/osdagbridge/desktop/resources/vectors/lock_close.svg b/src/osdagbridge/desktop/resources/vectors/lock_close.svg
new file mode 100644
index 00000000..8495792d
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/lock_close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/vectors/lock_open.svg b/src/osdagbridge/desktop/resources/vectors/lock_open.svg
new file mode 100644
index 00000000..5f22789a
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/lock_open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/vectors/lock_open_light.svg b/src/osdagbridge/desktop/resources/vectors/lock_open_light.svg
new file mode 100644
index 00000000..5f22789a
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/lock_open_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/vectors/logs_dock_active_light.svg b/src/osdagbridge/desktop/resources/vectors/logs_dock_active_light.svg
new file mode 100644
index 00000000..5d23c855
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/logs_dock_active_light.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/osdagbridge/desktop/resources/vectors/logs_dock_inactive_light.svg b/src/osdagbridge/desktop/resources/vectors/logs_dock_inactive_light.svg
new file mode 100644
index 00000000..56e27348
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/logs_dock_inactive_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/vectors/output_dock_active_light.svg b/src/osdagbridge/desktop/resources/vectors/output_dock_active_light.svg
new file mode 100644
index 00000000..7ef0439c
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/output_dock_active_light.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/osdagbridge/desktop/resources/vectors/output_dock_inactive_light.svg b/src/osdagbridge/desktop/resources/vectors/output_dock_inactive_light.svg
new file mode 100644
index 00000000..a8e65d4a
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/output_dock_inactive_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/resources/vectors/outputs_label_light.svg b/src/osdagbridge/desktop/resources/vectors/outputs_label_light.svg
new file mode 100644
index 00000000..cfddf439
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/outputs_label_light.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/osdagbridge/desktop/resources/vectors/save.svg b/src/osdagbridge/desktop/resources/vectors/save.svg
new file mode 100644
index 00000000..c86072a9
--- /dev/null
+++ b/src/osdagbridge/desktop/resources/vectors/save.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/__init__.py b/src/osdagbridge/desktop/ui/__init__.py
deleted file mode 100644
index 511085aa..00000000
--- a/src/osdagbridge/desktop/ui/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""UI package (Qt .ui files go here)."""
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/resources.qrc b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/resources.qrc
new file mode 100644
index 00000000..a876dcf2
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/resources.qrc
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+ vectors/arrow_down_light.svg
+ vectors/arrow_down_dark.svg
+ vectors/arrow_up_light.svg
+ vectors/arrow_up_dark.svg
+
+ themes/lightstyle.qss
+
+ vectors/design.svg
+ vectors/save.svg
+ vectors/checked.svg
+ vectors/design_report.svg
+ vectors/lock_open.svg
+ vectors/lock_close.svg
+ vectors/Osdag_logo.svg
+
+ vectors/inputs_label_light.svg
+ vectors/input_dock_active_light.svg
+ vectors/input_dock_inactive_light.svg
+
+ vectors/logs_dock_active_light.svg
+ vectors/logs_dock_inactive_light.svg
+
+ vectors/output_dock_active_light.svg
+ vectors/output_dock_inactive_light.svg
+ vectors/outputs_label_light.svg
+
+
+
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/resources_rc.py b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/resources_rc.py
new file mode 100644
index 00000000..79a25e66
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/resources_rc.py
@@ -0,0 +1,1471 @@
+# Resource object code (Python 3)
+# Created by: object code
+# Created by: The Resource Compiler for Qt version 6.9.2
+# WARNING! All changes made in this file will be lost!
+
+from PySide6 import QtCore
+
+qt_resource_data = b"\
+\x00\x00\x04\xe7\
+\x00\
+\x00\x15\xe8x\xda\xd5X]O\xdb<\x14\xbeG\xe2?\
+X\xea\xc5>\xb4\xae-\x85\x8e\x05\xbd\x17\xfd\x82!\x15\
+\x18\xb4\xdb\xc4n\x90\x93\xb8\xad\x85\x1bg\x8eC\xe1\x9d\
+\xf8\xef\xef\xb1\x9b\xef&n`\xef\xcdb\x0du\xb6s\
+\xce\xe3\xc7\x8f\xcf9N\xeb=\xfa\xe7E\xcf\xfe\x1eB\
+\xe8j:\xea\x9f\xa1\xb3o\xe7h:\xbb\x9d\x8c\xa7_\
+\xc6\xe3\x19j\xa2\xab\x9b\xb3\xfe\xe5\xf9\xcf\xf1\x08\x0dn\
+\xd1\xf4\xebxx~z><\x9f\xdd\xeaw^\xe6\x06\
+\xbdo\xed\xef\xed\xef\xb5^\x05\xaf\xf3\x11\x9dM\xae\x06\
+\xfdI\x04\x0f\xbd\x9d\x10\x1cH4\xf5\x89C\xe7\xd4y\
+\xf7Z@\xd7\x17\x98z?\xa8\xe7\xf25\xfa\xad\x8d \
+\x1b;\xf7\x0b\xc1C\xcfm:\x9cqa\xa1\xc6\xfcP\
+\xb5\x93h\x9c\x0b\x97@o\xc7\x7fD\x01g\xd4E\x8d\
+\xcfm<\xeft\xa3\xf1\x15\x16\x0b\xeaY\xa8\xed?F\
+=>v]\xea-\xe2\xaeg\xc5\xc3\xf5\x8cs6\xa3\
+\xbe\xc1\xeb\xa9~\x22\x1bqg[?&(\xfd\xd3\x04\
+J\xe2\xf8\x00&\x1c$x\xe6\xdc\x93\xcd\x80\xfeK\xe0\
+\xcd\xb4wc\xac)\xb0K\xc3 \x0b\xff\x97/\xb8O\
+\x84|jbF\x17\xde\x8ax\xd2B}\xf5\xf3\xfb\x10\
+~\x13\xb1Y\xd3\xf5\xd4gT\xc2\x7f-k\x89=\x97\
+\x11\xc3\xd2Fm\xd5b.@\x13g\x8c\xdb\x98\xa1\xe1\
+\x928\xf76\x7fDS\xf9\x04\x06\xf4\x0e\xe9\xbe\x01\xf4\
+\xfd\xdeF\x9f\x82\x8c-w\xbb\xf1\xe2\x03\x1f;z\xf1\
+\xbd\x98\xf5\xc4\x94e\xc1\x8eS\x07K.b\xabk\xea\
+\xca%X\xec%\x16\x97\x84.\x962\xd7U\xc2wW\
+?\xe5\x14v\xd3\x17K4\xa5\x9fj`\xd6\x92?\x90\
+\x04^\x89\xe7#\xfd\x18\x0c8\xaa\x8f\xb8&]\xc7\x18\
+\xea,\x8e\xae\xf0\x028\x0f\x05{k\xb5\x1e\x88\x03.\
+\x82V\xe4\xe3c\xf0\xb0x\x97H\xfb\x82x\xe1\x00\x8b\
+\x86$+\x9faI\xee|x\xf3\x0et\x13\xde\xd9X\
+\x98\x14\x7f\xa8\x9aI\xf1\xdbGi\x877 \x04\x06b\
+\x9f\xc9\xebG\xb0\xc8\x8czR,\x16\x92\x02{\xa0\x1d\
+\x01\xd26\x9d\x8c\xba\xae\xad\x800 \xabl\x1f\xb2g\
+\xbc\xb69_\x90 \xa8\xb06>V\xad\x965\xa4\xc7\
+\xebF\x9f\x12m$G\xb8\x84\xa1CC\xe8\xab\x85\xab\
+z\xd7L\xe2\xc8F\x86\xce+\xfc\x19\xb6*\xa1e\xdc\
+;m\x17h)\x9c\xf7\xfa>\x03\x022\xcb\x06\xa1$\
+\xe4\x94\x0a\xb3\x01\x9e\xa1\xe5\xd2L\x93\x91\xb9\xb42\xb1\
+=\xea\x16\x1bC[\xfd\x92\xfb\xd9\xd0\x1e\xf5\xda\x5cJ\
+\xbez\xf9&i/M,D\x9a=\xa38z\xbc\x15\
+F\x8f\xd3\xdc\x07\xf1~\x04)\x05r\xae\x87\x94I\x14\
+\xa4\xd1\xbe\x5c\x95\x9bXU3\xebeephN\xc2\
+\x06\xad\x1d\xab\x08\xd13\xe4\x97\x18\x86\xc7=RH\xfd\
+\x9dR\x17\xe6H\x90[B&@\x97K\xed \xc7\xe6\
++*\xaa.TT\xe3\xcb\xf1\x0d\x94T\x83o\xb3\xd9\
+\xd5eRY\x0dp@\xd0\x84<\x10\xf6\xfa\xaa\xeak\
+\x18,\x07!\x88\xca\xab>J\xeb%\x90\x92_\xaf\xcd\
+`Vv\x17\xd7\x91xl\xce\xdc\x13T\xca\xc4\x91!\
+;g\xed\xe5C\x7f\xaa\x0bI\x1e\xe5\xa6\xb0\xb1\x90\x93\
+)gr\xab($\xe3\xed\xb0\x90\xdb\xbd]\x02-\x10\
+\xb0\xe5\xac\x10\xe2K\xc81\xb3YM\xc4\xc6\xd5\x0f\xea\
+.\x88l\x0c\xc3\x00N\xfd\x8cJF\x06*-\xa3\xdd\
+\xf5\xaf\x0a\x0d\x13l\x13\xd6\xd0\xaf\xe9\x9f\x05\x98\xc6|\
+m\xce\xb5\xa9\xf5\x09_\xf0\x9cqs\x82.\xadg\xb6\
+\xc3@Zxo\x88n\x5cP\x8f\xae`\xc6.\xa9V\
+{,-\xc7\xdb\xbbk\xea,\xb8\x9ey\xfa\x0e\xd4;\
+\xa59\xef\xa8V\xc3Pee\x91\x98\xc2=\xd5JM\
+\xe1\xc7\x1b\x02r\x12\x7f\x1b\x97\x05\xdc\x7f\xc2f\xd1\xd4\
+\x9f\xf09d<\xf8\xcb\xa8\xcc@\xde\xc9\x22\xf9t\xe8\
+t\x9d\xaa\xd3k4\xbd\x9bU\xa7\xdd\xfd|`G\xf1\
+$\x8au\x03]\xe1L\xa8G\xea\x04\xf1g\x95U+\
+\x12\x1c\x84\xa6\xa09\xe2\xce\xbd!\x09\xea\x1c\x18yf\
+0\xff\xce\x85\xf9\xe8\xba\x22\xa2\xa5%\xf7\x81j\xb5\xae\
+?\xdd\xfc\x96\xe5\xf2d\xd5%\xff\xb9\x14\xd4\x0c\xf2\xdf\
+\xd8\xa5\xd2\x84\xebX\xb5\x9aW\x01\xedw\x8eW\x94=\
+Y\xe8\xcd\x90\x87\x82\x82\x14.\xc9\xfa\xcd\x07\xb4\xe2\x1e\
+W\xd7rb\xfc\x10Q\xa7\xdc/_\xcb\xd4\x11\x9c1\
+Hf\x16\xc8O\xc25\x98\x95_\x93\xda\xaa\x9d \xa8\
+\x9c&\x8a7\xb4\x10\xe4\x09\xc0\x08$\x97\x04\x05\xda\x88\
+\xaar\xe1|\x81U\xb5\x99\xa5\x95m\xe6CO\xf2\xaf\
+[uhL[\x90\xc2\x8e\xbe\x9c\x98\xe1\xf7\xdb\xaai\
+\xf8\x17\x04\xac\xaf\xaa\xf0G\x9fa\xe2\x05\xac\xa0\xd2\x8f\
+\xab\xf1n\xfb\xff\x05Zu\xe2\x01\xee\xa7\xb6j\x1a\xee\
+\x08\x8b{\x98\xa5\xe1B`\xdb\xbc\x13\xe3\xce\xa0\xdd\xe9\
+\x1d\x14\xd2dp\x98\x13\xff\x1fr\xc3Ah\xe7\x87\x8b\
+\x17,\xb5z\x05\xe8\x0buI\xa0\xbd\x87~K\xdfG\
+\xf4e&\xa8\x8fB\xdd\x8b\x0c(r\xc3e\xf4\xe8\xdb\
+C\x1e\x0a\x04t\x8cl\x22\xd7\x84x1+\xf0\xb7\x00\
+\xed??\x04\x81\x7f\
+\x00\x00\x00\xb2\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x222\
+4px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2224px\x22 fill=\
+\x22#90af13\x22>\
+\x00\x00\x02-\
+<\
+svg width=\x2240\x22 h\
+eight=\x2240\x22 viewB\
+ox=\x220 0 40 40\x22 f\
+ill=\x22none\x22 xmlns\
+=\x22http://www.w3.\
+org/2000/svg\x22>\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x0e;\
+<\
+svg width=\x22149\x22 \
+height=\x22141\x22 vie\
+wBox=\x220 0 149 14\
+1\x22 fill=\x22none\x22 x\
+mlns=\x22http://www\
+.w3.org/2000/svg\
+\x22>\x0d\x0a\x0d\x0a<\
+path d=\x22M130.212\
+ 67.3074L115.887\
+ 51.8725L130.121\
+ 36.1514L130.212\
+ 67.3074Z\x22 fill=\
+\x22black\x22/>\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x02\xfe\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\x00\x00\x02\x07\
+<\
+svg width=\x2240\x22 h\
+eight=\x2240\x22 viewB\
+ox=\x220 0 40 40\x22 f\
+ill=\x22none\x22 xmlns\
+=\x22http://www.w3.\
+org/2000/svg\x22>\x0d\x0a\
+\x0d\x0a\
+svg>\x0d\x0a\
+\x00\x00\x021\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#FFFFFF\x22>\
+\
+\x00\x00\x01\xa4\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\x00\x00\x02\xd3\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#FFFFFF\x22>\
+\x00\x00\x05O\
+<\
+svg width=\x2240\x22 h\
+eight=\x2240\x22 viewB\
+ox=\x220 0 40 40\x22 f\
+ill=\x22none\x22 xmlns\
+=\x22http://www.w3.\
+org/2000/svg\x22>\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x08+\
+<\
+svg width=\x2236\x22 h\
+eight=\x22922\x22 view\
+Box=\x220 0 36 922\x22\
+ fill=\x22none\x22 xml\
+ns=\x22http://www.w\
+3.org/2000/svg\x22>\
+\x0d\x0a\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x02\xde\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\x00\x00\x021\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\
+\x00\x00\x0aV\
+<\
+svg width=\x2236\x22 h\
+eight=\x22918\x22 view\
+Box=\x220 0 36 918\x22\
+ fill=\x22none\x22 xml\
+ns=\x22http://www.w\
+3.org/2000/svg\x22>\
+\x0d\x0a\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x05O\
+<\
+svg width=\x2240\x22 h\
+eight=\x2240\x22 viewB\
+ox=\x220 0 40 40\x22 f\
+ill=\x22none\x22 xmlns\
+=\x22http://www.w3.\
+org/2000/svg\x22>\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x02\xbb\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#FFFFFF\x22>\
+\x00\x00\x02/\
+<\
+svg width=\x2240\x22 h\
+eight=\x2240\x22 viewB\
+ox=\x220 0 40 40\x22 f\
+ill=\x22none\x22 xmlns\
+=\x22http://www.w3.\
+org/2000/svg\x22>\x0d\x0a\
+\x0d\x0a\x0d\x0a\
+\x00\x00\x01\x9d\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\x00\x00\x01\xa2\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#000000\x22>\
+\x00\x00\x025\
+<\
+svg xmlns=\x22http:\
+//www.w3.org/200\
+0/svg\x22 height=\x224\
+0px\x22 viewBox=\x220 \
+-960 960 960\x22 wi\
+dth=\x2240px\x22 fill=\
+\x22#FFFFFF\x22>\
+svg>\
+"
+
+qt_resource_name = b"\
+\x00\x07\
+\x0c\xba\xb6s\
+\x00v\
+\x00e\x00c\x00t\x00o\x00r\x00s\
+\x00\x06\
+\x07\xae\xc3\xc3\
+\x00t\
+\x00h\x00e\x00m\x00e\x00s\
+\x00\x0e\
+\x03\x9b1c\
+\x00l\
+\x00i\x00g\x00h\x00t\x00s\x00t\x00y\x00l\x00e\x00.\x00q\x00s\x00s\
+\x00\x0b\
+\x01d\x8d\x87\
+\x00c\
+\x00h\x00e\x00c\x00k\x00e\x00d\x00.\x00s\x00v\x00g\
+\x00\x1b\
+\x0aa\xaa'\
+\x00i\
+\x00n\x00p\x00u\x00t\x00_\x00d\x00o\x00c\x00k\x00_\x00a\x00c\x00t\x00i\x00v\x00e\
+\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x0e\
+\x06\xb1\x9b\x07\
+\x00O\
+\x00s\x00d\x00a\x00g\x00_\x00l\x00o\x00g\x00o\x00.\x00s\x00v\x00g\
+\x00\x0e\
+\x0d\xd6\xeag\
+\x00l\
+\x00o\x00c\x00k\x00_\x00c\x00l\x00o\x00s\x00e\x00.\x00s\x00v\x00g\
+\x00\x1a\
+\x0b}\x5cG\
+\x00l\
+\x00o\x00g\x00s\x00_\x00d\x00o\x00c\x00k\x00_\x00a\x00c\x00t\x00i\x00v\x00e\x00_\
+\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x13\
+\x0cPg\xa7\
+\x00a\
+\x00r\x00r\x00o\x00w\x00_\x00d\x00o\x00w\x00n\x00_\x00d\x00a\x00r\x00k\x00.\x00s\
+\x00v\x00g\
+\x00\x1e\
+\x08a\xb9\xc7\
+\x00o\
+\x00u\x00t\x00p\x00u\x00t\x00_\x00d\x00o\x00c\x00k\x00_\x00i\x00n\x00a\x00c\x00t\
+\x00i\x00v\x00e\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x0a\
+\x0f\xec\x03\xe7\
+\x00d\
+\x00e\x00s\x00i\x00g\x00n\x00.\x00s\x00v\x00g\
+\x00\x12\
+\x0aDDG\
+\x00a\
+\x00r\x00r\x00o\x00w\x00_\x00u\x00p\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\
+\x00g\
+\x00\x16\
+\x0e\x16='\
+\x00i\
+\x00n\x00p\x00u\x00t\x00s\x00_\x00l\x00a\x00b\x00e\x00l\x00_\x00l\x00i\x00g\x00h\
+\x00t\x00.\x00s\x00v\x00g\
+\x00\x0d\
+\x005w\xc7\
+\x00l\
+\x00o\x00c\x00k\x00_\x00o\x00p\x00e\x00n\x00.\x00s\x00v\x00g\
+\x00\x14\
+\x01\xd3}\xa7\
+\x00a\
+\x00r\x00r\x00o\x00w\x00_\x00d\x00o\x00w\x00n\x00_\x00l\x00i\x00g\x00h\x00t\x00.\
+\x00s\x00v\x00g\
+\x00\x17\
+\x0d\xa0\xcd'\
+\x00o\
+\x00u\x00t\x00p\x00u\x00t\x00s\x00_\x00l\x00a\x00b\x00e\x00l\x00_\x00l\x00i\x00g\
+\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x11\
+\x03\xf9t'\
+\x00a\
+\x00r\x00r\x00o\x00w\x00_\x00u\x00p\x00_\x00d\x00a\x00r\x00k\x00.\x00s\x00v\x00g\
+\
+\x00\x11\
+\x08\xb1e\xc7\
+\x00d\
+\x00e\x00s\x00i\x00g\x00n\x00_\x00r\x00e\x00p\x00o\x00r\x00t\x00.\x00s\x00v\x00g\
+\
+\x00\x1c\
+\x0ap\xe4'\
+\x00o\
+\x00u\x00t\x00p\x00u\x00t\x00_\x00d\x00o\x00c\x00k\x00_\x00a\x00c\x00t\x00i\x00v\
+\x00e\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x1c\
+\x01\xd8[\xc7\
+\x00l\
+\x00o\x00g\x00s\x00_\x00d\x00o\x00c\x00k\x00_\x00i\x00n\x00a\x00c\x00t\x00i\x00v\
+\x00e\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x1d\
+\x0e\xaf\xb9\xe7\
+\x00i\
+\x00n\x00p\x00u\x00t\x00_\x00d\x00o\x00c\x00k\x00_\x00i\x00n\x00a\x00c\x00t\x00i\
+\x00v\x00e\x00_\x00l\x00i\x00g\x00h\x00t\x00.\x00s\x00v\x00g\
+\x00\x08\
+\x08\xc8U\xe7\
+\x00s\
+\x00a\x00v\x00e\x00.\x00s\x00v\x00g\
+"
+
+qt_resource_struct = b"\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\
+\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x16\
+\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\
+\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x00/T\
+\x00\x00\x01\x9b\x13E\x17;\
+\x00\x00\x00H\x00\x00\x00\x00\x00\x01\x00\x00\x04\xeb\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x0026\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x03\x12\x00\x00\x00\x00\x00\x01\x00\x00I\x0a\
+\x00\x00\x01\x9a\xfc\x9f}A\
+\x00\x00\x02\x84\x00\x00\x00\x00\x00\x01\x00\x00>\xc5\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x01\x00\x00\x07\xd2\
+\x00\x00\x01\x9b\x13\xd0_\xa8\
+\x00\x00\x01J\x00\x00\x00\x00\x00\x01\x00\x00\x1dS\
+\x00\x00\x01\x9a\xfc\x9f}M\
+\x00\x00\x02\xac\x00\x00\x00\x00\x00\x01\x00\x00D\x18\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x03\x90\x00\x00\x00\x00\x00\x01\x00\x00LQ\
+\x00\x00\x01\x9b\x13E\x17;\
+\x00\x00\x01\xa6\x00\x00\x00\x00\x00\x01\x00\x00!\xd2\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x00d\x00\x00\x00\x00\x00\x01\x00\x00\x05\xa1\
+\x00\x00\x01\x9a\xfc\x9f};\
+\x00\x00\x02\xd4\x00\x00\x00\x00\x00\x01\x00\x00F\xd7\
+\x00\x00\x01\x9a\xfc\x9f}K\
+\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x01\x00\x00\x19\x13\
+\x00\x00\x01\x9a\xfc\x9f}A\
+\x00\x00\x01\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x1b\x1e\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x004k\
+\x00\x00\x01\x9b\x07\x002e\
+\x00\x00\x00\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x16\x11\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00'%\
+\x00\x00\x01\x9b\x07\x002a\
+\x00\x00\x03P\x00\x00\x00\x00\x00\x01\x00\x00J\xab\
+\x00\x00\x01\x9a\xfc\x9f}<\
+\x00\x00\x01\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x1e\xfb\
+\x00\x00\x01\x9b\x13E\x179\
+\x00\x00\x00&\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\
+\x00\x00\x01\x9b\x13\xd50\xdc\
+"
+
+def qInitResources():
+ QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+def qCleanupResources():
+ QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+qInitResources()
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/themes/darkstyle.qss b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/themes/darkstyle.qss
new file mode 100644
index 00000000..f9c6f9ac
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/themes/darkstyle.qss
@@ -0,0 +1,32 @@
+/* ==============================================
+ OSDAG GUI STYLESHEET - ORGANIZED BY SPECIFICITY
+ ============================================== */
+
+/* ==============================================
+ 1. GLOBAL STYLES (Least Specific)
+ ============================================== */
+* {
+ font-family: "Ubuntu Sans";
+}
+
+QMainWindow {
+ background-color: #282828;
+ border: 1px solid #6B7D20;
+ margin: 0px;
+ padding: 0px;
+}
+
+QTabWidget {
+ background-color: #333333;
+ border: 0px;
+}
+
+QToolTip {
+ background-color: #2B2B2B;
+ color: #D0D0D0;
+ border: 1px solid #6B7D20;
+ padding: 2px 2px;
+ font-size: 12px;
+ border-radius: 0px;
+ qproperty-alignment: AlignVCenter;
+}
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/themes/lightstyle.qss b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/themes/lightstyle.qss
new file mode 100644
index 00000000..452eeeb6
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/themes/lightstyle.qss
@@ -0,0 +1,243 @@
+/* ==============================================
+ OSDAG GUI STYLESHEET - ORGANIZED BY SPECIFICITY
+ ============================================== */
+
+/* ==============================================
+ 1. GLOBAL STYLES (Least Specific)
+ ============================================== */
+QMainWindow {
+ background-color: #f4f4f4;
+ border: 1px solid #90af13;
+ margin: 0px;
+ padding: 0px;
+}
+
+QToolTip {
+ background-color: #FFFFFF;
+ color: #000000;
+ border: 1px solid #90AF13;
+ padding: 2px 2px;
+ font-size: 12px;
+ border-radius: 0px;
+ qproperty-alignment: AlignVCenter;
+}
+QSplitter::handle {
+ background-color: #D0D0D0;
+}
+
+/* Global Checkbox Style */
+QCheckBox {
+ font-size: 10px;
+ color: #333;
+ spacing: 6px;
+}
+QCheckBox::indicator {
+ width: 16px;
+ height: 16px;
+ border: 1px solid #333333;
+ border-radius: 3px;
+ background-color: #ffffff;
+}
+QCheckBox::indicator:hover {
+ border: 1px solid #555555;
+}
+QCheckBox::indicator:checked {
+ background-color: #ffffff;
+ border: 1px solid #333333;
+ image: url(:/vectors/checked.svg);
+}
+
+QMenuBar#template_page_menu_bar {
+ background-color: #F4F4F4;
+ color: #000000;
+ padding: 0px;
+}
+QMenuBar#template_page_menu_bar::item {
+ padding: 5px 10px;
+ background: transparent;
+ border-radius: 0px;
+}
+QMenuBar#template_page_menu_bar::item:selected {
+ background: #FFFFFF;
+}
+QMenuBar#template_page_menu_bar::item:pressed {
+ background: #E8E8E8;
+}
+QMenuBar#template_page_menu_bar QMenu {
+ background-color: #FFFFFF;
+ border: 1px solid #D0D0D0;
+ border-radius: 4px;
+ padding: 0px;
+}
+QMenuBar#template_page_menu_bar QMenu::item {
+ padding: 5px;
+ color: #000000;
+ font-size: 11px;
+}
+QMenuBar#template_page_menu_bar QMenu::item:selected {
+ background-color: #E6F0FF;
+ border-radius: 3px;
+}
+QMenuBar#template_page_menu_bar QMenu::separator {
+ height: 1px;
+ background: #F0F0F0;
+ margin-left: 2px;
+ margin-right: 2px;
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+QMenuBar#template_page_menu_bar QMenu::right-arrow {
+ width: 8px;
+ height: 8px;
+}
+
+/* Dropdown menu style */
+QMenu {
+ background: #fff;
+ border: 1px solid #90AF13;
+ font-size: 14px;
+ padding: 0px;
+}
+
+QMenu::item {
+ padding: 8px 16px;
+ color: #333;
+ border: none;
+ margin: 1px;
+}
+
+QMenu::item:selected {
+ background: #90AF13;
+ color: #fff;
+ border-radius: 2px;
+}
+
+/* ==============================================
+ 3. GENERAL BUTTON STYLES (Base Level)
+ ============================================== */
+QPushButton {
+ background-color: white;
+ color: black;
+ font-weight: bold;
+ border-radius: 5px;
+ border: 1px solid black;
+ padding: 5px 14px;
+ text-align: center;
+}
+
+QPushButton:hover {
+ background-color: #90AF13;
+ border: 1px solid #90AF13;
+ color: white;
+}
+
+QPushButton:pressed {
+ color: black;
+ background-color: white;
+ border: 1px solid black;
+}
+
+QWidget#CustomTitleBar {
+ background-color: #f4f4f4;
+}
+QLabel#TitleLabel {
+ color: #000000;
+ padding: 0px;
+ background: transparent;
+}
+QLabel#LogoLabel {
+ background: transparent;
+ color: #ffffff;
+ font-size: 14px;
+}
+
+QToolButton#MinimizeButton {
+ background-color: transparent;
+ color: #000000;
+ border: 0px;
+ border-radius: 0px;
+ font-size: 16px;
+ border-radius: 0px;
+}
+
+QToolButton#MinimizeButton:hover {
+ background-color: #f1f1f1;
+}
+
+QToolButton#MinimizeButton:pressed {
+ background-color: #a6a6a6;
+}
+
+QToolButton#MaxRestoreButton {
+ background-color: transparent;
+ color: #000000;
+ border: 0px;
+ border-radius: 0px;
+ font-size: 16px;
+ border-radius: 0px;
+}
+
+QToolButton#MaxRestoreButton:hover {
+ background-color: #f1f1f1;
+}
+
+QToolButton#MaxRestoreButton:pressed {
+ background-color: #a6a6a6;
+}
+
+QToolButton#CloseButton {
+ background-color: transparent;
+ color: #000000;
+ border: 0px;
+ border-radius: 0px;
+ font-size: 16px;
+ border-radius: 0px;
+}
+
+QToolButton#CloseButton:hover {
+ background-color: #e74c3c;
+ color: #ffffff;
+}
+
+QToolButton#CloseButton:pressed {
+ background-color: #c0392b;
+}
+QWidget#BottomLine {
+ background-color: #90AF13;
+}
+/*=======================Logs-Dock===========================*/
+QWidget#logs_dock QLabel {
+ background-color: #F2F2F2;
+ color: #000000;
+ padding: 3px;
+ font-weight: bold;
+ font-size: 12px;
+}
+QWidget#logs_dock QTextEdit {
+ background-color: #F8F8F8;
+ border: 1px solid #D0D0D0;
+ font-family: 'Courier New', monospace;
+ font-size: 12px;
+ padding: 5px;
+ color: #000000;
+}
+QWidget#logs_dock QScrollBar:vertical {
+ background: #E0E0E0; /* Light grey for the scrollbar track */
+ width: 8px;
+ margin: 0px 0px 0px 3px;
+ border-radius: 2px;
+}
+QWidget#logs_dock QScrollBar::handle:vertical {
+ background: #A0A0A0; /* Medium grey for the scrollbar handle */
+ min-height: 30px;
+ border-radius: 2px;
+}
+QWidget#logs_dock QScrollBar::handle:vertical:hover {
+ background: #707070; /* Darker grey on hover for the handle */
+}
+QWidget#logs_dock QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
+ height: 0px; /* Hides the up/down arrows */
+}
+QWidget#logs_dock QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
+ background: none; /* Hides the area between handle and arrows */
+}
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/Osdag_logo.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/Osdag_logo.svg
new file mode 100644
index 00000000..1ee3a590
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/Osdag_logo.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_down_dark.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_down_dark.svg
new file mode 100644
index 00000000..2c10661b
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_down_dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_down_light.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_down_light.svg
new file mode 100644
index 00000000..52e93659
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_down_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_up_dark.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_up_dark.svg
new file mode 100644
index 00000000..34f7193e
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_up_dark.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_up_light.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_up_light.svg
new file mode 100644
index 00000000..494d7624
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/arrow_up_light.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/checked.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/checked.svg
new file mode 100644
index 00000000..829f9eda
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/checked.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/design.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/design.svg
new file mode 100644
index 00000000..a5085160
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/design.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/design_report.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/design_report.svg
new file mode 100644
index 00000000..5c224e91
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/design_report.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/input_dock_active_light.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/input_dock_active_light.svg
new file mode 100644
index 00000000..70151046
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/input_dock_active_light.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/input_dock_inactive_light.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/input_dock_inactive_light.svg
new file mode 100644
index 00000000..32bfa48d
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/input_dock_inactive_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/inputs_label_light.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/inputs_label_light.svg
new file mode 100644
index 00000000..4306a92a
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/inputs_label_light.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/lock_close.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/lock_close.svg
new file mode 100644
index 00000000..8495792d
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/lock_close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/lock_open.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/lock_open.svg
new file mode 100644
index 00000000..5f22789a
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/lock_open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/lock_open_light.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/lock_open_light.svg
new file mode 100644
index 00000000..5f22789a
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/lock_open_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/logs_dock_active_light.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/logs_dock_active_light.svg
new file mode 100644
index 00000000..5d23c855
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/logs_dock_active_light.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/logs_dock_inactive_light.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/logs_dock_inactive_light.svg
new file mode 100644
index 00000000..56e27348
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/logs_dock_inactive_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/output_dock_active_light.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/output_dock_active_light.svg
new file mode 100644
index 00000000..7ef0439c
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/output_dock_active_light.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/output_dock_inactive_light.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/output_dock_inactive_light.svg
new file mode 100644
index 00000000..a8e65d4a
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/output_dock_inactive_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/outputs_label_light.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/outputs_label_light.svg
new file mode 100644
index 00000000..cfddf439
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/outputs_label_light.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/save.svg b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/save.svg
new file mode 100644
index 00000000..c86072a9
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/__pycache__/resources/vectors/save.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/dialogs/additional_inputs.py b/src/osdagbridge/desktop/ui/dialogs/additional_inputs.py
new file mode 100644
index 00000000..a3a6f1e1
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/additional_inputs.py
@@ -0,0 +1,4766 @@
+"""
+Additional Inputs Widget for Highway Bridge Design
+Provides detailed input fields for manual bridge parameter definition
+"""
+import math
+import sys
+import os
+from PySide6.QtWidgets import (
+ QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QTabBar, QLabel, QLineEdit,
+ QComboBox, QGroupBox, QFormLayout, QPushButton, QScrollArea,
+ QCheckBox, QMessageBox, QSizePolicy, QSpacerItem, QStackedWidget,
+ QFrame, QGridLayout, QTableWidget, QTableWidgetItem, QHeaderView,
+ QTextEdit, QDialog, QSizePolicy, QSizeGrip
+)
+from PySide6.QtCore import Qt, Signal, QSize
+from PySide6.QtGui import QDoubleValidator, QIntValidator, QStandardItemModel
+
+from osdagbridge.core.bridge_components.super_structure.girder import properties as girder_properties
+from osdagbridge.core.utils.common import *
+from osdagbridge.desktop.ui.utils.custom_titlebar import CustomTitleBar
+from osdagbridge.desktop.ui.utils.rolled_section_preview import RolledSectionPreview
+
+# =================================================================================
+# CENTRALIZED STYLING
+# =================================================================================
+
+def get_combobox_style():
+ """Return the common stylesheet for dropdowns with the SVG icon from resources."""
+ return """
+ QComboBox{
+ padding: 1px 7px;
+ border: 1px solid black;
+ border-radius: 5px;
+ background-color: white;
+ color: black;
+ }
+ QComboBox::drop-down{
+ subcontrol-origin: padding;
+ subcontrol-position: top right;
+ border-left: 0px;
+ }
+ QComboBox::down-arrow{
+ image: url(:/vectors/arrow_down_light.svg);
+ width: 20px;
+ height: 20px;
+ margin-right: 8px;
+ }
+ QComboBox::down-arrow:on {
+ image: url(:/vectors/arrow_up_light.svg);
+ width: 20px;
+ height: 20px;
+ margin-right: 8px;
+ }
+ QComboBox QAbstractItemView{
+ background-color: white;
+ border: 1px solid black;
+ outline: none;
+ }
+ QComboBox QAbstractItemView::item{
+ color: black;
+ background-color: white;
+ border: none;
+ border: 1px solid white;
+ border-radius: 0;
+ padding: 2px;
+ }
+ QComboBox QAbstractItemView::item:hover{
+ border: 1px solid #90AF13;
+ background-color: #90AF13;
+ color: black;
+ }
+ QComboBox QAbstractItemView::item:selected{
+ background-color: #90AF13;
+ color: black;
+ border: 1px solid #90AF13;
+ }
+ QComboBox QAbstractItemView::item:selected:hover{
+ background-color: #90AF13;
+ color: black;
+ border: 1px solid #94b816;
+ }
+ QComboBox:disabled{
+ background: #f1f1f1;
+ color: #666;
+ }
+ """
+
+
+def get_lineedit_style():
+ """Return the shared stylesheet for line edits in the section inputs."""
+ return """
+ QLineEdit {
+ padding: 1px 7px;
+ border: 1px solid #070707;
+ border-radius: 6px;
+ background-color: white;
+ color: #000000;
+ font-weight: normal;
+ }
+ QLineEdit:disabled{
+ background: #f1f1f1;
+ color: #666;
+ }
+ QLineEdit:read-only{
+ background: #f6f6f6;
+ color: #555555;
+ }
+ QLineEdit:hover {
+ border: 1px solid #5d5d5d;
+ }
+ """
+
+
+def apply_field_style(widget):
+ """Apply the appropriate style to combo boxes and line edits."""
+ widget.setMinimumHeight(28)
+ if isinstance(widget, QComboBox):
+ widget.setStyleSheet(get_combobox_style())
+ elif isinstance(widget, QLineEdit):
+ widget.setStyleSheet(get_lineedit_style())
+
+
+def create_action_button_bar(parent=None):
+ """Create a standardized Defaults/Save bar with gray backing."""
+ frame = QFrame(parent)
+ frame.setObjectName("actionButtonBar")
+ frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ frame.setStyleSheet("""
+ QFrame#actionButtonBar {
+ background-color: #ededed;
+ border: 1px solid #c8c8c8;
+ border-radius: 6px;
+ }
+ QFrame#actionButtonBar QPushButton {
+ background-color: #ffffff;
+ color: #2f2f2f;
+ font-weight: 600;
+ border: 1px solid #8c8c8c;
+ border-radius: 4px;
+ padding: 6px 24px;
+ min-width: 120px;
+ }
+ QFrame#actionButtonBar QPushButton:hover {
+ background-color: #f6f6f6;
+ }
+ QFrame#actionButtonBar QPushButton:pressed {
+ background-color: #e0e0e0;
+ }
+ """)
+
+ layout = QHBoxLayout(frame)
+ layout.setContentsMargins(22, 10, 22, 10)
+ layout.setSpacing(12)
+ layout.addStretch()
+
+ defaults_button = QPushButton("Defaults", frame)
+ save_button = QPushButton("Save", frame)
+ layout.addWidget(defaults_button)
+ layout.addWidget(save_button)
+ layout.addStretch()
+
+ return frame, defaults_button, save_button
+
+
+SECTION_NAV_BUTTON_STYLE = """
+ QPushButton {
+ background-color: #f4f4f4;
+ border: 2px solid #d2d2d2;
+ border-radius: 12px;
+ padding: 20px 16px;
+ text-align: left;
+ font-weight: bold;
+ font-size: 12px;
+ color: #333333;
+ }
+ QPushButton:hover {
+ border-color: #b5b5b5;
+ }
+ QPushButton:checked {
+ background-color: #9ecb3d;
+ border-color: #7da523;
+ color: #ffffff;
+ }
+"""
+
+
+class CheckableComboBox(QComboBox):
+ """Multi-select combo box that keeps track of checked girders."""
+
+ checkedItemsChanged = Signal()
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ model = QStandardItemModel(self)
+ self.setModel(model)
+ self._updating_selection = False
+ model.itemChanged.connect(self._handle_item_changed)
+
+ def addItem(self, text, userData=None):
+ super().addItem(text, userData)
+ self._initialize_item(self.count() - 1)
+
+ def addItems(self, texts):
+ for text in texts:
+ self.addItem(text)
+
+ def _initialize_item(self, index):
+ item = self.model().item(index)
+ if not item:
+ return
+ item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
+ if item.text().lower() == "all":
+ item.setData(Qt.Checked, Qt.CheckStateRole)
+ else:
+ item.setData(Qt.Unchecked, Qt.CheckStateRole)
+
+ def _handle_item_changed(self, item):
+ if self._updating_selection or item is None:
+ return
+ self._updating_selection = True
+ try:
+ text = item.text().strip().lower()
+ if text == "all":
+ if item.checkState() == Qt.Checked:
+ self._uncheck_non_all_items()
+ else:
+ if item.checkState() == Qt.Checked:
+ self._uncheck_all_item()
+ finally:
+ self._updating_selection = False
+ self.checkedItemsChanged.emit()
+
+ def _uncheck_non_all_items(self):
+ model = self.model()
+ for row in range(model.rowCount()):
+ item = model.item(row)
+ if not item:
+ continue
+ if item.text().strip().lower() == "all":
+ continue
+ item.setCheckState(Qt.Unchecked)
+
+ def _uncheck_all_item(self):
+ model = self.model()
+ for row in range(model.rowCount()):
+ item = model.item(row)
+ if item and item.text().strip().lower() == "all":
+ item.setCheckState(Qt.Unchecked)
+ break
+
+ def checked_items(self, include_all=False):
+ model = self.model()
+ selected = []
+ all_checked = False
+ for row in range(model.rowCount()):
+ item = model.item(row)
+ if not item or item.checkState() != Qt.Checked:
+ continue
+ text = item.text()
+ if text.strip().lower() == "all":
+ all_checked = True
+ if include_all:
+ selected.append(text)
+ else:
+ selected.append(text)
+ if all_checked and not include_all:
+ return []
+ return [text for text in selected if text.strip().lower() != "all"]
+
+# =================================================================================
+# MAIN IMPLEMENTATION
+# =================================================================================
+
+class AdditionalInputs(QDialog):
+ """Main dialog for Additional Inputs with tabbed interface"""
+
+ def __init__(self, footpath_value="None", carriageway_width=7.5, parent=None):
+ super().__init__(parent)
+ self.setObjectName("AdditionalInputs")
+ self.resize(1024, 720)
+ self.setMinimumSize(900, 520)
+ self.setSizeGripEnabled(True)
+ self.footpath_value = footpath_value
+ self.carriageway_width = carriageway_width
+ self.init_ui()
+ self.setStyleSheet("""
+ QDialog {
+ background-color: #ffffff;
+ border: 1px solid #90AF13;
+ }
+ """)
+
+ def setupWrapper(self):
+ self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowSystemMenuHint)
+
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(1, 1, 1, 1)
+ main_layout.setSpacing(0)
+
+ self.title_bar = CustomTitleBar()
+ self.title_bar.setTitle("Additional Inputs")
+ main_layout.addWidget(self.title_bar)
+
+ self.content_widget = QWidget(self)
+ main_layout.addWidget(self.content_widget, 1)
+
+ size_grip = QSizeGrip(self)
+ size_grip.setFixedSize(16, 16)
+
+ overlay = QHBoxLayout()
+ overlay.setContentsMargins(0, 0, 4, 4)
+ overlay.addStretch(1)
+ overlay.addWidget(size_grip, 0, Qt.AlignBottom | Qt.AlignRight)
+ main_layout.addLayout(overlay)
+
+ def init_ui(self):
+ self.setupWrapper()
+
+ main_layout = QVBoxLayout(self.content_widget)
+ main_layout.setContentsMargins(5, 5, 5, 5)
+
+ # Main tab widget
+ self.tabs = QTabWidget()
+ self.tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ self.stretching_tab_bar = QTabBar()
+ self.stretching_tab_bar.setElideMode(Qt.ElideRight)
+ self.tabs.setTabBar(self.stretching_tab_bar)
+ self.tabs.setStyleSheet("""
+ QTabWidget::pane {
+ border: 1px solid #d1d1d1;
+ background-color: #ffffff;
+ border-radius: 6px;
+ }
+ QTabBar::tab {
+ font-weight: bold;
+ font-size: 12px;
+ background: #ffffff;
+ color: #3a3a3a;
+ border: 1px solid #d1d1d1;
+ padding: 10px 22px;
+ }
+ QTabBar::tab:selected {
+ background: #90AF13;
+ color: #ffffff;
+ border: 1px solid #90AF13;
+ }
+ QTabBar::tab:hover {
+ background: #90AF13;
+ color: #ffffff;
+ }
+ """)
+
+ # Sub-Tab 1: Typical Section Details
+ self.typical_section_tab = TypicalSectionDetailsTab(self.footpath_value, self.carriageway_width)
+ self.tabs.addTab(self.typical_section_tab, "Typical Section Details")
+
+ # Sub-Tab 2: Member Properties
+ self.section_properties_tab = SectionPropertiesTab()
+ self.tabs.addTab(self.section_properties_tab, "Member Properties")
+
+ # Sub-Tab 3: Loading
+ self.loading_tab = LoadingTab()
+ self.tabs.addTab(self.loading_tab, "Loading")
+
+ # Sub-Tab 4: Support Conditions
+ support_tab = self._build_support_conditions_tab()
+ self.tabs.addTab(support_tab, "Support Conditions")
+
+ # Sub-Tab 5: Design Options
+ design_options_tab = self._build_design_options_tab()
+ self.tabs.addTab(design_options_tab, "Design Options")
+
+ # Sub-Tab 6: Design Options (Cont.)
+ analysis_design_tab = self.create_placeholder_tab(
+ "Design Options (Cont.)",
+ "This tab will contain:\n\n" +
+ "• Analysis Method\n" +
+ "• Design Code Options\n" +
+ "• Safety Factors\n" +
+ "• Other Design Parameters\n\n" +
+ "Implementation in progress..."
+ )
+ self.tabs.addTab(analysis_design_tab, "Design Options (Cont.)")
+
+ main_layout.addWidget(self.tabs)
+
+
+ action_bar, self.defaults_button, self.save_button = create_action_button_bar()
+ self.defaults_button.clicked.connect(lambda: self._show_placeholder_message("Defaults"))
+ self.save_button.clicked.connect(lambda: self._show_placeholder_message("Save"))
+ main_layout.addSpacing(6)
+ main_layout.addWidget(action_bar)
+
+ def accept(self):
+ girder_tab = getattr(getattr(self, "section_properties_tab", None), "girder_tab", None)
+ if girder_tab and not girder_tab.validate_member_properties():
+ return
+ super().accept()
+
+ def _show_placeholder_message(self, action_name):
+ """Show placeholder message for action buttons"""
+ QMessageBox.information(self, action_name, "This action will be available in an upcoming update.")
+
+ def _build_support_conditions_tab(self):
+ """Build the Support Conditions tab matching reference design"""
+ widget = QWidget()
+ widget.setStyleSheet("background-color: #f5f5f5;")
+
+ main_layout = QVBoxLayout(widget)
+ main_layout.setContentsMargins(12, 12, 12, 12)
+ main_layout.setSpacing(12)
+
+ # Main card
+ card = QFrame()
+ card.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 10px; background-color: #ffffff; }")
+ card_layout = QVBoxLayout(card)
+ card_layout.setContentsMargins(16, 16, 16, 16)
+ card_layout.setSpacing(16)
+
+ label_style = "font-size: 11px; color: #3a3a3a; background: transparent; border: none;"
+ heading_style = "font-size: 12px; font-weight: 700; color: #2b2b2b; background: transparent; border: none;"
+ field_width = 120
+
+ # Support Condition section
+ support_title = QLabel("Support Condition*")
+ support_title.setStyleSheet(heading_style)
+ card_layout.addWidget(support_title)
+
+ support_grid = QGridLayout()
+ support_grid.setContentsMargins(0, 8, 0, 0)
+ support_grid.setHorizontalSpacing(12)
+ support_grid.setVerticalSpacing(12)
+ support_grid.setColumnMinimumWidth(0, 120)
+
+ # Left Support
+ lbl = QLabel("Left Support:")
+ lbl.setStyleSheet(label_style)
+ self.left_support_combo = QComboBox()
+ self.left_support_combo.addItems(["Fixed", "Pinned", "Roller"])
+ self.left_support_combo.setFixedWidth(field_width)
+ apply_field_style(self.left_support_combo)
+ support_grid.addWidget(lbl, 0, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ support_grid.addWidget(self.left_support_combo, 0, 1, Qt.AlignLeft)
+
+ # Right Support
+ lbl = QLabel("Right Support:")
+ lbl.setStyleSheet(label_style)
+ self.right_support_combo = QComboBox()
+ self.right_support_combo.addItems(["Fixed", "Pinned", "Roller"])
+ self.right_support_combo.setFixedWidth(field_width)
+ apply_field_style(self.right_support_combo)
+ support_grid.addWidget(lbl, 1, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ support_grid.addWidget(self.right_support_combo, 1, 1, Qt.AlignLeft)
+
+ card_layout.addLayout(support_grid)
+
+ # Bearing Length section
+ bearing_title = QLabel("Bearing length*")
+ bearing_title.setStyleSheet(heading_style)
+ card_layout.addWidget(bearing_title)
+
+ bearing_grid = QGridLayout()
+ bearing_grid.setContentsMargins(0, 8, 0, 0)
+ bearing_grid.setHorizontalSpacing(12)
+ bearing_grid.setVerticalSpacing(12)
+ bearing_grid.setColumnMinimumWidth(0, 120)
+
+ lbl = QLabel("Bearing Length Value")
+ lbl.setStyleSheet(label_style)
+ self.bearing_length_input = QLineEdit()
+ self.bearing_length_input.setText("0")
+ self.bearing_length_input.setFixedWidth(field_width)
+ apply_field_style(self.bearing_length_input)
+ bearing_grid.addWidget(lbl, 0, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ bearing_grid.addWidget(self.bearing_length_input, 0, 1, Qt.AlignLeft)
+
+ card_layout.addLayout(bearing_grid)
+ card_layout.addStretch()
+
+ main_layout.addWidget(card)
+ main_layout.addStretch()
+
+ return widget
+
+ def _build_design_options_tab(self):
+ """Build the Design Options tab matching reference design"""
+ widget = QWidget()
+ widget.setStyleSheet("background-color: #f5f5f5;")
+
+ main_layout = QVBoxLayout(widget)
+ main_layout.setContentsMargins(12, 12, 12, 12)
+ main_layout.setSpacing(12)
+
+ # Main card
+ card = QFrame()
+ card.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 10px; background-color: #ffffff; }")
+ card_layout = QVBoxLayout(card)
+ card_layout.setContentsMargins(16, 16, 16, 16)
+ card_layout.setSpacing(12)
+
+ label_style = "font-size: 11px; color: #3a3a3a; background: transparent; border: none;"
+ heading_style = "font-size: 12px; font-weight: 700; color: #2b2b2b; background: transparent; border: none;"
+ field_width = 120
+
+ # Deck Design section
+ deck_title = QLabel("Deck Design:")
+ deck_title.setStyleSheet(heading_style)
+ card_layout.addWidget(deck_title)
+
+ deck_grid = QGridLayout()
+ deck_grid.setContentsMargins(0, 4, 0, 0)
+ deck_grid.setHorizontalSpacing(12)
+ deck_grid.setVerticalSpacing(10)
+ deck_grid.setColumnMinimumWidth(0, 120)
+
+ lbl = QLabel("Reinforcement Size:")
+ lbl.setStyleSheet(label_style)
+ self.reinforcement_size_combo = QComboBox()
+ self.reinforcement_size_combo.addItems(["8 mm", "10 mm", "12 mm", "16 mm", "20 mm"])
+ self.reinforcement_size_combo.setFixedWidth(field_width)
+ apply_field_style(self.reinforcement_size_combo)
+ deck_grid.addWidget(lbl, 0, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ deck_grid.addWidget(self.reinforcement_size_combo, 0, 1, Qt.AlignLeft)
+
+ card_layout.addLayout(deck_grid)
+
+ # Shear Studs section
+ shear_title = QLabel("Shear Studs:")
+ shear_title.setStyleSheet(heading_style)
+ card_layout.addWidget(shear_title)
+
+ shear_grid = QGridLayout()
+ shear_grid.setContentsMargins(0, 4, 0, 0)
+ shear_grid.setHorizontalSpacing(12)
+ shear_grid.setVerticalSpacing(10)
+ shear_grid.setColumnMinimumWidth(0, 120)
+
+ # Material
+ lbl = QLabel("Material:")
+ lbl.setStyleSheet(label_style)
+ self.shear_stud_material_input = QLineEdit()
+ self.shear_stud_material_input.setFixedWidth(field_width)
+ apply_field_style(self.shear_stud_material_input)
+ shear_grid.addWidget(lbl, 0, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ shear_grid.addWidget(self.shear_stud_material_input, 0, 1, Qt.AlignLeft)
+
+ # Diameter
+ lbl = QLabel("Diameter (mm):")
+ lbl.setStyleSheet(label_style)
+ self.shear_stud_diameter_input = QLineEdit()
+ self.shear_stud_diameter_input.setFixedWidth(field_width)
+ apply_field_style(self.shear_stud_diameter_input)
+ shear_grid.addWidget(lbl, 1, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ shear_grid.addWidget(self.shear_stud_diameter_input, 1, 1, Qt.AlignLeft)
+
+ # Height
+ lbl = QLabel("Height (mm):")
+ lbl.setStyleSheet(label_style)
+ self.shear_stud_height_input = QLineEdit()
+ self.shear_stud_height_input.setFixedWidth(field_width)
+ apply_field_style(self.shear_stud_height_input)
+ shear_grid.addWidget(lbl, 2, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ shear_grid.addWidget(self.shear_stud_height_input, 2, 1, Qt.AlignLeft)
+
+ card_layout.addLayout(shear_grid)
+ card_layout.addStretch()
+
+ main_layout.addWidget(card)
+ main_layout.addStretch()
+
+ return widget
+
+ def create_placeholder_tab(self, title, description):
+ """Create a styled placeholder tab with title and description"""
+ widget = QWidget()
+ widget.setStyleSheet("background-color: white;")
+
+ layout = QVBoxLayout(widget)
+ layout.setAlignment(Qt.AlignCenter)
+ layout.setContentsMargins(40, 40, 40, 40)
+
+ # Icon or visual indicator
+ icon_label = QLabel("🚧")
+ icon_label.setStyleSheet("font-size: 48px;")
+ icon_label.setAlignment(Qt.AlignCenter)
+ layout.addWidget(icon_label)
+
+ # Title
+ title_label = QLabel(title)
+ title_label.setStyleSheet("""
+ font-size: 18px;
+ font-weight: bold;
+ color: #333;
+ margin-top: 20px;
+ margin-bottom: 10px;
+ """)
+ title_label.setAlignment(Qt.AlignCenter)
+ layout.addWidget(title_label)
+
+ # Status
+ status_label = QLabel("Under Development")
+ status_label.setStyleSheet("""
+ font-size: 14px;
+ color: #f39c12;
+ font-weight: bold;
+ margin-bottom: 20px;
+ """)
+ status_label.setAlignment(Qt.AlignCenter)
+ layout.addWidget(status_label)
+
+ # Description
+ desc_label = QLabel(description)
+ desc_label.setStyleSheet("""
+ font-size: 12px;
+ color: #666;
+ line-height: 1.6;
+ """)
+ desc_label.setAlignment(Qt.AlignCenter)
+ desc_label.setWordWrap(True)
+ desc_label.setMaximumWidth(600)
+ layout.addWidget(desc_label)
+
+ layout.addStretch()
+
+ return widget
+
+ def update_footpath_value(self, footpath_value):
+ """Update footpath value across all tabs"""
+ self.footpath_value = footpath_value
+ self.typical_section_tab.update_footpath_value(footpath_value)
+
+# =================================================================================
+# SUB COMPONENTS
+# =================================================================================
+
+class TypicalSectionDetailsTab(QWidget):
+ """Sub-tab for Typical Section Details inputs"""
+
+ footpath_changed = Signal(str)
+
+ def __init__(self, footpath_value="None", carriageway_width=7.5, parent=None):
+ super().__init__(parent)
+ self.footpath_value = footpath_value
+ self.carriageway_width = carriageway_width
+ self.updating_fields = False
+ self.init_ui()
+
+ def style_input_field(self, field):
+ apply_field_style(field)
+
+ def style_group_box(self, group_box):
+ group_box.setStyleSheet("""
+ QGroupBox {
+ font-weight: bold;
+ border: 2px solid #d0d0d0;
+ border-radius: 6px;
+ margin-top: 12px;
+ padding-top: 15px;
+ background-color: #f9f9f9;
+ }
+ QGroupBox::title {
+ subcontrol-origin: margin;
+ subcontrol-position: top left;
+ left: 10px;
+ padding: 0 5px;
+ background-color: white;
+ color: #4a7ba7;
+ }
+ """)
+
+ def _create_section_card(self, title):
+ card = QFrame()
+ card.setObjectName("sectionCard")
+ card.setStyleSheet("""
+ QFrame#sectionCard {
+ background-color: #f5f5f5;
+ border: none;
+ }
+ """)
+ card_layout = QVBoxLayout(card)
+ card_layout.setContentsMargins(0, 0, 0, 0)
+ card_layout.setSpacing(12)
+
+ title_label = QLabel(title)
+ title_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #000;")
+ card_layout.addWidget(title_label)
+
+ return card, card_layout
+
+ def init_ui(self):
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(10, 10, 10, 10)
+ main_layout.setSpacing(0)
+
+ diagram_widget = QWidget()
+ diagram_widget.setStyleSheet("""
+ QWidget {
+ background: transparent;
+ border: 1px solid #b0b0b0;
+ border-radius: 8px;
+ }
+ """)
+ diagram_widget.setMinimumHeight(150)
+ diagram_widget.setMaximumHeight(200)
+ diagram_layout = QVBoxLayout(diagram_widget)
+ diagram_layout.setContentsMargins(20, 20, 20, 20)
+ diagram_layout.setAlignment(Qt.AlignCenter)
+
+ diagram_label = QLabel("Typical Section Details\nDiagram")
+ diagram_label.setAlignment(Qt.AlignCenter)
+ diagram_label.setStyleSheet("""
+ QLabel {
+ background-color: transparent;
+ border: none;
+ padding: 20px;
+ font-size: 13px;
+ color: #333;
+ }
+ """)
+ diagram_layout.addWidget(diagram_label)
+
+ main_layout.addWidget(diagram_widget)
+ main_layout.addSpacing(10)
+
+ input_container = QWidget()
+ input_container.setStyleSheet("QWidget { background-color: white; }")
+ input_layout = QVBoxLayout(input_container)
+ input_layout.setContentsMargins(0, 0, 0, 0)
+ input_layout.setSpacing(0)
+
+ self.input_tabs = QTabWidget()
+ self.input_tabs.setStyleSheet("""
+ QTabWidget::pane {
+ border: 1px solid #b0b0b0;
+ border-top: none;
+ background-color: #f5f5f5;
+ border-radius: 0px 0px 8px 8px;
+ }
+ QTabBar::tab {
+ background-color: #e8e8e8;
+ color: #555;
+ padding: 10px 20px;
+ border: 1px solid #b0b0b0;
+ border-bottom: none;
+ border-right: none;
+ font-size: 11px;
+ min-width: 80px;
+ }
+ QTabBar::tab:last {
+ border-right: 1px solid #b0b0b0;
+ }
+ QTabBar::tab:selected {
+ background-color: #90AF13;
+ color: white;
+ font-weight: bold;
+ border: 1px solid #90AF13;
+ border-bottom: none;
+ }
+ QTabBar::tab:hover:!selected {
+ background-color: #d0d0d0;
+ }
+ """)
+
+ self.create_layout_tab()
+ self.create_crash_barrier_tab()
+ self.create_median_tab()
+ self.create_railing_tab()
+ self.create_wearing_course_tab()
+ self.create_lane_details_tab()
+
+ input_layout.addWidget(self.input_tabs)
+ main_layout.addWidget(input_container)
+
+ self.deck_thickness.textChanged.connect(self.update_footpath_thickness)
+ self.recalculate_girders()
+
+ def create_layout_tab(self):
+ layout_widget = QWidget()
+ layout_widget.setStyleSheet("background-color: #f5f5f5;")
+ layout_layout = QVBoxLayout(layout_widget)
+ layout_layout.setContentsMargins(18, 6, 18, 12)
+ layout_layout.setSpacing(0)
+
+ title_label = QLabel("Inputs:")
+ title_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #000;")
+ layout_layout.addWidget(title_label)
+ layout_layout.addSpacing(8)
+
+ grid = QGridLayout()
+ grid.setHorizontalSpacing(24)
+ grid.setVerticalSpacing(10)
+ grid.setColumnStretch(1, 1)
+ grid.setColumnStretch(3, 1)
+ grid.setContentsMargins(0, 0, 0, 0)
+
+ def _label(text):
+ lbl = QLabel(text)
+ lbl.setStyleSheet("font-size: 11px; color: #000;")
+ lbl.setMinimumWidth(180)
+ return lbl
+
+ self.girder_spacing = QLineEdit()
+ self.girder_spacing.setValidator(QDoubleValidator(0.01, 50.0, 3))
+ self.girder_spacing.setText(str(DEFAULT_GIRDER_SPACING))
+ self.style_input_field(self.girder_spacing)
+ self.girder_spacing.textChanged.connect(self.on_girder_spacing_changed)
+
+ self.no_of_girders = QLineEdit()
+ self.no_of_girders.setValidator(QIntValidator(2, 100))
+ self.style_input_field(self.no_of_girders)
+ self.no_of_girders.textChanged.connect(self.on_no_of_girders_changed)
+
+ grid.addWidget(_label("Girder Spacing (m):"), 0, 0, Qt.AlignLeft)
+ grid.addWidget(self.girder_spacing, 0, 1)
+ grid.addWidget(_label("No. of Girders:"), 0, 2, Qt.AlignLeft)
+ grid.addWidget(self.no_of_girders, 0, 3)
+
+ self.deck_overhang = QLineEdit()
+ self.deck_overhang.setValidator(QDoubleValidator(0.0, 10.0, 3))
+ self.deck_overhang.setText(str(DEFAULT_DECK_OVERHANG))
+ self.style_input_field(self.deck_overhang)
+ self.deck_overhang.textChanged.connect(self.on_deck_overhang_changed)
+
+ values_adjusted_label = QLabel("Values adjusted for:")
+ values_adjusted_label.setStyleSheet("font-size: 11px; color: #5b5b5b; font-style: italic;")
+
+ grid.addWidget(_label("Deck Overhang Width (m):"), 1, 0, Qt.AlignLeft)
+ grid.addWidget(self.deck_overhang, 1, 1)
+ #grid.addWidget(values_adjusted_label, 1, 2, 1, 2, Qt.AlignLeft)
+
+ self.overall_bridge_width_display = QLineEdit()
+ self.style_input_field(self.overall_bridge_width_display)
+ self.overall_bridge_width_display.setReadOnly(True)
+ self.overall_bridge_width_display.setEnabled(False)
+
+ grid.addWidget(_label("Overall Bridge Width (m):"), 2, 0, Qt.AlignLeft)
+ grid.addWidget(self.overall_bridge_width_display, 2, 1)
+
+ self.deck_thickness = QLineEdit()
+ self.deck_thickness.setValidator(QDoubleValidator(0.0, 500.0, 0))
+ self.style_input_field(self.deck_thickness)
+
+ self.footpath_thickness = QLineEdit()
+ self.footpath_thickness.setValidator(QDoubleValidator(0.0, 500.0, 0))
+ self.style_input_field(self.footpath_thickness)
+
+ grid.addWidget(_label("Deck Thickness (mm):"), 3, 0, Qt.AlignLeft)
+ grid.addWidget(self.deck_thickness, 3, 1)
+ grid.addWidget(_label("Footpath Thickness (mm):"), 4, 2, Qt.AlignLeft)
+ grid.addWidget(self.footpath_thickness, 4, 3)
+
+ self.footpath_width = QLineEdit()
+ self.footpath_width.setValidator(QDoubleValidator(MIN_FOOTPATH_WIDTH, 5.0, 3))
+ self.footpath_width.textChanged.connect(self.on_footpath_width_changed)
+ self.style_input_field(self.footpath_width)
+ self.footpath_width.setText(f"{MIN_FOOTPATH_WIDTH:.2f}")
+
+ grid.addWidget(_label("Footpath Width (m):"), 4, 0, Qt.AlignLeft)
+ grid.addWidget(self.footpath_width, 4, 1)
+
+ layout_layout.addLayout(grid)
+ # CHANGED: Add stretch at bottom to push content up
+ layout_layout.addStretch()
+
+ self.input_tabs.addTab(layout_widget, "Layout")
+ def create_crash_barrier_tab(self):
+ crash_widget = QWidget()
+ crash_widget.setStyleSheet("background-color: #f5f5f5;")
+ crash_layout = QVBoxLayout(crash_widget)
+ crash_layout.setContentsMargins(18, 6, 18, 12)
+ crash_layout.setSpacing(0)
+
+ card, card_layout = self._create_section_card("Crash Barrier Inputs:")
+ grid = QGridLayout()
+ grid.setContentsMargins(0, 0, 0, 0)
+ grid.setHorizontalSpacing(24)
+ grid.setVerticalSpacing(10)
+ grid.setColumnStretch(1, 1)
+
+ def add_row(row, label_text, widget):
+ label = QLabel(label_text)
+ label.setStyleSheet("font-size: 11px; color: #000;")
+ label.setMinimumWidth(210)
+ grid.addWidget(label, row, 0, Qt.AlignLeft)
+ grid.addWidget(widget, row, 1)
+
+ self.crash_barrier_type = QComboBox()
+ self.crash_barrier_type.addItems(VALUES_CRASH_BARRIER_TYPE)
+ self.style_input_field(self.crash_barrier_type)
+ self.crash_barrier_type.currentTextChanged.connect(self.on_crash_barrier_type_changed)
+ add_row(0, "Type:", self.crash_barrier_type)
+
+ self.crash_barrier_density = QLineEdit()
+ self.crash_barrier_density.setValidator(QDoubleValidator(0.0, 100.0, 2))
+ self.style_input_field(self.crash_barrier_density)
+ add_row(1, "Material Density (kN/m^3):", self.crash_barrier_density)
+
+ self.crash_barrier_width = QLineEdit()
+ self.crash_barrier_width.setValidator(QDoubleValidator(0.0, 2.0, 3))
+ self.crash_barrier_width.setText(str(DEFAULT_CRASH_BARRIER_WIDTH))
+ self.style_input_field(self.crash_barrier_width)
+ self.crash_barrier_width.textChanged.connect(self.recalculate_girders)
+ add_row(2, "Width (m):", self.crash_barrier_width)
+
+ self.crash_barrier_height = QLineEdit()
+ self.crash_barrier_height.setValidator(QDoubleValidator(0.0, 3.0, 3))
+ self.style_input_field(self.crash_barrier_height)
+ add_row(3, "Height (m):", self.crash_barrier_height)
+
+ self.crash_barrier_area = QLineEdit()
+ self.crash_barrier_area.setValidator(QDoubleValidator(0.0, 10.0, 4))
+ self.style_input_field(self.crash_barrier_area)
+ add_row(4, "Area (m^2):", self.crash_barrier_area)
+
+ card_layout.addLayout(grid)
+ crash_layout.addWidget(card)
+ crash_layout.addStretch()
+ self.input_tabs.addTab(crash_widget, "Crash Barrier")
+
+ def create_median_tab(self):
+ median_widget = QWidget()
+ median_widget.setStyleSheet("background-color: #f5f5f5;")
+ median_layout = QVBoxLayout(median_widget)
+ median_layout.setContentsMargins(18, 6, 18, 12)
+ median_layout.setSpacing(0)
+
+ card, card_layout = self._create_section_card("Median Inputs:")
+ grid = QGridLayout()
+ grid.setContentsMargins(0, 0, 0, 0)
+ grid.setHorizontalSpacing(24)
+ grid.setVerticalSpacing(10)
+ grid.setColumnStretch(1, 1)
+
+ def add_row(row, label_text, widget):
+ label = QLabel(label_text)
+ label.setStyleSheet("font-size: 11px; color: #000;")
+ label.setMinimumWidth(210)
+ grid.addWidget(label, row, 0, Qt.AlignLeft)
+ grid.addWidget(widget, row, 1)
+
+ self.median_type = QComboBox()
+ self.median_type.addItems(VALUES_MEDIAN_TYPE)
+ self.style_input_field(self.median_type)
+ add_row(0, "Type:", self.median_type)
+
+ self.median_density = QLineEdit()
+ self.median_density.setValidator(QDoubleValidator(0.0, 100.0, 2))
+ self.style_input_field(self.median_density)
+ add_row(1, "Material Density (kN/m^3):", self.median_density)
+
+ self.median_width = QLineEdit()
+ self.median_width.setValidator(QDoubleValidator(0.0, 3.0, 3))
+ self.style_input_field(self.median_width)
+ add_row(2, "Width (m):", self.median_width)
+
+ self.median_height = QLineEdit()
+ self.median_height.setValidator(QDoubleValidator(0.0, 3.0, 3))
+ self.style_input_field(self.median_height)
+ add_row(3, "Height (m):", self.median_height)
+
+ self.median_area = QLineEdit()
+ self.median_area.setValidator(QDoubleValidator(0.0, 10.0, 4))
+ self.style_input_field(self.median_area)
+ add_row(4, "Area (m^2):", self.median_area)
+
+ card_layout.addLayout(grid)
+ median_layout.addWidget(card)
+ median_layout.addStretch()
+ self.input_tabs.addTab(median_widget, "Median")
+
+ def create_railing_tab(self):
+ railing_widget = QWidget()
+ railing_widget.setStyleSheet("background-color: #f5f5f5;")
+ railing_layout = QVBoxLayout(railing_widget)
+ railing_layout.setContentsMargins(18, 6, 18, 12)
+ railing_layout.setSpacing(0)
+
+ card, card_layout = self._create_section_card("Railing Inputs:")
+ grid = QGridLayout()
+ grid.setContentsMargins(0, 0, 0, 0)
+ grid.setHorizontalSpacing(24)
+ grid.setVerticalSpacing(10)
+ grid.setColumnStretch(1, 1)
+
+ def add_row(row, label_text, widget):
+ label = QLabel(label_text)
+ label.setStyleSheet("font-size: 11px; color: #000;")
+ label.setMinimumWidth(180)
+ grid.addWidget(label, row, 0, Qt.AlignLeft)
+ grid.addWidget(widget, row, 1)
+
+ self.railing_type = QComboBox()
+ self.railing_type.addItems(VALUES_RAILING_TYPE)
+ self.style_input_field(self.railing_type)
+ add_row(0, "Type:", self.railing_type)
+
+ self.railing_width = QLineEdit()
+ self.railing_width.setValidator(QDoubleValidator(0.0, 2000.0, 1))
+ self.railing_width.setText(f"{DEFAULT_RAILING_WIDTH * 1000:.0f}")
+ self.style_input_field(self.railing_width)
+ self.railing_width.textChanged.connect(self.recalculate_girders)
+ add_row(1, "Width (mm):", self.railing_width)
+
+ self.railing_height = QLineEdit()
+ self.railing_height.setValidator(QDoubleValidator(MIN_RAILING_HEIGHT, 3.0, 3))
+ self.style_input_field(self.railing_height)
+ self.railing_height.editingFinished.connect(self.validate_railing_height)
+ add_row(2, "Height (m):", self.railing_height)
+
+ load_row = QHBoxLayout()
+ load_row.setContentsMargins(0, 0, 0, 0)
+ load_row.setSpacing(12)
+
+ self.railing_load_mode = QComboBox()
+ self.railing_load_mode.addItems(["Automatic (IRC 6)", "User-defined"])
+ self.style_input_field(self.railing_load_mode)
+ self.railing_load_mode.currentTextChanged.connect(self.on_railing_load_mode_changed)
+ load_row.addWidget(self.railing_load_mode)
+
+ self.railing_load_value = QLineEdit()
+ self.railing_load_value.setValidator(QDoubleValidator(0.0, 50.0, 2))
+ self.railing_load_value.setPlaceholderText("Value")
+ self.railing_load_value.setEnabled(False)
+ self.style_input_field(self.railing_load_value)
+ load_row.addWidget(self.railing_load_value)
+
+ load_container = QWidget()
+ load_container.setLayout(load_row)
+ add_row(3, "Load (kN/m):", load_container)
+
+ card_layout.addLayout(grid)
+ railing_layout.addWidget(card)
+ railing_layout.addStretch()
+ self.input_tabs.addTab(railing_widget, "Railing")
+
+ def create_wearing_course_tab(self):
+ wearing_widget = QWidget()
+ wearing_widget.setStyleSheet("background-color: #f5f5f5;")
+ wearing_layout = QVBoxLayout(wearing_widget)
+ wearing_layout.setContentsMargins(18, 6, 18, 12)
+ wearing_layout.setSpacing(0)
+
+ card, card_layout = self._create_section_card("Wearing Course Inputs:")
+ grid = QGridLayout()
+ grid.setContentsMargins(0, 0, 0, 0)
+ grid.setHorizontalSpacing(24)
+ grid.setVerticalSpacing(10)
+ grid.setColumnStretch(1, 1)
+
+ def add_row(row, label_text, widget):
+ label = QLabel(label_text)
+ label.setStyleSheet("font-size: 11px; color: #000;")
+ label.setMinimumWidth(200)
+ grid.addWidget(label, row, 0, Qt.AlignLeft)
+ grid.addWidget(widget, row, 1)
+
+ self.wearing_material = QComboBox()
+ self.wearing_material.addItems(VALUES_WEARING_COAT_MATERIAL)
+ self.style_input_field(self.wearing_material)
+ add_row(0, "Material:", self.wearing_material)
+
+ self.wearing_density = QLineEdit()
+ self.wearing_density.setValidator(QDoubleValidator(0.0, 40.0, 2))
+ self.style_input_field(self.wearing_density)
+ add_row(1, "Density (kN/m^3):", self.wearing_density)
+
+ self.wearing_thickness = QLineEdit()
+ self.wearing_thickness.setValidator(QDoubleValidator(0.0, 200.0, 1))
+ self.style_input_field(self.wearing_thickness)
+ add_row(2, "Thickness (mm):", self.wearing_thickness)
+
+ card_layout.addLayout(grid)
+ wearing_layout.addWidget(card)
+ wearing_layout.addStretch()
+ self.input_tabs.addTab(wearing_widget, "Wearing Course")
+
+ def create_lane_details_tab(self):
+ lane_widget = QWidget()
+ lane_widget.setStyleSheet("background-color: #f5f5f5;")
+ lane_layout = QVBoxLayout(lane_widget)
+ lane_layout.setContentsMargins(18, 6, 18, 12)
+ lane_layout.setSpacing(0)
+
+ card, card_layout = self._create_section_card("Inputs:")
+
+ selector_layout = QHBoxLayout()
+ selector_layout.setContentsMargins(0, 0, 0, 0)
+ selector_layout.setSpacing(12)
+
+ lanes_label = QLabel("No. of Traffic Lanes:")
+ lanes_label.setStyleSheet("font-size: 11px; color: #000;")
+ selector_layout.addWidget(lanes_label)
+
+ self.lane_count_combo = QComboBox()
+ self.lane_count_combo.addItems([str(i) for i in range(1, 7)])
+ self.style_input_field(self.lane_count_combo)
+ self.lane_count_combo.currentTextChanged.connect(self.on_lane_count_changed)
+ selector_layout.addWidget(self.lane_count_combo)
+ selector_layout.addStretch()
+
+ card_layout.addLayout(selector_layout)
+
+ self.lane_table = QTableWidget()
+ self.lane_table.setColumnCount(3)
+ self.lane_table.setHorizontalHeaderLabels([
+ "Traffic Lane Number",
+ "Distance from inner edge of crash barrier to left edge of lane (m)",
+ "Lane Width (m)"
+ ])
+ header = self.lane_table.horizontalHeader()
+ header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
+ header.setSectionResizeMode(1, QHeaderView.Stretch)
+ header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
+ self.lane_table.verticalHeader().setVisible(False)
+ self.lane_table.setAlternatingRowColors(True)
+ self.lane_table.setStyleSheet("""
+ QTableWidget {
+ background-color: #ffffff;
+ alternate-background-color: #f9f9f9;
+ gridline-color: #e0e0e0;
+ border: 1px solid #e0e0e0;
+ }
+ QTableWidget::item {
+ padding: 8px;
+ border-bottom: 1px solid #e0e0e0;
+ }
+ QTableWidget::item:hover {
+ background-color: #e8f4f8;
+ }
+ QHeaderView::section {
+ background-color: #f5f5f5;
+ color: #333;
+ padding: 8px;
+ border: 1px solid #e0e0e0;
+ font-weight: bold;
+ font-size: 11px;
+ }
+ """)
+
+ card_layout.addWidget(self.lane_table)
+ lane_layout.addWidget(card)
+ lane_layout.addStretch()
+
+ self.input_tabs.addTab(lane_widget, "Lane Details")
+ self._update_lane_details_rows(self.lane_count_combo.currentText())
+
+ def _update_lane_details_rows(self, count):
+ try:
+ num_lanes = int(count)
+ self.lane_table.setRowCount(num_lanes)
+
+ for i in range(num_lanes):
+ # Lane number (non-editable)
+ lane_num_item = QTableWidgetItem(str(i + 1))
+ lane_num_item.setFlags(lane_num_item.flags() & ~Qt.ItemIsEditable)
+ lane_num_item.setTextAlignment(Qt.AlignCenter)
+ self.lane_table.setItem(i, 0, lane_num_item)
+
+ # Distance field (editable)
+ if not self.lane_table.item(i, 1):
+ self.lane_table.setItem(i, 1, QTableWidgetItem(""))
+
+ # Width field (editable)
+ if not self.lane_table.item(i, 2):
+ self.lane_table.setItem(i, 2, QTableWidgetItem(""))
+ except ValueError:
+ pass
+
+ def update_footpath_value(self, footpath_value):
+ self.footpath_value = footpath_value
+ if hasattr(self, "footpath_width"):
+ self.footpath_width.setEnabled(footpath_value != "None")
+ self.footpath_thickness.setEnabled(footpath_value != "None")
+ self.recalculate_girders()
+ self.footpath_changed.emit(footpath_value)
+
+ def get_overall_bridge_width(self):
+ try:
+ overall_width = self.carriageway_width
+ if self.footpath_value != "None":
+ footpath_width = float(self.footpath_width.text()) if self.footpath_width.text() else 0
+ num_footpaths = 2 if self.footpath_value == "Both" else (1 if self.footpath_value == "Single Sided" else 0)
+ overall_width += footpath_width * num_footpaths
+
+ crash_barrier_width = float(self.crash_barrier_width.text()) if self.crash_barrier_width.text() else DEFAULT_CRASH_BARRIER_WIDTH
+ overall_width += crash_barrier_width * 2
+
+ if self.footpath_value != "None":
+ railing_width_text = self.railing_width.text() if hasattr(self, "railing_width") else ""
+ if railing_width_text:
+ railing_width = float(railing_width_text) / 1000.0
+ else:
+ railing_width = DEFAULT_RAILING_WIDTH
+ overall_width += railing_width * 2
+
+ return overall_width
+ except:
+ return self.carriageway_width
+
+ def _update_overall_bridge_width_display(self):
+ if hasattr(self, "overall_bridge_width_display"):
+ try:
+ overall_width = self.get_overall_bridge_width()
+ self.overall_bridge_width_display.setText(f"{overall_width:.3f}")
+ except:
+ self.overall_bridge_width_display.clear()
+
+ def recalculate_girders(self):
+ if self.updating_fields:
+ return
+ try:
+ self._update_overall_bridge_width_display()
+ overall_width = self.get_overall_bridge_width()
+ spacing = float(self.girder_spacing.text()) if self.girder_spacing.text() else DEFAULT_GIRDER_SPACING
+ overhang = float(self.deck_overhang.text()) if self.deck_overhang.text() else DEFAULT_DECK_OVERHANG
+ if spacing >= overall_width or overhang >= overall_width:
+ self.no_of_girders.setText("")
+ return
+ if spacing > 0:
+ no_girders = int(round((overall_width - 2 * overhang) / spacing)) + 1
+ if no_girders >= 2:
+ self.updating_fields = True
+ self.no_of_girders.setText(str(no_girders))
+ self.updating_fields = False
+ except:
+ pass
+
+ def on_girder_spacing_changed(self):
+ if not self.updating_fields:
+ try:
+ overall_width = self.get_overall_bridge_width()
+ spacing_text = self.girder_spacing.text()
+ if spacing_text:
+ spacing = float(spacing_text)
+ if spacing >= overall_width:
+ QMessageBox.warning(self, "Invalid Girder Spacing",
+ f"Girder spacing ({spacing:.2f} m) must be less than overall bridge width ({overall_width:.2f} m).")
+ return
+ self.recalculate_girders()
+ except:
+ pass
+
+ def on_deck_overhang_changed(self):
+ if not self.updating_fields:
+ try:
+ overall_width = self.get_overall_bridge_width()
+ overhang_text = self.deck_overhang.text()
+ if overhang_text:
+ overhang = float(overhang_text)
+ if overhang >= overall_width:
+ QMessageBox.warning(self, "Invalid Deck Overhang",
+ f"Deck overhang ({overhang:.2f} m) must be less than overall bridge width ({overall_width:.2f} m).")
+ return
+ self.recalculate_girders()
+ except:
+ pass
+
+ def on_no_of_girders_changed(self):
+ if not self.updating_fields:
+ try:
+ no_girders_text = self.no_of_girders.text()
+ if no_girders_text:
+ no_girders = int(no_girders_text)
+ if no_girders < 2:
+ QMessageBox.warning(self, "Invalid Number of Girders",
+ "Number of girders must be at least 2.")
+ return
+ overall_width = self.get_overall_bridge_width()
+ overhang = float(self.deck_overhang.text()) if self.deck_overhang.text() else DEFAULT_DECK_OVERHANG
+ if no_girders > 1:
+ new_spacing = (overall_width - 2 * overhang) / (no_girders - 1)
+ self.updating_fields = True
+ self.girder_spacing.setText(f"{new_spacing:.3f}")
+ self.updating_fields = False
+ except:
+ pass
+
+ def on_footpath_width_changed(self):
+ if not self.updating_fields:
+ self.recalculate_girders()
+
+ def validate_footpath_width(self):
+ try:
+ if self.footpath_width.text():
+ width = float(self.footpath_width.text())
+ if width < MIN_FOOTPATH_WIDTH:
+ QMessageBox.critical(self, "Footpath Width Error",
+ f"Footpath width must be at least {MIN_FOOTPATH_WIDTH} m as per IRC 5 Clause 104.3.6.")
+ except:
+ pass
+
+ def validate_railing_height(self):
+ try:
+ if self.railing_height.text():
+ height = float(self.railing_height.text())
+ if height < MIN_RAILING_HEIGHT:
+ QMessageBox.critical(self, "Railing Height Error",
+ f"Railing height must be at least {MIN_RAILING_HEIGHT} m as per IRC 5 Clauses 109.7.2.3 and 109.7.2.4.")
+ except:
+ pass
+
+ def update_footpath_thickness(self):
+ if self.deck_thickness.text() and not self.footpath_thickness.text():
+ self.footpath_thickness.setText(self.deck_thickness.text())
+
+ def on_crash_barrier_type_changed(self, barrier_type):
+ if (barrier_type in ["Flexible", "Semi-Rigid"]) and (self.footpath_value == "None"):
+ QMessageBox.critical(self, "Crash Barrier Type Not Permitted",
+ f"{barrier_type} crash barriers are not permitted on bridges without an outer footpath per IRC 5 Clause 109.6.4.")
+
+ def on_railing_load_mode_changed(self, mode):
+ if not hasattr(self, "railing_load_value"):
+ return
+ is_auto = mode.startswith("Automatic")
+ self.railing_load_value.setEnabled(not is_auto)
+ if is_auto:
+ self.railing_load_value.clear()
+
+ def on_lane_count_changed(self, text):
+ self._update_lane_details_rows(text)
+
+ def _update_lane_details_rows(self, count):
+ try:
+ total_rows = int(count)
+ except (TypeError, ValueError):
+ total_rows = 1
+ if not hasattr(self, "lane_table"):
+ return
+ self.lane_table.setRowCount(total_rows)
+ for row in range(total_rows):
+ lane_item = QTableWidgetItem(str(row + 1))
+ lane_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
+ self.lane_table.setItem(row, 0, lane_item)
+ for col in range(1, self.lane_table.columnCount()):
+ existing_item = self.lane_table.item(row, col)
+ if existing_item is None:
+ self.lane_table.setItem(row, col, QTableWidgetItem(""))
+
+ def _show_placeholder_message(self, action_name):
+ QMessageBox.information(self, action_name, "This action will be available in an upcoming update.")
+
+class OptimizableField(QWidget):
+ """Widget that allows selection between Optimized/Customized/All modes with input field"""
+
+ def __init__(self, label_text, parent=None):
+ super().__init__(parent)
+ self.layout = QHBoxLayout(self)
+ self.layout.setContentsMargins(0, 0, 0, 0)
+ self.layout.setSpacing(8)
+
+ self.mode_combo = QComboBox()
+ self.mode_combo.addItems(VALUES_OPTIMIZATION_MODE)
+ self.mode_combo.setMinimumWidth(140)
+ self.mode_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+
+ self.input_field = QLineEdit()
+ self.input_field.setEnabled(False)
+ self.input_field.setVisible(False)
+ self.input_field.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+
+ self.layout.addWidget(self.mode_combo)
+ self.layout.addWidget(self.input_field)
+
+ self.mode_combo.currentTextChanged.connect(self.on_mode_changed)
+ self.on_mode_changed(self.mode_combo.currentText())
+
+ def on_mode_changed(self, text):
+ """Enable/disable input field based on selection"""
+ if text in ("Optimized", "All", "NA"):
+ self.input_field.setEnabled(False)
+ self.input_field.clear()
+ self.input_field.setVisible(False)
+ else:
+ self.input_field.setEnabled(True)
+ self.input_field.setVisible(True)
+
+ def get_value(self):
+ """Returns tuple of (mode, value)"""
+ return (self.mode_combo.currentText(), self.input_field.text())
+
+class SectionPropertiesTab(QWidget):
+ """Sub-tab for Section Properties with custom navigation layout."""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.nav_buttons = []
+ self.section_widgets = []
+ self.girder_tab = None
+ self.init_ui()
+
+ def init_ui(self):
+ """Initialize styled navigation and content panels."""
+ self.setStyleSheet("background-color: #f0f0f0;")
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(10, 10, 10, 10)
+ main_layout.setSpacing(10)
+
+ # Top navigation bar (horizontal)
+ nav_bar = QWidget()
+ nav_bar.setStyleSheet("background-color: white;")
+ nav_bar_layout = QHBoxLayout(nav_bar)
+ nav_bar_layout.setContentsMargins(6, 0, 6, 0)
+ nav_bar_layout.setSpacing(5)
+
+ main_layout.addWidget(nav_bar)
+
+ # Content frame
+ content_frame = QFrame()
+ content_frame.setObjectName("sectionContentFrame")
+ content_frame.setStyleSheet("""
+ QFrame#sectionContentFrame {
+ background-color: #f0f0f0;
+ border: none;
+ }
+ """)
+ content_inner_layout = QVBoxLayout(content_frame)
+ content_inner_layout.setContentsMargins(0, 0, 0, 0)
+ content_inner_layout.setSpacing(0)
+
+ self.stack = QStackedWidget()
+ self.stack.setObjectName("sectionStack")
+ self.stack.setStyleSheet("QStackedWidget#sectionStack { background-color: transparent; }")
+ content_inner_layout.addWidget(self.stack)
+
+ main_layout.addWidget(content_frame, 1)
+
+ sections = [
+ ("Girder Details:", GirderDetailsTab),
+ ("Stiffener Details:", StiffenerDetailsTab),
+ ("Cross-Bracing Details:", CrossBracingDetailsTab),
+ ("End Diaphragm Details:", EndDiaphragmDetailsTab),
+ ]
+
+ for i, (label, widget_class) in enumerate(sections):
+ btn = QPushButton(label)
+ btn.setObjectName("sectionNavBtn")
+ btn.setCheckable(True)
+ btn.setStyleSheet("""
+ QPushButton#sectionNavBtn {
+ background-color: white;
+ color: #333;
+ border: 1px solid #b0b0b0;
+ border-right: none;
+ padding: 3px 10px;
+ text-align: center;
+ font-size: 10px;
+ font-weight: normal;
+ min-height: 26px;
+ }
+ QPushButton#sectionNavBtn:first {
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+ }
+ QPushButton#sectionNavBtn:last {
+ border-right: 1px solid #b0b0b0;
+ border-top-right-radius: 5px;
+ border-bottom-right-radius: 5px;
+ }
+ QPushButton#sectionNavBtn:checked {
+ background-color: #90AF13;
+ color: white;
+ font-weight: bold;
+ border: 1px solid #90AF13;
+ }
+ QPushButton#sectionNavBtn:hover:!checked {
+ background-color: #f5f5f5;
+ }
+ """)
+ btn.clicked.connect(lambda checked, idx=i: self.switch_section(idx))
+ self.nav_buttons.append(btn)
+ nav_bar_layout.addWidget(btn)
+
+ section_widget = widget_class()
+ if isinstance(section_widget, GirderDetailsTab):
+ self.girder_tab = section_widget
+ self.section_widgets.append(section_widget)
+ self.stack.addWidget(section_widget)
+
+ if self.nav_buttons:
+ self.nav_buttons[0].setChecked(True)
+ self.stack.setCurrentIndex(0)
+
+ def switch_section(self, index):
+ """Switch the stacked widget page and update navigation states."""
+ self.stack.setCurrentIndex(index)
+ for btn_index, button in enumerate(self.nav_buttons):
+ button.setChecked(btn_index == index)
+
+
+class GirderDetailsTab(QWidget):
+ """Tab for Girder Details styled to match the provided reference."""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.welded_rows = []
+ self.rolled_rows = []
+ self.symmetry_row = []
+ self.web_type_row = []
+ self.section_property_inputs = {}
+ self.segment_chain = {}
+ self._suppress_distance_updates = False
+ self.available_girders = [f"G{i}" for i in range(1, 6)]
+ self.init_ui()
+
+ def init_ui(self):
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setFrameShape(QFrame.NoFrame)
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ scroll.setStyleSheet("QScrollArea { border: none; background: transparent; }")
+ main_layout.addWidget(scroll)
+
+ content = QWidget()
+ scroll.setWidget(content)
+ content.setStyleSheet("background-color: white;")
+
+ content_layout = QVBoxLayout(content)
+ content_layout.setContentsMargins(10, 0, 10, 10)
+ content_layout.setSpacing(12)
+
+ content_layout.addWidget(self._build_overview_card())
+ content_layout.addWidget(self._build_section_card())
+ content_layout.addStretch()
+
+ def _build_overview_card(self):
+ card = self._create_card_frame()
+ layout = QGridLayout(card)
+ layout.setContentsMargins(18, 16, 18, 16)
+ layout.setHorizontalSpacing(20)
+ layout.setVerticalSpacing(12)
+ layout.setColumnStretch(1, 1)
+ layout.setColumnStretch(3, 1)
+
+ self.select_girder_combo = CheckableComboBox()
+ self.select_girder_combo.addItems(["All"] + self.available_girders)
+ apply_field_style(self.select_girder_combo)
+ self._set_field_width(self.select_girder_combo)
+ layout.addWidget(self._create_label("Select Girder:"), 0, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ layout.addWidget(self.select_girder_combo, 0, 1, 1, 3)
+
+ self.span_combo = QComboBox()
+ self.span_combo.addItems(VALUES_GIRDER_SPAN_MODE)
+ apply_field_style(self.span_combo)
+ self._set_field_width(self.span_combo)
+ self.span_combo.currentTextChanged.connect(self._on_span_changed)
+ layout.addWidget(self._create_label("Span:"), 1, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ layout.addWidget(self.span_combo, 1, 1, Qt.AlignLeft)
+
+ self.member_id_input = QLineEdit()
+ self.member_id_input.setPlaceholderText("G1-1")
+ apply_field_style(self.member_id_input)
+ self._set_field_width(self.member_id_input)
+ self.member_id_input.textChanged.connect(self._on_member_id_changed)
+ layout.addWidget(self._create_label("Member ID:"), 1, 2, Qt.AlignLeft | Qt.AlignVCenter)
+ layout.addWidget(self.member_id_input, 1, 3, Qt.AlignLeft)
+
+ self.distance_start_input = QLineEdit("0")
+ self.distance_end_input = QLineEdit("30")
+ apply_field_style(self.distance_start_input)
+ apply_field_style(self.distance_end_input)
+ self._set_field_width(self.distance_start_input, 80)
+ self._set_field_width(self.distance_end_input, 80)
+ self.distance_start_input.editingFinished.connect(self._on_distance_start_changed)
+ self.distance_end_input.editingFinished.connect(self._on_distance_end_changed)
+ distance_row = self._build_distance_row()
+ layout.addWidget(self._create_label("Distance from left edge (m):"), 2, 0, Qt.AlignLeft | Qt.AlignTop)
+ layout.addLayout(distance_row, 2, 1, Qt.AlignLeft)
+
+ self.length_input = QLineEdit("30")
+ apply_field_style(self.length_input)
+ self._set_field_width(self.length_input)
+ self.length_input.setReadOnly(True)
+ self.length_input.textChanged.connect(self._on_length_changed)
+ layout.addWidget(self._create_label("Length (m):"), 2, 2, Qt.AlignLeft | Qt.AlignVCenter)
+ layout.addWidget(self.length_input, 2, 3, Qt.AlignLeft)
+
+ self._setup_girder_selector()
+ self._on_span_changed(self.span_combo.currentText())
+
+ return card
+
+ def _build_distance_row(self):
+ row = QHBoxLayout()
+ row.setContentsMargins(0, 0, 0, 0)
+ row.setSpacing(16)
+
+ def _build_column(line_edit, caption):
+ column = QVBoxLayout()
+ column.setContentsMargins(0, 0, 0, 0)
+ column.setSpacing(2)
+ column.addWidget(line_edit)
+ label = self._create_small_label(caption)
+ label.setAlignment(Qt.AlignCenter)
+ column.addWidget(label, alignment=Qt.AlignCenter)
+ return column
+
+ row.addLayout(_build_column(self.distance_start_input, "Start"))
+ row.addLayout(_build_column(self.distance_end_input, "End"))
+ return row
+
+ def _build_section_card(self):
+ container = QWidget()
+ container.setStyleSheet("background: transparent;")
+ main_layout = QHBoxLayout(container)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(16)
+
+ # Left side - two bordered boxes stacked vertically
+ left_column = QWidget()
+ left_column.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+ left_column_layout = QVBoxLayout(left_column)
+ left_column_layout.setContentsMargins(0, 0, 0, 0)
+ left_column_layout.setSpacing(12)
+
+ # Section Inputs box (single frame containing all fields)
+ section_inputs_box = self._create_inner_box()
+ section_inputs_layout = QVBoxLayout(section_inputs_box)
+ section_inputs_layout.setContentsMargins(12, 8, 12, 12)
+ section_inputs_layout.setSpacing(8)
+
+ section_inputs_title = self._create_label("Section Inputs:")
+ section_inputs_layout.addWidget(section_inputs_title)
+
+ inputs_grid = QGridLayout()
+ inputs_grid.setContentsMargins(0, 0, 0, 0)
+ inputs_grid.setHorizontalSpacing(16)
+ inputs_grid.setVerticalSpacing(12)
+ inputs_grid.setColumnMinimumWidth(0, 150)
+ inputs_grid.setColumnStretch(0, 0)
+ inputs_grid.setColumnStretch(1, 1)
+
+ self.design_combo = QComboBox()
+ self.design_combo.addItems(VALUES_GIRDER_DESIGN_MODE)
+ apply_field_style(self.design_combo)
+ row = self._add_box_row(inputs_grid, 0, "Design:", self.design_combo)
+
+ self.type_combo = QComboBox()
+ self.type_combo.addItems(VALUES_GIRDER_TYPE)
+ apply_field_style(self.type_combo)
+ row = self._add_box_row(inputs_grid, row, "Type:", self.type_combo)
+
+ self.symmetry_combo = QComboBox()
+ self.symmetry_combo.addItems(VALUES_GIRDER_SYMMETRY)
+ apply_field_style(self.symmetry_combo)
+ row = self._add_box_row(inputs_grid, row, "Symmetry:", self.symmetry_combo, self.symmetry_row)
+
+ self.total_depth_input = self._create_line_edit()
+ row = self._add_box_row(
+ inputs_grid,
+ row,
+ "Total Depth (d, mm):",
+ self.total_depth_input,
+ self.welded_rows,
+ )
+
+ self.web_thickness_combo = QComboBox()
+ self.web_thickness_combo.addItems(VALUES_PROFILE_SCOPE)
+ apply_field_style(self.web_thickness_combo)
+ row = self._add_box_row(
+ inputs_grid,
+ row,
+ "Web Thickness (wt, mm):",
+ self.web_thickness_combo,
+ self.welded_rows,
+ )
+
+ self.top_width_input = self._create_line_edit()
+ row = self._add_box_row(
+ inputs_grid,
+ row,
+ "Width of Top Flange (tfw, mm):",
+ self.top_width_input,
+ self.welded_rows,
+ )
+
+ self.top_thickness_combo = QComboBox()
+ self.top_thickness_combo.addItems(VALUES_PROFILE_SCOPE)
+ apply_field_style(self.top_thickness_combo)
+ row = self._add_box_row(
+ inputs_grid,
+ row,
+ "Top Flange Thickness (tft, mm):",
+ self.top_thickness_combo,
+ self.welded_rows,
+ )
+
+ self.bottom_width_input = self._create_line_edit()
+ row = self._add_box_row(
+ inputs_grid,
+ row,
+ "Width of Bottom Flange (bfw, mm):",
+ self.bottom_width_input,
+ self.welded_rows,
+ )
+
+ self.bottom_thickness_combo = QComboBox()
+ self.bottom_thickness_combo.addItems(VALUES_PROFILE_SCOPE)
+ apply_field_style(self.bottom_thickness_combo)
+ row = self._add_box_row(
+ inputs_grid,
+ row,
+ "Bottom Flange Thickness (bft, mm):",
+ self.bottom_thickness_combo,
+ self.welded_rows,
+ )
+
+ self.is_section_combo = QComboBox()
+ self._populate_rolled_section_combo()
+ apply_field_style(self.is_section_combo)
+ self._add_box_row(inputs_grid, row, "IS Section:", self.is_section_combo, self.rolled_rows)
+
+ section_inputs_layout.addLayout(inputs_grid)
+ left_column_layout.addWidget(section_inputs_box)
+
+ # Restraint/Web details box
+ restraint_box = self._create_inner_box()
+ restraint_layout = QVBoxLayout(restraint_box)
+ restraint_layout.setContentsMargins(12, 6, 12, 10)
+ restraint_layout.setSpacing(6)
+
+ restraint_title = self._create_label("Restraint & Web Details:")
+ restraint_layout.addWidget(restraint_title)
+
+ restraint_grid = QGridLayout()
+ restraint_grid.setContentsMargins(0, 0, 0, 0)
+ restraint_grid.setHorizontalSpacing(16)
+ restraint_grid.setVerticalSpacing(12)
+ restraint_grid.setColumnMinimumWidth(0, 150)
+ restraint_grid.setColumnStretch(0, 0)
+ restraint_grid.setColumnStretch(1, 1)
+
+ self.torsion_combo = QComboBox()
+ apply_field_style(self.torsion_combo)
+ row = self._add_box_row(restraint_grid, 0, "Torsional Restraint:", self.torsion_combo)
+
+ self.warping_combo = QComboBox()
+ apply_field_style(self.warping_combo)
+ row = self._add_box_row(restraint_grid, row, "Warping Restraint:", self.warping_combo)
+
+ self.web_type_combo = QComboBox()
+ apply_field_style(self.web_type_combo)
+ self._add_box_row(restraint_grid, row, "Web Type*:", self.web_type_combo, self.web_type_row)
+
+ restraint_layout.addLayout(restraint_grid)
+ restraint_layout.addStretch(1)
+ left_column_layout.addWidget(restraint_box)
+ self._configure_restraint_fields()
+
+ main_layout.addWidget(left_column)
+
+ # Right side - image + section properties box
+ right_column = QWidget()
+ right_column.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+ right_column_layout = QVBoxLayout(right_column)
+ right_column_layout.setContentsMargins(0, 0, 0, 0)
+ right_column_layout.setSpacing(12)
+
+ # Dynamic image box
+ image_box = self._create_inner_box()
+ image_layout = QVBoxLayout(image_box)
+ image_layout.setContentsMargins(10, 10, 10, 10)
+ image_layout.setSpacing(5)
+
+ self.section_preview = RolledSectionPreview()
+ image_layout.addWidget(self.section_preview, 1)
+
+ self.preview_caption = QLabel("Provide girder inputs to preview")
+ self.preview_caption.setAlignment(Qt.AlignCenter)
+ self.preview_caption.setStyleSheet(
+ "QLabel { font-size: 13px; font-weight: 700; color: #1e1e1e; border: none; padding-top: 6px; font-family: 'Ubuntu Sans', 'Segoe UI', sans-serif; }"
+ )
+ image_layout.addWidget(self.preview_caption)
+
+ right_column_layout.addWidget(image_box)
+
+ # Section Properties box
+ props_box = self._create_inner_box()
+ props_layout = QVBoxLayout(props_box)
+ props_layout.setContentsMargins(12, 10, 12, 10)
+ props_layout.setSpacing(10)
+
+ props_title = self._create_label("Section Properties:")
+ props_layout.addWidget(props_title)
+
+ properties_grid = QGridLayout()
+ properties_grid.setContentsMargins(0, 0, 0, 0)
+ properties_grid.setHorizontalSpacing(12)
+ properties_grid.setVerticalSpacing(10)
+ properties_grid.setColumnMinimumWidth(0, 140)
+ properties_grid.setColumnStretch(0, 0)
+ properties_grid.setColumnStretch(1, 1)
+
+ property_fields = [
+ "Mass, M (Kg/m)",
+ "Sectional Area, a (cm2)",
+ "2nd Moment of Area, Iz (cm4)",
+ "2nd Moment of Area, Iy (cm4)",
+ "Radius of Gyration, rz (cm)",
+ "Radius of Gyration, ry (cm)",
+ "Elastic Modulus, Zz (cm3)",
+ "Elastic Modulus, Zy (cm3)",
+ "Plastic Modulus, Zuz (cm3)",
+ "Plastic Modulus, Zuy (cm3)",
+ "Torsion Constant, It (cm4)",
+ "Warping Constant, Iw (cm6)"
+ ]
+
+ for index, text in enumerate(property_fields):
+ label = self._create_small_label(text)
+ line_edit = self._create_line_edit()
+ line_edit.setPlaceholderText("")
+ properties_grid.addWidget(label, index, 0)
+ properties_grid.addWidget(line_edit, index, 1)
+ self.section_property_inputs[text] = line_edit
+
+ props_layout.addLayout(properties_grid)
+ right_column_layout.addWidget(props_box)
+
+ main_layout.addWidget(right_column)
+
+ self.design_combo.currentTextChanged.connect(self._on_design_changed)
+ self.type_combo.currentTextChanged.connect(self._on_type_changed)
+ self.is_section_combo.currentTextChanged.connect(self._update_preview)
+ for watcher in (self.total_depth_input, self.top_width_input, self.bottom_width_input):
+ watcher.textChanged.connect(self._update_preview)
+ self._on_design_changed(self.design_combo.currentText())
+ self._on_type_changed(self.type_combo.currentText())
+
+ return container
+
+ def _create_card_frame(self):
+ frame = QFrame()
+ frame.setObjectName("girderCard")
+ frame.setStyleSheet("QFrame#girderCard { background-color: white; border: 1px solid #cfcfcf; border-radius: 10px; }")
+ return frame
+
+ def _create_label(self, text):
+ label = QLabel(text)
+ label.setStyleSheet("font-size: 12px; color: #2f2f2f; font-weight: 600; background: transparent;")
+ label.setAutoFillBackground(False)
+ return label
+
+ def _create_small_label(self, text):
+ label = QLabel(text)
+ label.setStyleSheet("font-size: 10px; color: #5a5a5a; background: transparent;")
+ label.setAutoFillBackground(False)
+ return label
+
+ def _create_line_edit(self):
+ line_edit = QLineEdit()
+ apply_field_style(line_edit)
+ return line_edit
+
+ def _add_section_row(self, layout, row, text, widget, tracker=None):
+ label = self._create_label(text)
+ widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+ self._set_field_width(widget)
+ layout.addWidget(label, row, 0)
+ layout.addWidget(widget, row, 1)
+ if tracker is not None:
+ tracker.append((label, widget))
+ return row + 1
+
+ def _set_field_width(self, widget, width=230):
+ widget.setMaximumWidth(width)
+ widget.setMinimumWidth(min(width, 160))
+
+ def _setup_girder_selector(self):
+ if hasattr(self.select_girder_combo, "checkedItemsChanged"):
+ self.select_girder_combo.checkedItemsChanged.connect(self._on_girders_selection_changed)
+ self._on_girders_selection_changed()
+
+ def _on_girders_selection_changed(self, *args):
+ if self.span_combo.currentText() == "Full Length":
+ self._update_member_id_edit_state()
+ return
+ current_text = self.member_id_input.text().strip()
+ if not self._is_valid_segment_id(current_text):
+ default_id = self._default_member_segment_id()
+ self._set_member_id_text(default_id)
+ self._update_member_id_edit_state()
+
+ def _get_selected_girders(self):
+ selected = []
+ if hasattr(self.select_girder_combo, "checked_items"):
+ selected = self.select_girder_combo.checked_items(include_all=True)
+ model = self.select_girder_combo.model()
+ if model is None:
+ return self.available_girders.copy()
+ all_checked = False
+ if selected:
+ all_checked = any(item.strip().lower() == "all" for item in selected)
+ else:
+ for row in range(model.rowCount()):
+ item = model.item(row)
+ if not item or item.checkState() != Qt.Checked:
+ continue
+ text = item.text()
+ if text.strip().lower() == "all":
+ all_checked = True
+ else:
+ selected.append(text)
+ if all_checked or not selected:
+ return self.available_girders.copy()
+ normalized = [text for text in selected if text in self.available_girders]
+ return normalized if normalized else self.available_girders.copy()
+
+ def _default_member_segment_id(self, girders=None):
+ girders = girders or self._get_selected_girders()
+ base = girders[0] if girders else "G1"
+ return f"{base}-1"
+
+ def _set_member_id_text(self, value, block_signals=False):
+ if block_signals:
+ previous = self.member_id_input.blockSignals(True)
+ self.member_id_input.setText(value)
+ self.member_id_input.blockSignals(previous)
+ else:
+ self.member_id_input.setText(value)
+
+ def _is_valid_segment_id(self, member_id):
+ if not member_id or "-" not in member_id:
+ return False
+ base, index = self._split_member_id(member_id)
+ return bool(base and isinstance(index, int))
+
+ def _update_member_id_edit_state(self):
+ is_full_span = self.span_combo.currentText() == "Full Length"
+ self.member_id_input.setReadOnly(is_full_span)
+ if is_full_span:
+ girders = self._get_selected_girders()
+ display = ", ".join(girders) if girders else "G1"
+ self._set_member_id_text(display, block_signals=True)
+ else:
+ current_text = self.member_id_input.text().strip()
+ if not self._is_valid_segment_id(current_text):
+ default_id = self._default_member_segment_id()
+ self._set_member_id_text(default_id)
+ self._update_distance_field_states()
+
+ def _on_design_changed(self, text):
+ is_custom = text.lower() == "customized"
+ toggle_targets = (
+ self.type_combo,
+ self.symmetry_combo,
+ self.total_depth_input,
+ self.top_width_input,
+ self.bottom_width_input,
+ )
+ for widget in toggle_targets:
+ widget.setEnabled(is_custom)
+ if not is_custom:
+ self._lock_type_to_welded()
+ self._reset_section_state()
+ self._apply_type_state()
+
+ def _on_type_changed(self, text):
+ self._apply_type_state()
+ self._update_preview()
+
+ def _apply_type_state(self):
+ is_welded = self.type_combo.currentText().lower() == "welded"
+ is_custom = self.design_combo.currentText().lower() == "customized"
+
+ self._set_row_visibility(self.welded_rows, is_welded)
+ self._set_row_visibility(self.rolled_rows, not is_welded)
+
+ for label, widget in self.symmetry_row:
+ label.setVisible(is_welded)
+ widget.setVisible(is_welded)
+ self.symmetry_combo.setEnabled(is_welded and is_custom)
+
+ plate_widgets = (
+ self.total_depth_input,
+ self.web_thickness_combo,
+ self.top_width_input,
+ self.top_thickness_combo,
+ self.bottom_width_input,
+ self.bottom_thickness_combo,
+ )
+ for widget in plate_widgets:
+ widget.setEnabled(is_welded and is_custom)
+ widget.setVisible(is_welded)
+
+ for label, widget in self.web_type_row:
+ label.setVisible(is_welded)
+ widget.setVisible(is_welded)
+ widget.setEnabled(is_welded and is_custom)
+
+ self.is_section_combo.setVisible(not is_welded)
+ self.is_section_combo.setEnabled(not is_welded)
+
+ def _lock_type_to_welded(self):
+ welded_index = self.type_combo.findText("Welded", Qt.MatchFixedString)
+ if welded_index != -1 and self.type_combo.currentIndex() != welded_index:
+ previous = self.type_combo.blockSignals(True)
+ self.type_combo.setCurrentIndex(welded_index)
+ self.type_combo.blockSignals(previous)
+
+ def _reset_section_state(self):
+ for widget in (self.total_depth_input, self.top_width_input, self.bottom_width_input):
+ previous = widget.blockSignals(True)
+ widget.clear()
+ widget.blockSignals(previous)
+ self._update_preview()
+
+ def _update_distance_field_states(self):
+ member_id = self.member_id_input.text().strip()
+ is_full_span = self.span_combo.currentText() == "Full Length"
+ if is_full_span:
+ self.distance_start_input.setReadOnly(True)
+ self.distance_end_input.setReadOnly(True)
+ return
+ if not self._is_valid_segment_id(member_id):
+ self.distance_start_input.setReadOnly(True)
+ self.distance_end_input.setReadOnly(True)
+ return
+ self.distance_start_input.setReadOnly(self._is_first_segment(member_id))
+ self.distance_end_input.setReadOnly(False)
+
+ def _on_span_changed(self, span_text):
+ is_full = span_text == "Full Length"
+ self.length_input.setReadOnly(not is_full)
+ if is_full:
+ self._apply_full_length_distances()
+ else:
+ member_id = self.member_id_input.text().strip()
+ if not self._is_valid_segment_id(member_id):
+ member_id = self._default_member_segment_id()
+ self._set_member_id_text(member_id)
+ self._load_segment_distances(member_id)
+ self._update_member_id_edit_state()
+
+ def _on_length_changed(self, _):
+ if self.span_combo.currentText() == "Full Length":
+ self._apply_full_length_distances()
+
+ def _apply_full_length_distances(self):
+ self._suppress_distance_updates = True
+ try:
+ total_span = self._get_total_span()
+ self._set_line_edit_value(self.distance_start_input, 0.0)
+ self._set_line_edit_value(self.distance_end_input, total_span)
+ finally:
+ self._suppress_distance_updates = False
+
+ def _on_member_id_changed(self, member_id):
+ member_id = member_id.strip()
+ if not member_id or self.span_combo.currentText() == "Full Length":
+ return
+ if not self._is_valid_segment_id(member_id):
+ self._update_distance_field_states()
+ return
+ if self._is_first_segment(member_id):
+ self._update_segment_record(member_id, start=0.0)
+ else:
+ previous_id = self._get_previous_segment_id(member_id)
+ previous_end = self.segment_chain.get(previous_id, {}).get("end") if previous_id else None
+ if previous_end is not None:
+ self._update_segment_record(member_id, start=previous_end)
+ self._load_segment_distances(member_id)
+ self._update_distance_field_states()
+
+ def _on_distance_start_changed(self):
+ if self._suppress_distance_updates:
+ return
+ current_id = self.member_id_input.text().strip()
+ if not current_id or not self._is_valid_segment_id(current_id):
+ return
+ if self._is_first_segment(current_id):
+ self._suppress_distance_updates = True
+ try:
+ self._set_line_edit_value(self.distance_start_input, 0.0)
+ finally:
+ self._suppress_distance_updates = False
+ self._update_segment_record(current_id, start=0.0)
+ return
+ value = self._parse_float(self.distance_start_input.text()) or 0.0
+ self._update_segment_record(current_id, start=value)
+
+ def _on_distance_end_changed(self):
+ if self._suppress_distance_updates:
+ return
+ current_id = self.member_id_input.text().strip()
+ if not current_id or self.span_combo.currentText() == "Full Length" or not self._is_valid_segment_id(current_id):
+ return
+ end_value = self._parse_float(self.distance_end_input.text())
+ if end_value is None:
+ end_value = 0.0
+
+ start_value = self._parse_float(self.distance_start_input.text())
+ if start_value is None:
+ start_value = self.segment_chain.get(current_id, {}).get("start")
+ if start_value is None and self._is_first_segment(current_id):
+ start_value = 0.0
+
+ if start_value is not None:
+ self._update_segment_record(current_id, start=start_value)
+ self._update_segment_record(current_id, end=end_value)
+ self._propagate_next_segment_start(current_id, end_value)
+
+ def _propagate_next_segment_start(self, member_id, next_start_value):
+ next_id = self._get_next_segment_id(member_id)
+ if not next_id:
+ return
+ self._update_segment_record(next_id, start=next_start_value)
+ if next_id == self.member_id_input.text().strip() and self.span_combo.currentText() != "Full Length":
+ self._load_segment_distances(next_id)
+
+ def _load_segment_distances(self, member_id):
+ if not member_id or not self._is_valid_segment_id(member_id):
+ return
+ record = self.segment_chain.setdefault(member_id, {})
+ if self._is_first_segment(member_id):
+ record.setdefault("start", 0.0)
+ elif "start" not in record:
+ previous_id = self._get_previous_segment_id(member_id)
+ if previous_id:
+ previous = self.segment_chain.get(previous_id, {})
+ if "end" in previous:
+ record["start"] = previous["end"]
+
+ self._suppress_distance_updates = True
+ try:
+ if "start" in record:
+ self._set_line_edit_value(self.distance_start_input, record["start"])
+ else:
+ self.distance_start_input.clear()
+ if "end" in record:
+ self._set_line_edit_value(self.distance_end_input, record["end"])
+ else:
+ self.distance_end_input.clear()
+ finally:
+ self._suppress_distance_updates = False
+
+ def _update_segment_record(self, member_id, start=None, end=None):
+ if not member_id or not self._is_valid_segment_id(member_id):
+ return
+ record = self.segment_chain.setdefault(member_id, {})
+ if start is not None:
+ record["start"] = start
+ if end is not None:
+ record["end"] = end
+
+ def _get_total_span(self):
+ return self._parse_float(self.length_input.text()) or 0.0
+
+ def _is_first_segment(self, member_id):
+ _, index = self._split_member_id(member_id)
+ return index == 1
+
+ def _get_next_segment_id(self, member_id):
+ base, index = self._split_member_id(member_id)
+ if base is None or index is None:
+ return None
+ return f"{base}-{index + 1}"
+
+ def _get_previous_segment_id(self, member_id):
+ base, index = self._split_member_id(member_id)
+ if base is None or index is None or index <= 1:
+ return None
+ return f"{base}-{index - 1}"
+
+ def _split_member_id(self, member_id):
+ if "-" not in member_id:
+ return member_id, None
+ base, index = member_id.rsplit("-", 1)
+ try:
+ return base, int(index)
+ except ValueError:
+ return base, None
+
+ def _set_line_edit_value(self, line_edit, value):
+ if value is None:
+ return
+ text = f"{value:.3f}".rstrip("0").rstrip(".")
+ if not text:
+ text = "0"
+ previous_state = line_edit.blockSignals(True)
+ line_edit.setText(text)
+ line_edit.blockSignals(previous_state)
+
+ def validate_member_properties(self) -> bool:
+ if self.design_combo.currentText() != "Customized":
+ return True
+ required_fields = [
+ (self.total_depth_input, "Total Depth (d, mm)"),
+ (self.top_width_input, "Width of Top Flange (t_fw, mm)"),
+ (self.bottom_width_input, "Width of Bottom Flange (b_fw, mm)"),
+ ]
+ missing = []
+ for field, label in required_fields:
+ value = self._parse_float(field.text())
+ if value is None or value <= 0:
+ missing.append(label)
+ if missing:
+ QMessageBox.critical(
+ self,
+ "Incomplete Girder Inputs",
+ f"Please provide valid values for: {', '.join(missing)}.",
+ )
+ return False
+ return True
+
+ def _create_inner_box(self):
+ """Create a bordered box for grouped controls"""
+ box = QFrame()
+ box.setStyleSheet("""
+ QFrame {
+ border: 1px solid #b0b0b0;
+ border-radius: 6px;
+ background-color: #ffffff;
+ }
+ QFrame QComboBox, QFrame QLineEdit {
+ border: none;
+ border-bottom: 1px solid #d0d0d0;
+ border-radius: 0px;
+ min-height: 28px;
+ padding: 4px 8px;
+ background-color: #ffffff;
+ }
+ QFrame QComboBox:hover, QFrame QLineEdit:hover {
+ border-bottom: 1px solid #5d5d5d;
+ }
+ QFrame QComboBox:focus, QFrame QLineEdit:focus {
+ border-bottom: 1px solid #90AF13;
+ }
+ QFrame QLabel {
+ border: none;
+ padding: 0px;
+ margin: 0px;
+ }
+ """)
+ return box
+
+ def _create_small_label(self, text):
+ """Create a smaller label for compact layouts"""
+ label = QLabel(text)
+ label.setStyleSheet("""
+ QLabel {
+ color: #2b2b2b;
+ font-size: 11px;
+ font-weight: 500;
+ background: transparent;
+ border: none;
+ padding: 0px;
+ margin: 0px;
+ }
+ """)
+ label.setAutoFillBackground(False)
+ return label
+
+ def _add_box_row(self, layout, row, label_text, widget, visibility_list=None):
+ """Add a row to a box grid layout"""
+ label = self._create_small_label(label_text)
+ layout.addWidget(label, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ layout.addWidget(widget, row, 1)
+ if visibility_list is not None:
+ visibility_list.append((label, widget))
+ return row + 1
+
+ def _set_row_visibility(self, rows, visible):
+ for label, widget in rows:
+ label.setVisible(visible)
+ widget.setVisible(visible)
+
+ def _populate_rolled_section_combo(self):
+ designations = sorted(girder_properties.list_available_sections().keys())
+ if not designations:
+ designations = [
+ "ISMB 500", "ISMB 550", "ISMB 600",
+ "ISWB 500", "ISWB 550", "ISWB 600",
+ ]
+ self.is_section_combo.clear()
+ self.is_section_combo.addItems(designations)
+
+ def _configure_restraint_fields(self):
+ torsion_items = self._constant_items("VALUES_TORSIONAL_RESTRAINT")
+ warping_items = self._constant_items("VALUES_WARPING_RESTRAINT")
+ web_type_items = self._constant_items("VALUES_WEB_TYPE")
+
+ self._reload_combo_items(self.torsion_combo, torsion_items)
+ self._reload_combo_items(self.warping_combo, warping_items)
+ self._reload_combo_items(self.web_type_combo, web_type_items)
+
+ @staticmethod
+ def _reload_combo_items(combo, items):
+ block = combo.blockSignals(True)
+ combo.clear()
+ combo.addItems(items)
+ combo.setCurrentIndex(0 if items else -1)
+ combo.blockSignals(block)
+
+ @staticmethod
+ def _constant_items(constant_name):
+ return list(globals().get(constant_name, []))
+
+ def _update_preview(self):
+ if not hasattr(self, "section_preview"):
+ return
+
+ is_welded = self.type_combo.currentText().lower() == "welded"
+ if is_welded:
+ dims = self._gather_welded_dimensions()
+ caption = "Welded girder preview" if dims else "Enter depth and flange widths"
+ if dims:
+ self.section_preview.set_dimensions(
+ depth_mm=dims["depth_mm"],
+ flange_width_mm=dims["top_flange_width_mm"],
+ bottom_flange_width_mm=dims["bottom_flange_width_mm"],
+ web_thickness_mm=dims["web_thickness_mm"],
+ flange_thickness_mm=dims["top_flange_thickness_mm"],
+ bottom_flange_thickness_mm=dims["bottom_flange_thickness_mm"],
+ show_welds=True,
+ )
+ else:
+ self.section_preview.clear()
+ else:
+ designation = self.is_section_combo.currentText()
+ beam = girder_properties.get_beam_profile(designation)
+ outline = girder_properties.get_rolled_section(designation) if beam is None else None
+ has_data = bool(beam or outline)
+ caption = f"Rolled section • {designation}" if has_data else "Rolled section unavailable"
+ if beam:
+ self.section_preview.set_section(beam)
+ elif outline:
+ self.section_preview.set_dimensions(
+ depth_mm=outline["depth_mm"],
+ flange_width_mm=outline["top_flange_width_mm"],
+ bottom_flange_width_mm=outline["bottom_flange_width_mm"],
+ web_thickness_mm=outline["web_thickness_mm"],
+ flange_thickness_mm=outline["top_flange_thickness_mm"],
+ bottom_flange_thickness_mm=outline["bottom_flange_thickness_mm"],
+ )
+ else:
+ self.section_preview.clear()
+
+ if hasattr(self, "preview_caption"):
+ self.preview_caption.setText(caption)
+ self._update_section_properties()
+
+ def _gather_welded_dimensions(self):
+ depth = self._parse_float(self.total_depth_input.text())
+ top_width = self._parse_float(self.top_width_input.text())
+ bottom_width = self._parse_float(self.bottom_width_input.text()) or top_width
+
+ if not depth or not top_width or not bottom_width:
+ return None
+
+ web_thickness = max(8.0, depth * 0.02)
+ flange_thickness = max(10.0, depth * 0.03)
+
+ return {
+ "designation": "Custom Welded Girder",
+ "section_type": "welded",
+ "depth_mm": depth,
+ "top_flange_width_mm": top_width,
+ "bottom_flange_width_mm": bottom_width,
+ "web_thickness_mm": web_thickness,
+ "top_flange_thickness_mm": flange_thickness,
+ "bottom_flange_thickness_mm": flange_thickness,
+ }
+
+ def _update_section_properties(self):
+ if not self.section_property_inputs:
+ return
+ values = None
+ if self.type_combo.currentText().lower() == "welded":
+ dims = self._gather_welded_dimensions()
+ if dims:
+ values = self._compute_welded_properties(dims)
+ else:
+ designation = self.is_section_combo.currentText()
+ values = self._fetch_rolled_properties(designation)
+ if values:
+ self._apply_section_properties(values)
+ else:
+ self._clear_section_properties()
+
+ def _fetch_rolled_properties(self, designation):
+ if not designation:
+ return None
+ beam = girder_properties.get_beam_profile(designation)
+ if not beam:
+ return None
+ values = {
+ "Mass, M (Kg/m)": beam.mass_per_meter_kg,
+ "Sectional Area, a (cm2)": beam.area_cm2,
+ "2nd Moment of Area, Iz (cm4)": beam.moment_of_inertia_zz_cm4,
+ "2nd Moment of Area, Iy (cm4)": beam.moment_of_inertia_yy_cm4,
+ "Radius of Gyration, rz (cm)": beam.radius_of_gyration_z_cm,
+ "Radius of Gyration, ry (cm)": beam.radius_of_gyration_y_cm,
+ "Elastic Modulus, Zz (cm3)": beam.elastic_section_modulus_z_cm3,
+ "Elastic Modulus, Zy (cm3)": beam.elastic_section_modulus_y_cm3,
+ "Plastic Modulus, Zuz (cm3)": beam.plastic_section_modulus_z_cm3,
+ "Plastic Modulus, Zuy (cm3)": beam.plastic_section_modulus_y_cm3,
+ "Torsion Constant, It (cm4)": beam.torsion_constant_cm4,
+ "Warping Constant, Iw (cm6)": beam.warping_constant_cm6,
+ }
+ area = values.get("Sectional Area, a (cm2)")
+ iz = values.get("2nd Moment of Area, Iz (cm4)")
+ iy = values.get("2nd Moment of Area, Iy (cm4)")
+ if values.get("Radius of Gyration, rz (cm)") is None and area and iz:
+ values["Radius of Gyration, rz (cm)"] = math.sqrt(iz / area)
+ if values.get("Radius of Gyration, ry (cm)") is None and area and iy:
+ values["Radius of Gyration, ry (cm)"] = math.sqrt(iy / area)
+ return values
+
+ def _compute_welded_properties(self, dims):
+ depth = dims["depth_mm"]
+ top_width = dims["top_flange_width_mm"]
+ bottom_width = dims["bottom_flange_width_mm"]
+ web_thickness = dims["web_thickness_mm"]
+ top_thickness = dims["top_flange_thickness_mm"]
+ bottom_thickness = dims["bottom_flange_thickness_mm"]
+
+ h_web = max(depth - top_thickness - bottom_thickness, 1.0)
+ area_top = top_width * top_thickness
+ area_bottom = bottom_width * bottom_thickness
+ area_web = web_thickness * h_web
+ area_total_mm2 = area_top + area_bottom + area_web
+ area_cm2 = area_total_mm2 / 100.0
+ mass_kg_per_m = (area_total_mm2 / 1_000_000.0) * 7850.0
+
+ iz_web = (web_thickness * h_web ** 3) / 12.0
+ iz_top = (top_width * top_thickness ** 3) / 12.0
+ iz_bottom = (bottom_width * bottom_thickness ** 3) / 12.0
+ distance_top = h_web / 2.0 + top_thickness / 2.0
+ distance_bottom = h_web / 2.0 + bottom_thickness / 2.0
+ iz_top += area_top * distance_top ** 2
+ iz_bottom += area_bottom * distance_bottom ** 2
+ iz_cm4 = (iz_web + iz_top + iz_bottom) / 10000.0
+
+ iy_web = (h_web * web_thickness ** 3) / 12.0
+ iy_top = (top_thickness * top_width ** 3) / 12.0
+ iy_bottom = (bottom_thickness * bottom_width ** 3) / 12.0
+ iy_cm4 = (iy_web + iy_top + iy_bottom) / 10000.0
+
+ rz_cm = math.sqrt(iz_cm4 / area_cm2) if area_cm2 > 0 else None
+ ry_cm = math.sqrt(iy_cm4 / area_cm2) if area_cm2 > 0 else None
+
+ depth_cm = depth / 10.0
+ width_cm = max(top_width, bottom_width) / 10.0
+ zz_cm3 = iz_cm4 / (depth_cm / 2.0) if depth_cm > 0 else None
+ zy_cm3 = iy_cm4 / (width_cm / 2.0) if width_cm > 0 else None
+
+ zpl_major = (
+ area_top * distance_top +
+ area_bottom * distance_bottom +
+ (web_thickness * h_web ** 2) / 4.0
+ ) / 1000.0
+ zpl_minor = (
+ (top_thickness * top_width ** 2) / 4.0 +
+ (bottom_thickness * bottom_width ** 2) / 4.0 +
+ (h_web * web_thickness ** 2) / 4.0
+ ) / 1000.0
+
+ torsion_constant_cm4 = (
+ (top_width * top_thickness ** 3) / 3.0 +
+ (bottom_width * bottom_thickness ** 3) / 3.0 +
+ (h_web * web_thickness ** 3) / 3.0
+ ) / 10000.0
+
+ warping_constant_cm6 = (
+ ((top_width * top_thickness ** 3) + (bottom_width * bottom_thickness ** 3)) * h_web ** 2 / 24.0
+ ) / 1_000_000.0
+
+ return {
+ "Mass, M (Kg/m)": mass_kg_per_m,
+ "Sectional Area, a (cm2)": area_cm2,
+ "2nd Moment of Area, Iz (cm4)": iz_cm4,
+ "2nd Moment of Area, Iy (cm4)": iy_cm4,
+ "Radius of Gyration, rz (cm)": rz_cm,
+ "Radius of Gyration, ry (cm)": ry_cm,
+ "Elastic Modulus, Zz (cm3)": zz_cm3,
+ "Elastic Modulus, Zy (cm3)": zy_cm3,
+ "Plastic Modulus, Zuz (cm3)": zpl_major,
+ "Plastic Modulus, Zuy (cm3)": zpl_minor,
+ "Torsion Constant, It (cm4)": torsion_constant_cm4,
+ "Warping Constant, Iw (cm6)": warping_constant_cm6,
+ }
+
+ def _apply_section_properties(self, values):
+ for label, widget in self.section_property_inputs.items():
+ display = self._format_property_value(values.get(label))
+ previous = widget.blockSignals(True)
+ widget.setText(display)
+ widget.blockSignals(previous)
+
+ def _clear_section_properties(self):
+ for widget in self.section_property_inputs.values():
+ previous = widget.blockSignals(True)
+ widget.clear()
+ widget.blockSignals(previous)
+
+ @staticmethod
+ def _format_property_value(value):
+ if value is None:
+ return ""
+ if isinstance(value, (int, float)):
+ return f"{value:.2f}"
+ return str(value)
+
+ @staticmethod
+ def _parse_float(text):
+ try:
+ return float(text)
+ except (TypeError, ValueError):
+ return None
+
+class StiffenerDetailsTab(QWidget):
+ """Tab for Stiffener Details with compact layout"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.init_ui()
+
+ def init_ui(self):
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setFrameShape(QFrame.NoFrame)
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ scroll.setStyleSheet(
+ "QScrollArea { border: none; background: transparent; }"
+ "QScrollArea > QWidget > QWidget { background: transparent; }"
+ )
+ main_layout.addWidget(scroll)
+
+ container = QWidget()
+ scroll.setWidget(container)
+ container.setStyleSheet("background-color: #f4f4f4;")
+
+ container_layout = QVBoxLayout(container)
+ container_layout.setContentsMargins(0, 0, 0, 0)
+ container_layout.setSpacing(4)
+
+ # Combined card for inputs and description
+ card_frame = self._create_card_frame()
+ card_layout = QHBoxLayout(card_frame)
+ card_layout.setContentsMargins(18, 16, 18, 16)
+ card_layout.setSpacing(18)
+
+ # Left column - inputs
+ left_column = QWidget()
+ left_layout = QVBoxLayout(left_column)
+ left_layout.setContentsMargins(0, 0, 0, 0)
+ left_layout.setSpacing(12)
+
+ girder_row = QHBoxLayout()
+ girder_row.setContentsMargins(0, 0, 0, 0)
+ girder_row.setSpacing(10)
+
+ girder_label = QLabel("Select Girder Member:")
+ girder_label.setStyleSheet("font-size: 11px; font-weight: 600; color: #3a3a3a; border: none;")
+ girder_row.addWidget(girder_label)
+
+ self.girder_member_combo = QComboBox()
+ self.girder_member_combo.addItems(["G1-1", "G1-2", "G1-3", "All"])
+ apply_field_style(self.girder_member_combo)
+ self.girder_member_combo.setFixedWidth(190)
+ self.girder_member_combo.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+ girder_row.addWidget(self.girder_member_combo, 1)
+
+ left_layout.addLayout(girder_row)
+
+ stiffener_heading = QLabel("Stiffener Inputs")
+ stiffener_heading.setStyleSheet("font-size: 11px; font-weight: 700; color: #000000; border: none; margin-top: 4px;")
+ left_layout.addWidget(stiffener_heading)
+
+ inputs_grid = QGridLayout()
+ inputs_grid.setContentsMargins(0, 0, 0, 0)
+ inputs_grid.setHorizontalSpacing(12)
+ inputs_grid.setVerticalSpacing(10)
+ inputs_grid.setColumnMinimumWidth(0, 180)
+ inputs_grid.setColumnStretch(0, 0)
+ inputs_grid.setColumnStretch(1, 1)
+
+ self.intermediate_combo = QComboBox()
+ self.intermediate_combo.addItems(["No", "Yes - At Supports", "Yes - Spaced"])
+ apply_field_style(self.intermediate_combo)
+ row = self._add_form_row(inputs_grid, 0, "Intermediate Stiffener:", self.intermediate_combo)
+
+ self.spacing_field = OptimizableField("Intermediate Stiffener Spacing")
+ self.spacing_field.mode_combo.clear()
+ self.spacing_field.mode_combo.addItems(["NA", "Optimized", "Customized"])
+ self.spacing_field.on_mode_changed(self.spacing_field.mode_combo.currentText())
+ self._prepare_optimizable_field(self.spacing_field)
+ row = self._add_form_row(inputs_grid, row, "Intermediate Stiffener Spacing:", self.spacing_field)
+
+ self.longitudinal_combo = QComboBox()
+ self.longitudinal_combo.addItems(["None", "Yes and 1 stiffener", "Yes and 2 stiffeners"])
+ apply_field_style(self.longitudinal_combo)
+ row = self._add_form_row(inputs_grid, row, "Longitudinal Stiffener:", self.longitudinal_combo)
+
+ self.intermediate_thick_combo = QComboBox()
+ self.intermediate_thick_combo.addItems(["All", "Custom"])
+ apply_field_style(self.intermediate_thick_combo)
+ row = self._add_form_row(inputs_grid, row, "Intermediate Stiffener Thickness:", self.intermediate_thick_combo)
+
+ self.long_thick_combo = QComboBox()
+ self.long_thick_combo.addItems(["All", "Custom"])
+ self.long_thick_combo.setEnabled(False)
+ apply_field_style(self.long_thick_combo)
+ row = self._add_form_row(inputs_grid, row, "Longitudinal Stiffener Thickness:", self.long_thick_combo)
+
+ left_layout.addLayout(inputs_grid)
+
+ buckling_heading = QLabel("Web Buckling Details")
+ buckling_heading.setStyleSheet("font-size: 11px; font-weight: 700; color: #000000; border: none; margin-top: 4px;")
+ left_layout.addWidget(buckling_heading)
+
+ buckling_grid = QGridLayout()
+ buckling_grid.setContentsMargins(0, 0, 0, 0)
+ buckling_grid.setHorizontalSpacing(12)
+ buckling_grid.setVerticalSpacing(10)
+ buckling_grid.setColumnMinimumWidth(0, 180)
+
+ self.method_combo = QComboBox()
+ self.method_combo.addItems(VALUES_STIFFENER_DESIGN)
+ apply_field_style(self.method_combo)
+ self._add_form_row(buckling_grid, 0, "Shear Buckling Design Method:", self.method_combo)
+
+ left_layout.addLayout(buckling_grid)
+
+ card_layout.addWidget(left_column, 2)
+
+ # Right column - description
+ right_column = QWidget()
+ right_layout = QVBoxLayout(right_column)
+ right_layout.setContentsMargins(0, 0, 0, 0)
+ right_layout.setSpacing(8)
+
+ desc_heading = QLabel("Description")
+ desc_heading.setStyleSheet("font-size: 11px; font-weight: 700; color: #000000; border: none;")
+ right_layout.addWidget(desc_heading)
+
+ self.description_text = QTextEdit()
+ self.description_text.setReadOnly(True)
+ self.description_text.setPlaceholderText("Describe stiffener assumptions or notes here.")
+ self.description_text.setMinimumHeight(210)
+ self.description_text.setStyleSheet(
+ "QTextEdit { border: 1px solid #d0d0d0; border-radius: 6px; background: #ffffff; color: #3a3a3a; font-size: 11px; }"
+ )
+ right_layout.addWidget(self.description_text, 1)
+
+ card_layout.addWidget(right_column, 3)
+
+ container_layout.addWidget(card_frame)
+
+ # Dynamic image box
+ image_box = self._create_card_frame()
+ image_layout = QVBoxLayout(image_box)
+ image_layout.setContentsMargins(16, 16, 16, 16)
+ image_layout.setSpacing(8)
+
+ self.dynamic_image_label = QLabel("Dynamic Image")
+ self.dynamic_image_label.setAlignment(Qt.AlignCenter)
+ self.dynamic_image_label.setMinimumHeight(140)
+ self.dynamic_image_label.setStyleSheet(
+ "QLabel { border: 1px solid #d8d8d8; border-radius: 8px; background-color: #f8f8f8; "
+ "font-weight: 600; color: #5b5b5b; font-size: 11px; }"
+ )
+ image_layout.addWidget(self.dynamic_image_label)
+ container_layout.addWidget(image_box)
+
+
+ # Signals
+ self.longitudinal_combo.currentTextChanged.connect(self.on_longitudinal_changed)
+
+ def _create_card_frame(self):
+ card = QFrame()
+ card.setStyleSheet(
+ "QFrame { border: 1px solid #d6d6d6; border-radius: 8px; background-color: #f7f7f7; }"
+ )
+ return card
+
+ def _create_label(self, text):
+ label = QLabel(text)
+ label.setStyleSheet("font-size: 11px; color: #3a3a3a; border: none;")
+ return label
+
+ def _add_form_row(self, layout, row, text, widget):
+ label = self._create_label(text)
+ layout.addWidget(label, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ layout.addWidget(widget, row, 1)
+ return row + 1
+
+ def _prepare_optimizable_field(self, field):
+ field.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ apply_field_style(field.mode_combo)
+ apply_field_style(field.input_field)
+
+ def on_longitudinal_changed(self, text):
+ has_longitudinal = text.lower().startswith("yes")
+ self.long_thick_combo.setEnabled(has_longitudinal)
+
+class CrossBracingDetailsTab(QWidget):
+ """Tab for Cross-Bracing Details with visual previews"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.init_ui()
+
+ def init_ui(self):
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setFrameShape(QFrame.NoFrame)
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ scroll.setStyleSheet(
+ "QScrollArea { border: none; background: transparent; }"
+ "QScrollArea > QWidget > QWidget { background: transparent; }"
+ )
+ main_layout.addWidget(scroll)
+
+ container = QWidget()
+ scroll.setWidget(container)
+
+ container_layout = QVBoxLayout(container)
+ container_layout.setContentsMargins(0, 0, 0, 0)
+ container_layout.setSpacing(16)
+
+ primary_card = self._create_card_frame()
+ card_layout = QHBoxLayout(primary_card)
+ card_layout.setContentsMargins(12, 10, 12, 10)
+ card_layout.setSpacing(10)
+
+ # Left column (inputs)
+ left_column = QWidget()
+ left_column.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+ left_layout = QVBoxLayout(left_column)
+ left_layout.setContentsMargins(0, 0, 0, 0)
+ left_layout.setSpacing(4)
+
+ selection_box = self._create_inner_box()
+ selection_layout = QGridLayout(selection_box)
+ selection_layout.setContentsMargins(8, 4, 8, 4)
+ selection_layout.setHorizontalSpacing(8)
+ selection_layout.setVerticalSpacing(4)
+ selection_layout.setColumnMinimumWidth(0, 130)
+ selection_layout.setColumnStretch(1, 1)
+
+ self.select_girders_combo = QComboBox()
+ self.select_girders_combo.addItems(["G1 to G2", "G3 to G4", "All"])
+ apply_field_style(self.select_girders_combo)
+ selection_layout.addWidget(self._create_label("Select Girders:"), 0, 0)
+ selection_layout.addWidget(self.select_girders_combo, 0, 1)
+
+ self.member_id_combo = QComboBox()
+ self.member_id_combo.addItems(["B1-1 to B1-15", "B2-1 to B2-10", "Custom"])
+ apply_field_style(self.member_id_combo)
+ selection_layout.addWidget(self._create_label("Member ID:"), 1, 0)
+ selection_layout.addWidget(self.member_id_combo, 1, 1)
+
+ left_layout.addWidget(selection_box)
+
+ inputs_box = self._create_inner_box()
+ inputs_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+ inputs_layout = QVBoxLayout(inputs_box)
+ inputs_layout.setContentsMargins(12, 8, 12, 8)
+ inputs_layout.setSpacing(6)
+ inputs_layout.addWidget(self._create_heading_label("Section Inputs:"))
+
+ inputs_grid = QGridLayout()
+ inputs_grid.setContentsMargins(0, 0, 0, 0)
+ inputs_grid.setHorizontalSpacing(16)
+ inputs_grid.setVerticalSpacing(12)
+ inputs_grid.setColumnMinimumWidth(0, 130)
+ inputs_grid.setColumnStretch(0, 0)
+ inputs_grid.setColumnStretch(1, 1)
+
+ self.design_combo = QComboBox()
+ self.design_combo.addItems(["Customized", "Optimized"])
+ apply_field_style(self.design_combo)
+ row = self._add_grid_row(inputs_grid, 0, "Design:", self.design_combo)
+
+ self.bracing_type_combo = QComboBox()
+ self.bracing_type_combo.addItems(["K-Bracing", "X-Bracing", "Diagonal", "Horizontal"])
+ apply_field_style(self.bracing_type_combo)
+ row = self._add_grid_row(inputs_grid, row, "Type of Bracing:", self.bracing_type_combo)
+
+ section_options = [
+ "ISA 50 x 50 x 6", "ISA 65 x 65 x 6", "ISA 75 x 75 x 6",
+ "ISA 90 x 90 x 8", "ISA 100 x 100 x 8", "ISA 110 x 110 x 10",
+ "ISA 130 x 130 x 10", "ISMC 75", "ISMC 100", "ISMC 125",
+ "ISMC 150", "2-ISA 65 x 65 x 6", "2-ISA 75 x 75 x 6"
+ ]
+
+ self.bracing_section_combo = QComboBox()
+ self.bracing_section_combo.addItems(section_options)
+ apply_field_style(self.bracing_section_combo)
+ row = self._add_grid_row(inputs_grid, row, "Bracing Section:", self.bracing_section_combo)
+
+ self.top_bracket_type_combo = QComboBox()
+ self.top_bracket_type_combo.addItems(["Double Angles", "Single Angle", "Channel"])
+ apply_field_style(self.top_bracket_type_combo)
+ row = self._add_grid_row(inputs_grid, row, "Top Bracket Section:", self.top_bracket_type_combo)
+
+ self.top_bracket_size_combo = QComboBox()
+ self.top_bracket_size_combo.addItems(section_options)
+ apply_field_style(self.top_bracket_size_combo)
+ row = self._add_grid_row(inputs_grid, row, "Top Bracket Size:", self.top_bracket_size_combo)
+
+ self.bottom_bracket_type_combo = QComboBox()
+ self.bottom_bracket_type_combo.addItems(["Double Angles", "Single Angle", "Channel"])
+ apply_field_style(self.bottom_bracket_type_combo)
+ row = self._add_grid_row(inputs_grid, row, "Bottom Bracket Section:", self.bottom_bracket_type_combo)
+
+ self.bottom_bracket_size_combo = QComboBox()
+ self.bottom_bracket_size_combo.addItems(section_options)
+ apply_field_style(self.bottom_bracket_size_combo)
+ row = self._add_grid_row(inputs_grid, row, "Bottom Bracket Size:", self.bottom_bracket_size_combo)
+
+ self.spacing_input = QLineEdit()
+ self.spacing_input.setPlaceholderText("Spacing (mm)")
+ self.spacing_input.setValidator(QDoubleValidator(0, 100000, 2))
+ apply_field_style(self.spacing_input)
+ self._add_grid_row(inputs_grid, row, "Spacing:", self.spacing_input)
+
+ inputs_layout.addLayout(inputs_grid)
+ left_layout.addWidget(inputs_box)
+
+ card_layout.addWidget(left_column)
+
+ # Right column (previews)
+ right_column = QWidget()
+ right_column.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+ right_layout = QVBoxLayout(right_column)
+ right_layout.setContentsMargins(0, 0, 0, 0)
+ right_layout.setSpacing(14)
+
+ type_box = self._create_inner_box()
+ type_layout = QVBoxLayout(type_box)
+ type_layout.setContentsMargins(12, 10, 12, 10)
+ type_layout.setSpacing(10)
+ type_layout.addWidget(self._create_heading_label("Type of Bracing"))
+ self.bracing_image_label = self._create_image_placeholder(210)
+ type_layout.addWidget(self.bracing_image_label)
+ right_layout.addWidget(type_box)
+
+ self.bracing_preview_box, self.bracing_preview_label = self._create_preview_box("Bracing")
+ right_layout.addWidget(self.bracing_preview_box)
+
+ self.top_bracket_preview_box, self.top_bracket_preview_label = self._create_preview_box("Top Bracket")
+ right_layout.addWidget(self.top_bracket_preview_box)
+
+ self.bottom_bracket_preview_box, self.bottom_bracket_preview_label = self._create_preview_box("Bottom Bracket")
+ right_layout.addWidget(self.bottom_bracket_preview_box)
+
+ card_layout.addWidget(right_column)
+ card_layout.setStretch(0, 3)
+ card_layout.setStretch(1, 2)
+ container_layout.addWidget(primary_card)
+ container_layout.addStretch()
+
+ self.bracing_type_combo.currentTextChanged.connect(self._update_previews)
+ self.bracing_section_combo.currentTextChanged.connect(self._update_previews)
+ self.top_bracket_size_combo.currentTextChanged.connect(self._update_previews)
+ self.bottom_bracket_size_combo.currentTextChanged.connect(self._update_previews)
+ self._update_previews()
+
+ def _create_card_frame(self):
+ card = QFrame()
+ card.setStyleSheet("QFrame { border: 1px solid #d0d0d0; border-radius: 12px; background-color: #ffffff; }")
+ return card
+
+ def _create_inner_box(self):
+ box = QFrame()
+ box.setStyleSheet(
+ "QFrame { border: 1px solid #cfcfcf; border-radius: 8px; background-color: #ffffff; }"
+ "QFrame QComboBox, QFrame QLineEdit { border: none; border-bottom: 1px solid #d0d0d0; border-radius: 0px; min-height: 28px; padding: 4px 8px; background-color: #ffffff; }"
+ "QFrame QComboBox:hover, QFrame QLineEdit:hover { border-bottom: 1px solid #5d5d5d; }"
+ "QFrame QComboBox:focus, QFrame QLineEdit:focus { border-bottom: 1px solid #90AF13; }"
+ "QFrame QLabel { border: none; }"
+ )
+ return box
+
+ def _create_heading_label(self, text):
+ label = QLabel(text)
+ label.setStyleSheet("font-size: 12px; font-weight: 600; color: #4b4b4b; border: none;")
+ return label
+
+ def _create_label(self, text):
+ label = QLabel(text)
+ label.setStyleSheet("font-size: 11px; color: #4b4b4b; border: none;")
+ return label
+
+ def _add_grid_row(self, layout, row, text, widget):
+ label = self._create_label(text)
+ layout.addWidget(label, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ layout.addWidget(widget, row, 1)
+ return row + 1
+
+ def _create_image_placeholder(self, height):
+ label = QLabel("Bracing Preview")
+ label.setAlignment(Qt.AlignCenter)
+ label.setMinimumHeight(height)
+ label.setStyleSheet("QLabel { border: 1px solid #d0d0d0; border-radius: 10px; background-color: #f7f7f7; font-weight: bold; color: #5b5b5b; }")
+ return label
+
+ def _create_preview_box(self, title):
+ box = self._create_inner_box()
+ layout = QVBoxLayout(box)
+ layout.setContentsMargins(12, 10, 12, 10)
+ layout.setSpacing(8)
+ layout.addWidget(self._create_heading_label(title))
+ image = self._create_image_placeholder(120)
+ layout.addWidget(image)
+ return box, image
+
+ def _update_previews(self):
+ self.bracing_image_label.setText(self.bracing_type_combo.currentText())
+ self.bracing_preview_label.setText(self.bracing_section_combo.currentText())
+ self.top_bracket_preview_label.setText(self.top_bracket_size_combo.currentText())
+ self.bottom_bracket_preview_label.setText(self.bottom_bracket_size_combo.currentText())
+
+class EndDiaphragmDetailsTab(QWidget):
+ """Tab for End Diaphragm Details with type-specific layouts"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.init_ui()
+
+ def init_ui(self):
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setFrameShape(QFrame.NoFrame)
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ scroll.setStyleSheet(
+ "QScrollArea { border: none; background: transparent; }"
+ "QScrollArea > QWidget > QWidget { background: transparent; }"
+ )
+ main_layout.addWidget(scroll)
+
+ container = QWidget()
+ scroll.setWidget(container)
+
+ container_layout = QVBoxLayout(container)
+ container_layout.setContentsMargins(0, 0, 0, 0)
+ container_layout.setSpacing(8)
+
+ self.type_stack = QStackedWidget()
+ container_layout.addWidget(self.type_stack)
+
+ self.views = {}
+ self.view_order = []
+ self.type_selector_map = {}
+ self.type_selectors = []
+ self.current_type = None
+ self.block_type_sync = False
+
+ cross_view, cross_selector = self._build_cross_bracing_view()
+ self._add_type_view("Cross Bracing", cross_view, cross_selector)
+ rolled_view, rolled_selector = self._build_rolled_view()
+ self._add_type_view("Rolled Beam", rolled_view, rolled_selector)
+ welded_view, welded_selector = self._build_welded_view()
+ self._add_type_view("Welded Beam", welded_view, welded_selector)
+
+ self._set_current_type("Cross Bracing")
+
+ def _add_type_view(self, key, widget, type_selector):
+ self.views[key] = widget
+ self.view_order.append(key)
+ self.type_stack.addWidget(widget)
+ self.type_selector_map[key] = type_selector
+ self.type_selectors.append(type_selector)
+ type_selector.currentTextChanged.connect(self._handle_type_selection)
+
+ # ---- Shared helpers ----
+ def _create_card_frame(self):
+ card = QFrame()
+ card.setStyleSheet("QFrame { border: 1px solid #d0d0d0; border-radius: 12px; background-color: #ffffff; }")
+ return card
+
+ def _create_inner_box(self):
+ box = QFrame()
+ box.setStyleSheet(
+ "QFrame { border: 1px solid #cfcfcf; border-radius: 8px; background-color: #ffffff; padding: 0px; margin: 0px; }"
+ "QFrame QComboBox, QFrame QLineEdit { border: none; border-bottom: 1px solid #d0d0d0; border-radius: 0px; min-height: 28px; padding: 4px 8px; background-color: #ffffff; }"
+ "QFrame QComboBox:hover, QFrame QLineEdit:hover { border-bottom: 1px solid #5d5d5d; }"
+ "QFrame QComboBox:focus, QFrame QLineEdit:focus { border-bottom: 1px solid #90AF13; }"
+ "QFrame QLabel { border: none; padding: 0px; margin: 0px; }"
+ )
+ return box
+
+ def _create_heading_label(self, text):
+ label = QLabel(text)
+ label.setStyleSheet("font-size: 12px; font-weight: 600; color: #4b4b4b; border: none; padding: 0px; margin: 0px;")
+ return label
+
+ def _create_label(self, text):
+ label = QLabel(text)
+ label.setStyleSheet("font-size: 11px; color: #4b4b4b; border: none;")
+ return label
+
+ def _add_grid_row(self, layout, row, text, widget):
+ label = self._create_label(text)
+ layout.addWidget(label, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ layout.addWidget(widget, row, 1)
+ return row + 1
+
+ def _create_image_placeholder(self, text, min_height=140):
+ label = QLabel(text)
+ label.setAlignment(Qt.AlignCenter)
+ label.setMinimumHeight(min_height)
+ label.setStyleSheet("QLabel { border: 1px solid #d0d0d0; border-radius: 10px; background-color: #f7f7f7; font-weight: bold; color: #5b5b5b; }")
+ return label
+
+ def _create_line_edit(self, placeholder=""):
+ line_edit = QLineEdit()
+ if placeholder:
+ line_edit.setPlaceholderText(placeholder)
+ apply_field_style(line_edit)
+ return line_edit
+
+ def _create_selection_box(self):
+ box = self._create_inner_box()
+ box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ layout = QGridLayout(box)
+ layout.setContentsMargins(10, 8, 10, 8)
+ layout.setHorizontalSpacing(12)
+ layout.setVerticalSpacing(8)
+ layout.setColumnMinimumWidth(0, 120)
+ layout.setColumnStretch(1, 1)
+
+ girders_combo = QComboBox()
+ girders_combo.addItems(["G1 to G2", "G3 to G4", "All"])
+ apply_field_style(girders_combo)
+ layout.addWidget(self._create_label("Select Girders:"), 0, 0)
+ layout.addWidget(girders_combo, 0, 1)
+
+ member_combo = QComboBox()
+ member_combo.addItems(["E1-1, E1-2", "E2-1, E2-2", "Custom"])
+ apply_field_style(member_combo)
+ layout.addWidget(self._create_label("Member ID:"), 1, 0)
+ layout.addWidget(member_combo, 1, 1)
+
+ return box
+
+ def _create_section_properties_box(self, title):
+ box = self._create_inner_box()
+ layout = QVBoxLayout(box)
+ layout.setContentsMargins(12, 10, 12, 10)
+ layout.setSpacing(10)
+ layout.addWidget(self._create_heading_label(title))
+
+ grid = QGridLayout()
+ grid.setContentsMargins(0, 0, 0, 0)
+ grid.setHorizontalSpacing(12)
+ grid.setVerticalSpacing(10)
+ grid.setColumnMinimumWidth(0, 150)
+ grid.setColumnStretch(0, 0)
+ grid.setColumnStretch(1, 1)
+
+ properties = [
+ "Mass, M (Kg/m)",
+ "Sectional Area, a (cm2)",
+ "2nd Moment of Area, Iz (cm4)",
+ "2nd Moment of Area, Iy (cm4)",
+ "Radius of Gyration, rz (cm)",
+ "Radius of Gyration, ry (cm)",
+ "Elastic Modulus, Zz (cm3)",
+ "Elastic Modulus, Zy (cm3)",
+ "Plastic Modulus, Zuz (cm3)",
+ "Plastic Modulus, Zuy (cm3)"
+ ]
+
+ inputs = {}
+ for row, name in enumerate(properties):
+ label = self._create_label(name)
+ field = self._create_line_edit()
+ grid.addWidget(label, row, 0)
+ grid.addWidget(field, row, 1)
+ inputs[name] = field
+
+ layout.addLayout(grid)
+ return box, inputs
+
+ # ---- View builders ----
+ def _build_cross_bracing_view(self):
+ view = self._create_card_frame()
+ layout = QHBoxLayout(view)
+ layout.setContentsMargins(12, 12, 12, 12)
+ layout.setSpacing(12)
+
+ left_column = QWidget()
+ left_layout = QVBoxLayout(left_column)
+ left_layout.setContentsMargins(0, 0, 0, 0)
+ left_layout.setSpacing(6)
+ left_layout.addWidget(self._create_selection_box())
+
+ inputs_box = self._create_inner_box()
+ inputs_layout = QVBoxLayout(inputs_box)
+ inputs_layout.setContentsMargins(12, 4, 12, 8)
+ inputs_layout.setSpacing(6)
+ title = self._create_heading_label("Section Inputs:")
+ title.setStyleSheet("font-size: 12px; font-weight: 600; color: #4b4b4b; border: none; margin-top: 0px; margin-bottom: 2px;")
+ inputs_layout.addWidget(title)
+
+ grid = QGridLayout()
+ grid.setContentsMargins(0, 0, 0, 0)
+ grid.setHorizontalSpacing(12)
+ grid.setVerticalSpacing(10)
+ grid.setColumnMinimumWidth(0, 130)
+ grid.setColumnStretch(0, 0)
+ grid.setColumnStretch(1, 1)
+
+ design_combo = QComboBox()
+ design_combo.addItems(["Customized", "Optimized"])
+ apply_field_style(design_combo)
+ row = self._add_grid_row(grid, 0, "Design:", design_combo)
+
+ type_selector = QComboBox()
+ type_selector.addItems(VALUES_END_DIAPHRAGM_TYPE)
+ type_selector.setCurrentText("Cross Bracing")
+ apply_field_style(type_selector)
+ row = self._add_grid_row(grid, row, "Type:", type_selector)
+
+ bracing_combo = QComboBox()
+ bracing_combo.addItems(["K-Bracing", "X-Bracing", "Diagonal", "Horizontal"])
+ apply_field_style(bracing_combo)
+ row = self._add_grid_row(grid, row, "Type of Bracing:", bracing_combo)
+
+ section_options = [
+ "Double Angles", "Single Angle", "Channel",
+ "ISA 100 x 100 x 8", "ISA 110 x 110 x 10"
+ ]
+
+ bracing_section = QComboBox()
+ bracing_section.addItems(section_options)
+ apply_field_style(bracing_section)
+ row = self._add_grid_row(grid, row, "Bracing Section:", bracing_section)
+
+ top_bracket = QComboBox()
+ top_bracket.addItems(section_options)
+ apply_field_style(top_bracket)
+ row = self._add_grid_row(grid, row, "Top Bracket Section:", top_bracket)
+
+ bottom_bracket = QComboBox()
+ bottom_bracket.addItems(section_options)
+ apply_field_style(bottom_bracket)
+ row = self._add_grid_row(grid, row, "Bottom Bracket Section:", bottom_bracket)
+
+ spacing_input = self._create_line_edit("Spacing (mm)")
+ spacing_input.setValidator(QDoubleValidator(0, 100000, 2))
+ self._add_grid_row(grid, row, "Spacing:", spacing_input)
+
+ inputs_layout.addLayout(grid)
+ inputs_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ left_layout.addWidget(inputs_box)
+ left_layout.addStretch()
+
+ layout.addWidget(left_column)
+
+ right_column = QWidget()
+ right_layout = QVBoxLayout(right_column)
+ right_layout.setContentsMargins(0, 0, 0, 0)
+ right_layout.setSpacing(10)
+
+ type_box = self._create_inner_box()
+ type_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ type_layout = QVBoxLayout(type_box)
+ type_layout.setContentsMargins(12, 8, 12, 10)
+ type_layout.setSpacing(6)
+ type_layout.addWidget(self._create_heading_label("Type of Bracing"))
+ type_layout.addWidget(self._create_image_placeholder("Bracing Layout", 170))
+ right_layout.addWidget(type_box)
+
+ for title in ["Bracing", "Top Bracket", "Bottom Bracket"]:
+ preview_box = self._create_inner_box()
+ preview_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ preview_layout = QVBoxLayout(preview_box)
+ preview_layout.setContentsMargins(12, 8, 12, 8)
+ preview_layout.setSpacing(6)
+ preview_layout.addWidget(self._create_heading_label(title))
+ preview_layout.addWidget(self._create_image_placeholder("Preview", 110))
+ right_layout.addWidget(preview_box)
+
+ right_layout.addStretch()
+ layout.addWidget(right_column)
+ layout.setStretch(0, 3)
+ layout.setStretch(1, 4)
+ return view, type_selector
+
+ def _build_rolled_view(self):
+ view = self._create_card_frame()
+ layout = QHBoxLayout(view)
+ layout.setContentsMargins(12, 12, 12, 12)
+ layout.setSpacing(12)
+
+ left_column = QWidget()
+ left_layout = QVBoxLayout(left_column)
+ left_layout.setContentsMargins(0, 0, 0, 0)
+ left_layout.setSpacing(8)
+ left_layout.addWidget(self._create_selection_box())
+
+ inputs_box = self._create_inner_box()
+ inputs_layout = QVBoxLayout(inputs_box)
+ inputs_layout.setContentsMargins(12, 4, 12, 8)
+ inputs_layout.setSpacing(6)
+ title = self._create_heading_label("Section Inputs")
+ title.setStyleSheet("font-size: 12px; font-weight: 600; color: #4b4b4b; border: none; margin-top: 0px; margin-bottom: 2px;")
+ inputs_layout.addWidget(title)
+
+ grid = QGridLayout()
+ grid.setContentsMargins(0, 0, 0, 0)
+ grid.setHorizontalSpacing(12)
+ grid.setVerticalSpacing(8)
+ grid.setColumnMinimumWidth(0, 130)
+ grid.setColumnStretch(0, 0)
+ grid.setColumnStretch(1, 1)
+
+ design_combo = QComboBox()
+ design_combo.addItems(["Customized", "Optimized"])
+ apply_field_style(design_combo)
+ row = self._add_grid_row(grid, 0, "Design:", design_combo)
+
+ type_selector = QComboBox()
+ type_selector.addItems(VALUES_END_DIAPHRAGM_TYPE)
+ type_selector.setCurrentText("Rolled Beam")
+ apply_field_style(type_selector)
+ row = self._add_grid_row(grid, row, "Type:", type_selector)
+
+ is_section_combo = QComboBox()
+ is_section_combo.addItems([
+ "ISMB 500", "ISMB 550", "ISMB 600",
+ "ISWB 500", "ISWB 550", "ISWB 600"
+ ])
+ apply_field_style(is_section_combo)
+ self._add_grid_row(grid, row, "IS Section:", is_section_combo)
+
+ inputs_layout.addLayout(grid)
+ inputs_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ left_layout.addWidget(inputs_box)
+ left_layout.addStretch()
+ layout.addWidget(left_column)
+
+ right_column = QWidget()
+ right_layout = QVBoxLayout(right_column)
+ right_layout.setContentsMargins(0, 0, 0, 0)
+ right_layout.setSpacing(10)
+
+ image_box = self._create_inner_box()
+ image_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ image_layout = QVBoxLayout(image_box)
+ image_layout.setContentsMargins(12, 8, 12, 10)
+ image_layout.setSpacing(6)
+ image_layout.addWidget(self._create_heading_label("Dynamic Image"))
+ image_layout.addWidget(self._create_image_placeholder("Rolled Section", 170))
+ right_layout.addWidget(image_box)
+
+ props_box, _ = self._create_section_properties_box("Section Properties:")
+ props_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ right_layout.addWidget(props_box)
+ right_layout.addStretch()
+
+ layout.addWidget(right_column)
+ return view, type_selector
+
+ def _build_welded_view(self):
+ view = self._create_card_frame()
+ layout = QHBoxLayout(view)
+ layout.setContentsMargins(12, 12, 12, 12)
+ layout.setSpacing(12)
+
+ left_column = QWidget()
+ left_layout = QVBoxLayout(left_column)
+ left_layout.setContentsMargins(0, 0, 0, 0)
+ left_layout.setSpacing(8)
+ left_layout.addWidget(self._create_selection_box())
+
+ inputs_box = self._create_inner_box()
+ inputs_layout = QVBoxLayout(inputs_box)
+ inputs_layout.setContentsMargins(12, 8, 12, 10)
+ inputs_layout.setSpacing(8)
+ inputs_layout.addWidget(self._create_heading_label("Section Inputs:"))
+
+ grid = QGridLayout()
+ grid.setContentsMargins(0, 0, 0, 0)
+ grid.setHorizontalSpacing(12)
+ grid.setVerticalSpacing(10)
+ grid.setColumnMinimumWidth(0, 150)
+ grid.setColumnStretch(0, 0)
+ grid.setColumnStretch(1, 1)
+
+ design_combo = QComboBox()
+ design_combo.addItems(["Customized", "Optimized"])
+ apply_field_style(design_combo)
+ row = self._add_grid_row(grid, 0, "Design:", design_combo)
+
+ type_selector = QComboBox()
+ type_selector.addItems(VALUES_END_DIAPHRAGM_TYPE)
+ type_selector.setCurrentText("Welded Beam")
+ apply_field_style(type_selector)
+ row = self._add_grid_row(grid, row, "Type:", type_selector)
+
+ symmetry_combo = QComboBox()
+ symmetry_combo.addItems(["Girder Symmetric", "Girder Unsymmetric"])
+ apply_field_style(symmetry_combo)
+ row = self._add_grid_row(grid, row, "Symmetry:", symmetry_combo)
+
+ total_depth = self._create_line_edit()
+ row = self._add_grid_row(grid, row, "Total Depth (mm):", total_depth)
+
+ web_thick_combo = QComboBox()
+ web_thick_combo.addItems(["All", "Custom"])
+ apply_field_style(web_thick_combo)
+ row = self._add_grid_row(grid, row, "Web Thickness (mm):", web_thick_combo)
+
+ top_width = self._create_line_edit()
+ row = self._add_grid_row(grid, row, "Width of Top Flange (mm):", top_width)
+
+ top_thickness_combo = QComboBox()
+ top_thickness_combo.addItems(["All", "Custom"])
+ apply_field_style(top_thickness_combo)
+ row = self._add_grid_row(grid, row, "Top Flange Thickness (mm):", top_thickness_combo)
+
+ bottom_width = self._create_line_edit()
+ row = self._add_grid_row(grid, row, "Width of Bottom Flange (mm):", bottom_width)
+
+ bottom_thickness_combo = QComboBox()
+ bottom_thickness_combo.addItems(["All", "Custom"])
+ apply_field_style(bottom_thickness_combo)
+ row = self._add_grid_row(grid, row, "Bottom Flange Thickness (mm):", bottom_thickness_combo)
+
+ bearing_thickness = self._create_line_edit()
+ self._add_grid_row(grid, row, "Bearing Stiffener Thickness (mm):", bearing_thickness)
+
+ inputs_layout.addLayout(grid)
+ inputs_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ left_layout.addWidget(inputs_box)
+ left_layout.addStretch()
+ layout.addWidget(left_column)
+
+ right_column = QWidget()
+ right_layout = QVBoxLayout(right_column)
+ right_layout.setContentsMargins(0, 0, 0, 0)
+ right_layout.setSpacing(10)
+
+ image_box = self._create_inner_box()
+ image_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ image_layout = QVBoxLayout(image_box)
+ image_layout.setContentsMargins(12, 8, 12, 10)
+ image_layout.setSpacing(6)
+ image_layout.addWidget(self._create_heading_label("Dynamic Image"))
+ image_layout.addWidget(self._create_image_placeholder("Welded Section", 170))
+ right_layout.addWidget(image_box)
+
+ props_box, _ = self._create_section_properties_box("Section Properties:")
+ props_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ right_layout.addWidget(props_box)
+ right_layout.addStretch()
+
+ layout.addWidget(right_column)
+ return view, type_selector
+
+ def _handle_type_selection(self, value):
+ if self.block_type_sync:
+ return
+ if value in self.view_order:
+ self._set_current_type(value)
+
+ def _set_current_type(self, target):
+ if target not in self.view_order:
+ return
+ if self.current_type == target:
+ return
+ self.current_type = target
+ index = self.view_order.index(target)
+ self.type_stack.setCurrentIndex(index)
+ self.block_type_sync = True
+ for selector in self.type_selectors:
+ selector.setCurrentText(target)
+ self.block_type_sync = False
+
+class CustomVehicleDialog(QDialog):
+ """Dialog for adding or editing custom live load vehicles"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Live Load Custom Vehicle Add/Edit")
+ self.setModal(True)
+ self.setMinimumWidth(420)
+ self.setMinimumHeight(500)
+ self.setStyleSheet("""
+ QDialog { background-color: #ffffff; }
+ QLabel { color: #2b2b2b; font-size: 11px; background: transparent; }
+ QLineEdit {
+ background-color: #ffffff;
+ border: 1px solid #8a8a8a;
+ border-radius: 4px;
+ padding: 4px 8px;
+ min-height: 24px;
+ color: #2b2b2b;
+ }
+ QLineEdit:focus { border: 1px solid #5a5a5a; }
+ QLineEdit:read-only { background-color: #f0f0f0; color: #5a5a5a; }
+ QPushButton {
+ background-color: #ffffff;
+ color: #2b2b2b;
+ border: 1px solid #8a8a8a;
+ border-radius: 4px;
+ padding: 5px 12px;
+ min-width: 50px;
+ }
+ QPushButton:hover { background-color: #e8e8e8; }
+ QPushButton:pressed { background-color: #d8d8d8; }
+ QTableWidget {
+ background-color: #ffffff;
+ border: 1px solid #8a8a8a;
+ gridline-color: #d0d0d0;
+ color: #2b2b2b;
+ }
+ QTableWidget::item { padding: 4px; }
+ QHeaderView::section {
+ background-color: #f0f0f0;
+ color: #2b2b2b;
+ border: 1px solid #d0d0d0;
+ padding: 4px;
+ font-weight: 600;
+ }
+ """)
+ self.init_ui()
+
+ def init_ui(self):
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(16, 16, 16, 16)
+ layout.setSpacing(12)
+
+ # Vehicle Name row
+ name_row = QHBoxLayout()
+ name_row.setSpacing(10)
+ name_label = QLabel("Vehicle Name:")
+ name_label.setStyleSheet("font-weight: 600;")
+ self.vehicle_name_input = QLineEdit()
+ self.vehicle_name_input.setFixedWidth(120)
+ name_row.addWidget(name_label)
+ name_row.addWidget(self.vehicle_name_input)
+ name_row.addStretch()
+ layout.addLayout(name_row)
+
+ # P# D# row with Add/Modify/Delete buttons
+ pd_button_row = QHBoxLayout()
+ pd_button_row.setSpacing(8)
+
+ p_label = QLabel("P#")
+ self.P_input = QLineEdit()
+ self.P_input.setFixedWidth(50)
+ pd_button_row.addWidget(p_label)
+ pd_button_row.addWidget(self.P_input)
+
+ d_label = QLabel("D#")
+ self.D_input = QLineEdit()
+ self.D_input.setFixedWidth(50)
+ pd_button_row.addWidget(d_label)
+ pd_button_row.addWidget(self.D_input)
+
+ pd_button_row.addStretch()
+
+ self.add_axle_button = QPushButton("Add")
+ self.modify_axle_button = QPushButton("Modify")
+ self.delete_axle_button = QPushButton("Delete")
+ pd_button_row.addWidget(self.add_axle_button)
+ pd_button_row.addWidget(self.modify_axle_button)
+ pd_button_row.addWidget(self.delete_axle_button)
+
+ layout.addLayout(pd_button_row)
+
+ # Table and diagram row
+ table_diagram_row = QHBoxLayout()
+ table_diagram_row.setSpacing(12)
+
+ # Axle table
+ self.axle_table = QTableWidget()
+ self.axle_table.setColumnCount(3)
+ self.axle_table.setHorizontalHeaderLabels(["No.", "Load (kN)", "Spacing (m)"])
+ self.axle_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+ self.axle_table.verticalHeader().setVisible(False)
+ self.axle_table.setMinimumHeight(120)
+ self.axle_table.setMaximumHeight(140)
+ table_diagram_row.addWidget(self.axle_table, 1)
+
+ # Axle diagram placeholder
+ axle_diagram = QLabel("Axle Layout Diagram")
+ axle_diagram.setAlignment(Qt.AlignCenter)
+ axle_diagram.setMinimumHeight(120)
+ axle_diagram.setStyleSheet("""
+ QLabel {
+ border: 1px solid #8a8a8a;
+ border-radius: 4px;
+ background: #ffffff;
+ color: #6a6a6a;
+ font-size: 10px;
+ }
+ """)
+ table_diagram_row.addWidget(axle_diagram, 1)
+
+ layout.addLayout(table_diagram_row)
+
+ # Input fields grid
+ fields_grid = QGridLayout()
+ fields_grid.setContentsMargins(0, 8, 0, 0)
+ fields_grid.setHorizontalSpacing(12)
+ fields_grid.setVerticalSpacing(10)
+ fields_grid.setColumnMinimumWidth(0, 240)
+
+ field_labels = [
+ "Minimum nose to tail distance (m):",
+ "Width of Wheel, w (mm):",
+ "Minimum Clearance from Carriageway\nEdge, f (mm):",
+ "Minimum Clearance from Crossing Vehicles,\ng (mm):",
+ "Wheel Spacing in Transverse Direction (m):",
+ "Impact Factor:",
+ ]
+
+ self.custom_fields = {}
+ for row, text in enumerate(field_labels):
+ lbl = QLabel(text)
+ field = QLineEdit()
+ if "Impact" in text:
+ field.setText("0.25")
+ field.setReadOnly(True)
+ field.setFixedWidth(100)
+ fields_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ fields_grid.addWidget(field, row, 1, Qt.AlignLeft | Qt.AlignVCenter)
+ self.custom_fields[text] = field
+
+ layout.addLayout(fields_grid)
+
+ # Bottom diagram - Clear Carriageway Width
+ bottom_diagram_label = QLabel("CLEAR CARRIAGEWAY WIDTH")
+ bottom_diagram_label.setAlignment(Qt.AlignCenter)
+ bottom_diagram_label.setStyleSheet("font-size: 9px; font-weight: 600; color: #5a5a5a; background: transparent;")
+ layout.addWidget(bottom_diagram_label)
+
+ bottom_diagram = QLabel("")
+ bottom_diagram.setAlignment(Qt.AlignCenter)
+ bottom_diagram.setMinimumHeight(80)
+ bottom_diagram.setStyleSheet("""
+ QLabel {
+ border: 1px solid #8a8a8a;
+ border-radius: 4px;
+ background: #ffffff;
+ }
+ """)
+ layout.addWidget(bottom_diagram)
+
+ layout.addStretch()
+
+class LoadingTab(QWidget):
+ """Loading tab with permanent load layout and load-type subtabs"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.custom_vehicle_dialog = CustomVehicleDialog(self)
+ self.init_ui()
+
+ def init_ui(self):
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+
+ self.load_tabs = QTabWidget()
+ self.load_tabs.setDocumentMode(True)
+ self.load_tabs.setStyleSheet(
+ "QTabWidget::pane { border: none; background: #f5f5f5; }"
+ "QTabBar::tab { background: #e8e8e8; color: #4b4b4b; border: 1px solid #cfcfcf;"
+ " border-bottom: none; padding: 8px 20px; margin-right: 2px; min-width: 120px;"
+ " font-size: 11px; border-top-left-radius: 6px; border-top-right-radius: 6px; }"
+ "QTabBar::tab:selected { background: #9ecb3d; color: #ffffff; font-weight: bold; }"
+ "QTabBar::tab:!selected { margin-top: 2px; }"
+ )
+
+ self.load_tabs.addTab(self._build_permanent_load_tab(), "Permanent Load")
+ self.load_tabs.addTab(self._build_live_load_tab(), "Live Load")
+ self.load_tabs.addTab(self._build_seismic_load_tab(), "Seismic Load")
+ self.load_tabs.addTab(self._build_wind_load_tab(), "Wind Load")
+ self.load_tabs.addTab(self._build_temperature_load_tab(), "Temperature Load")
+ self.load_tabs.addTab(self._create_placeholder_page("Custom Load"), "Custom Load")
+ self.load_tabs.addTab(self._build_load_combination_tab(), "Load Combination")
+ main_layout.addWidget(self.load_tabs)
+
+ def _build_permanent_load_tab(self):
+ page = QWidget()
+ page.setStyleSheet("background-color: #f5f5f5;")
+ page_layout = QVBoxLayout(page)
+ page_layout.setContentsMargins(12, 12, 12, 12)
+ page_layout.setSpacing(12)
+
+ content_row = QHBoxLayout()
+ content_row.setContentsMargins(0, 0, 0, 0)
+ content_row.setSpacing(16)
+
+ left_card = self._create_card()
+ left_card.setStyleSheet(
+ "QFrame { border: 1px solid #b2b2b2; border-radius: 10px; background-color: #ffffff; }"
+ )
+ left_layout = QVBoxLayout(left_card)
+ left_layout.setContentsMargins(16, 16, 16, 16)
+ left_layout.setSpacing(16)
+
+ self._add_load_section(left_layout, "Dead Load (DL):", [
+ ("Include Member Self Weight:", self._create_yes_no_combo()),
+ ("Self-weight factor:", self._create_line_edit()),
+ ("Include Concrete Deck Weight:", self._create_yes_no_combo()),
+ ])
+
+ self._add_load_section(left_layout, "Dead Load for Surfacing (DW):", [
+ ("Include Load from Wearing Course:", self._create_yes_no_combo()),
+ ])
+
+ self._add_load_section(left_layout, "Super-Imposed Dead Load (SIDL):", [
+ ("Include Load from Crash Barrier:", self._create_yes_no_combo()),
+ ("Include Load from Median:", self._create_yes_no_combo()),
+ ("Include Load from Railing:", self._create_yes_no_combo()),
+ ])
+
+ left_layout.addStretch()
+
+ right_card = self._create_card()
+ right_card.setStyleSheet(
+ "QFrame { border: 1px solid #9c9c9c; border-radius: 10px; background-color: #c8c8c8; }"
+ )
+ right_card.setMinimumWidth(270)
+ right_card.setMinimumHeight(360)
+ right_layout = QVBoxLayout(right_card)
+ right_layout.setContentsMargins(18, 18, 18, 18)
+ right_layout.setSpacing(12)
+ description_label = QLabel("Description Box")
+ description_label.setAlignment(Qt.AlignCenter)
+ description_label.setStyleSheet("font-size: 12px; font-weight: 700; color: #000000;")
+ description_label.setMinimumHeight(320)
+ right_layout.addWidget(description_label)
+
+ content_row.addWidget(left_card, 3)
+ content_row.addWidget(right_card, 2)
+
+ page_layout.addLayout(content_row)
+ page_layout.addSpacing(4)
+ return page
+
+ def _build_live_load_tab(self):
+ page = QWidget()
+ page.setStyleSheet("background-color: #f5f5f5;")
+ page_layout = QVBoxLayout(page)
+ page_layout.setContentsMargins(12, 12, 12, 12)
+ page_layout.setSpacing(12)
+
+ content_row = QHBoxLayout()
+ content_row.setContentsMargins(0, 0, 0, 0)
+ content_row.setSpacing(16)
+
+ left_card = self._create_card()
+ left_card.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 10px; background-color: #ffffff; }")
+ left_layout = QVBoxLayout(left_card)
+ left_layout.setContentsMargins(14, 14, 14, 14)
+ left_layout.setSpacing(8)
+
+ # Title without box
+ title = QLabel("Live Load (LL) Inputs:")
+ title.setStyleSheet("font-size: 12px; font-weight: 700; color: #3a3a3a; background: transparent; border: none;")
+ left_layout.addWidget(title)
+
+ irc_vehicles = [
+ "Class A", "Class 70R Wheeled", "Class 70R Tracked",
+ "Class AA Wheeled", "Class AA Tracked", "Class SV", "Fatigue Truck"
+ ]
+ self._add_checkbox_section(left_layout, "Vehicles from IRC 6:", irc_vehicles)
+
+ # Custom Vehicle header with Add/Edit buttons
+ custom_header = QHBoxLayout()
+ custom_header.setSpacing(8)
+ custom_label = QLabel("Custom Vehicle:")
+ custom_label.setStyleSheet("font-size: 12px; font-weight: 700; color: #3a3a3a; background: transparent; border: none;")
+ custom_header.addWidget(custom_label)
+ custom_header.addStretch()
+ self.custom_vehicle_add_button = QPushButton("Add")
+ self.custom_vehicle_edit_button = QPushButton("Edit")
+ for btn in (self.custom_vehicle_add_button, self.custom_vehicle_edit_button):
+ btn.setFixedWidth(50)
+ btn.setStyleSheet(
+ "QPushButton { background: #ffffff; color: #2f2f2f; border: 1px solid #7a7a7a; border-radius: 4px; padding: 4px 10px; }"
+ "QPushButton:hover { background: #f0f0f0; }"
+ "QPushButton:pressed { background: #e0e0e0; }"
+ )
+ custom_header.addWidget(btn)
+ left_layout.addLayout(custom_header)
+
+ # Vehicle Name 1 and 2 as simple checkbox rows (like reference image 2)
+ self.custom_vehicle_checkboxes = []
+ for index in range(2):
+ row_layout = QHBoxLayout()
+ row_layout.setContentsMargins(0, 0, 0, 0)
+ row_layout.setSpacing(8)
+ label = QLabel(f"Vehicle Name {index + 1}")
+ label.setStyleSheet("font-size: 11px; font-style: italic; color: #4b4b4b; background: transparent; border: none;")
+ checkbox = QCheckBox()
+ checkbox.setChecked(False)
+ row_layout.addWidget(label)
+ row_layout.addStretch()
+ row_layout.addWidget(checkbox)
+ left_layout.addLayout(row_layout)
+ self.custom_vehicle_checkboxes.append(checkbox)
+
+ # Braking Load from Vehicles section - includes IRC vehicles + Vehicle Name 1/2
+ braking_vehicles = irc_vehicles + ["Vehicle Name 1", "Vehicle Name 2"]
+ self._add_checkbox_section(left_layout, "Braking Load from Vehicles:", braking_vehicles)
+
+ # Bottom inputs with aligned widths
+ input_width = 120
+
+ # Eccentricity row
+ ecc_row = QHBoxLayout()
+ ecc_row.setSpacing(10)
+ ecc_label = QLabel("Eccentricity from top of Deck (m):")
+ ecc_label.setStyleSheet("font-size: 11px; font-weight: 600; color: #3a3a3a; background: transparent; border: none;")
+ ecc_label.setMinimumWidth(200)
+ self.eccentricity_input = QLineEdit()
+ self.eccentricity_input.setFixedWidth(input_width)
+ apply_field_style(self.eccentricity_input)
+ ecc_row.addWidget(ecc_label)
+ ecc_row.addWidget(self.eccentricity_input)
+ ecc_row.addStretch()
+ left_layout.addLayout(ecc_row)
+
+ # Footpath Pressure row with dropdown
+ footpath_row = QHBoxLayout()
+ footpath_row.setSpacing(10)
+ footpath_label = QLabel("Footpath Pressure (kN/mm2 ):")
+ footpath_label.setStyleSheet("font-size: 11px; font-weight: 600; color: #3a3a3a; background: transparent; border: none;")
+ footpath_label.setMinimumWidth(200)
+ self.footpath_mode_combo = QComboBox()
+ self.footpath_mode_combo.addItems(["Automatic", "User-defined"])
+ self.footpath_mode_combo.setFixedWidth(input_width)
+ apply_field_style(self.footpath_mode_combo)
+ footpath_row.addWidget(footpath_label)
+ footpath_row.addWidget(self.footpath_mode_combo)
+ footpath_row.addStretch()
+ left_layout.addLayout(footpath_row)
+
+ # Value input below footpath (aligned with dropdown above)
+ value_row = QHBoxLayout()
+ value_row.setContentsMargins(0, 0, 0, 0)
+ value_row.setSpacing(10)
+ value_spacer = QLabel("")
+ value_spacer.setMinimumWidth(200)
+ self.footpath_value_input = QLineEdit()
+ self.footpath_value_input.setPlaceholderText("Value")
+ self.footpath_value_input.setFixedWidth(input_width)
+ apply_field_style(self.footpath_value_input)
+ value_row.addWidget(value_spacer)
+ value_row.addWidget(self.footpath_value_input)
+ value_row.addStretch()
+ left_layout.addLayout(value_row)
+
+ left_layout.addStretch()
+
+ # Right description card
+ right_card = self._create_card()
+ right_card.setStyleSheet("QFrame { border: 1px solid #9c9c9c; border-radius: 10px; background-color: #d4d4d4; }")
+ right_card.setMinimumWidth(260)
+ right_card.setMinimumHeight(420)
+ right_layout = QVBoxLayout(right_card)
+ right_layout.setContentsMargins(16, 16, 16, 16)
+ right_layout.setSpacing(10)
+
+ # Description Box title - no box around it
+ desc_label = QLabel("Description Box")
+ desc_label.setAlignment(Qt.AlignCenter)
+ desc_label.setStyleSheet("font-size: 12px; font-weight: 700; color: #000000; background: transparent; border: none;")
+ right_layout.addWidget(desc_label)
+
+ description_text = (
+ "211.2 The braking effect on a simply supported span or a continuous unit of spans "
+ "or on any other type of bridge unit shall be assumed to have the following value:\n\n"
+ "a) In the case of a single lane or a two lane bridge: twenty percent of the first train "
+ "load plus ten percent of the load of the succeeding trains or part thereof, the train "
+ "loads in one lane only being considered for the purpose of this subclause. Where the "
+ "entire first train is not on the full span, the braking force shall be taken as equal to "
+ "twenty percent of the loads actually on the span or continuous unit of spans.\n"
+ "b) In the case of bridges having more than two lanes: as in (a) above for the first two "
+ "lanes plus five percent of the loads on the lanes in excess of two."
+ )
+ description_label = QLabel(description_text)
+ description_label.setWordWrap(True)
+ description_label.setStyleSheet("font-size: 11px; color: #4b4b4b; background: transparent; border: none;")
+ right_layout.addWidget(description_label)
+ right_layout.addStretch()
+
+ content_row.addWidget(left_card, 3)
+ content_row.addWidget(right_card, 2)
+ page_layout.addLayout(content_row)
+ page_layout.addSpacing(4)
+
+ self.custom_vehicle_add_button.clicked.connect(self.show_custom_vehicle_dialog)
+ self.custom_vehicle_edit_button.clicked.connect(self.show_custom_vehicle_dialog)
+ self.footpath_mode_combo.currentTextChanged.connect(self._on_footpath_mode_changed)
+ self._on_footpath_mode_changed(self.footpath_mode_combo.currentText())
+
+ return page
+
+ def _build_seismic_load_tab(self):
+ """Build the Seismic/Earthquake Load tab matching reference design"""
+ page = QWidget()
+ page.setStyleSheet("background-color: #f5f5f5;")
+ page_layout = QVBoxLayout(page)
+ page_layout.setContentsMargins(12, 12, 12, 12)
+ page_layout.setSpacing(12)
+
+ content_row = QHBoxLayout()
+ content_row.setContentsMargins(0, 0, 0, 0)
+ content_row.setSpacing(16)
+
+ # Left card with inputs
+ left_card = self._create_card()
+ left_card.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 10px; background-color: #ffffff; }")
+ left_layout = QVBoxLayout(left_card)
+ left_layout.setContentsMargins(16, 16, 16, 16)
+ left_layout.setSpacing(12)
+
+ # Title
+ title = QLabel("Seismic/Earthquake Load (EL) Inputs for Evaluation per IRC 6")
+ title.setStyleSheet("font-size: 12px; font-weight: 700; color: #2b2b2b; background: transparent; border: none;")
+ left_layout.addWidget(title)
+
+ label_style = "font-size: 11px; color: #3a3a3a; background: transparent; border: none;"
+ field_width = 120
+
+ # ===== Seismic Inputs Box =====
+ seismic_inputs_box = QFrame()
+ seismic_inputs_box.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 8px; background-color: #ffffff; }")
+ seismic_inputs_layout = QGridLayout(seismic_inputs_box)
+ seismic_inputs_layout.setContentsMargins(12, 12, 12, 12)
+ seismic_inputs_layout.setHorizontalSpacing(12)
+ seismic_inputs_layout.setVerticalSpacing(10)
+ seismic_inputs_layout.setColumnMinimumWidth(0, 200)
+
+ row = 0
+
+ # Seismic Zone
+ lbl = QLabel("Seismic Zone:")
+ lbl.setStyleSheet(label_style)
+ self.seismic_zone_combo = QComboBox()
+ self.seismic_zone_combo.addItems(["II", "III", "IV", "V"])
+ self.seismic_zone_combo.setFixedWidth(field_width)
+ apply_field_style(self.seismic_zone_combo)
+ seismic_inputs_layout.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ seismic_inputs_layout.addWidget(self.seismic_zone_combo, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Importance Factor
+ lbl = QLabel("Importance Factor:")
+ lbl.setStyleSheet(label_style)
+ self.importance_factor_input = QLineEdit()
+ self.importance_factor_input.setText("1")
+ self.importance_factor_input.setFixedWidth(field_width)
+ apply_field_style(self.importance_factor_input)
+ seismic_inputs_layout.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ seismic_inputs_layout.addWidget(self.importance_factor_input, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Type of Soil
+ lbl = QLabel("Type of Soil:")
+ lbl.setStyleSheet(label_style)
+ self.soil_type_combo = QComboBox()
+ self.soil_type_combo.addItems([
+ "Type I – Rocky or Hard Soil Sites (N>30)",
+ "Type II – Medium Soil Sites",
+ "Type III – Soft Soil Sites"
+ ])
+ self.soil_type_combo.setFixedWidth(field_width + 30)
+ apply_field_style(self.soil_type_combo)
+ seismic_inputs_layout.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ seismic_inputs_layout.addWidget(self.soil_type_combo, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Time Period
+ lbl = QLabel("Time Period:")
+ lbl.setStyleSheet(label_style)
+ self.time_period_input = QLineEdit()
+ self.time_period_input.setFixedWidth(field_width)
+ apply_field_style(self.time_period_input)
+ seismic_inputs_layout.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ seismic_inputs_layout.addWidget(self.time_period_input, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Damping Percentage
+ lbl = QLabel("Damping Percentage:")
+ lbl.setStyleSheet(label_style)
+ self.damping_input = QLineEdit()
+ self.damping_input.setText("2")
+ self.damping_input.setFixedWidth(field_width)
+ apply_field_style(self.damping_input)
+ seismic_inputs_layout.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ seismic_inputs_layout.addWidget(self.damping_input, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Response Reduction Factor
+ lbl = QLabel("Response Reduction Factor:")
+ lbl.setStyleSheet(label_style)
+ self.response_factor_combo = QComboBox()
+ self.response_factor_combo.addItems(["1", "2", "3", "4", "5"])
+ self.response_factor_combo.setCurrentText("1")
+ self.response_factor_combo.setFixedWidth(field_width)
+ apply_field_style(self.response_factor_combo)
+ seismic_inputs_layout.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ seismic_inputs_layout.addWidget(self.response_factor_combo, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Dead Load for Seismic Force
+ lbl = QLabel("Dead Load for Seismic Force (kN):")
+ lbl.setStyleSheet(label_style)
+ self.dead_load_seismic_combo = QComboBox()
+ self.dead_load_seismic_combo.addItems(["Automatic", "Custom"])
+ self.dead_load_seismic_combo.setFixedWidth(field_width)
+ apply_field_style(self.dead_load_seismic_combo)
+ seismic_inputs_layout.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ seismic_inputs_layout.addWidget(self.dead_load_seismic_combo, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Custom Value for Dead Load
+ self.dead_load_custom_input = QLineEdit()
+ self.dead_load_custom_input.setPlaceholderText("Custom Value")
+ self.dead_load_custom_input.setFixedWidth(field_width)
+ self.dead_load_custom_input.setEnabled(False)
+ apply_field_style(self.dead_load_custom_input)
+ seismic_inputs_layout.addWidget(self.dead_load_custom_input, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Live Load for Seismic Force
+ lbl = QLabel("Live Load for Seismic Force (kN):")
+ lbl.setStyleSheet(label_style)
+ self.live_load_seismic_combo = QComboBox()
+ self.live_load_seismic_combo.addItems(["Automatic", "Custom"])
+ self.live_load_seismic_combo.setFixedWidth(field_width)
+ apply_field_style(self.live_load_seismic_combo)
+ seismic_inputs_layout.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ seismic_inputs_layout.addWidget(self.live_load_seismic_combo, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Custom Value for Live Load
+ self.live_load_custom_input = QLineEdit()
+ self.live_load_custom_input.setPlaceholderText("Custom Value")
+ self.live_load_custom_input.setFixedWidth(field_width)
+ self.live_load_custom_input.setEnabled(False)
+ apply_field_style(self.live_load_custom_input)
+ seismic_inputs_layout.addWidget(self.live_load_custom_input, row, 1, Qt.AlignLeft)
+
+ left_layout.addWidget(seismic_inputs_box)
+
+ # ===== Computed Values Box =====
+ computed_box = QFrame()
+ computed_box.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 8px; background-color: #ffffff; }")
+ computed_layout = QGridLayout(computed_box)
+ computed_layout.setContentsMargins(12, 12, 12, 12)
+ computed_layout.setHorizontalSpacing(12)
+ computed_layout.setVerticalSpacing(10)
+ computed_layout.setColumnMinimumWidth(0, 200)
+
+ computed_fields = [
+ ("Zone Factor:", "zone_factor"),
+ ("Spectral Acceleration Coefficient:", "spectral_coeff"),
+ ("Horizontal Seismic Coefficient:", "horizontal_coeff"),
+ ("Vertical Seismic Coefficient:", "vertical_coeff"),
+ ]
+
+ self.seismic_computed_fields = {}
+ for idx, (label_text, field_name) in enumerate(computed_fields):
+ lbl = QLabel(label_text)
+ lbl.setStyleSheet(label_style)
+ field = QLineEdit()
+ field.setFixedWidth(field_width)
+ field.setReadOnly(True)
+ apply_field_style(field)
+ computed_layout.addWidget(lbl, idx, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ computed_layout.addWidget(field, idx, 1, Qt.AlignLeft)
+ self.seismic_computed_fields[field_name] = field
+
+ left_layout.addWidget(computed_box)
+ left_layout.addStretch()
+
+ # Right description card
+ right_card = self._create_card()
+ right_card.setStyleSheet("QFrame { border: 1px solid #9c9c9c; border-radius: 10px; background-color: #d4d4d4; }")
+ right_card.setMinimumWidth(200)
+ right_card.setMinimumHeight(400)
+ right_layout = QVBoxLayout(right_card)
+ right_layout.setContentsMargins(16, 16, 16, 16)
+ right_layout.setSpacing(10)
+
+ desc_title = QLabel("Description Box")
+ desc_title.setAlignment(Qt.AlignCenter)
+ desc_title.setStyleSheet("font-size: 12px; font-weight: 700; color: #2b2b2b; background: transparent; border: none;")
+ right_layout.addWidget(desc_title)
+
+ desc_text = QLabel("Importance factor for normal, important, and critical bridges.")
+ desc_text.setWordWrap(True)
+ desc_text.setStyleSheet("font-size: 11px; color: #4b4b4b; background: transparent; border: none;")
+ right_layout.addWidget(desc_text)
+ right_layout.addStretch()
+
+ content_row.addWidget(left_card, 3)
+ content_row.addWidget(right_card, 2)
+
+ page_layout.addLayout(content_row)
+
+ # Connect signals for enabling/disabling custom inputs
+ self.dead_load_seismic_combo.currentTextChanged.connect(self._on_dead_load_mode_changed)
+ self.live_load_seismic_combo.currentTextChanged.connect(self._on_live_load_mode_changed)
+
+ return page
+
+ def _on_dead_load_mode_changed(self, mode):
+ is_custom = mode == "Custom"
+ self.dead_load_custom_input.setEnabled(is_custom)
+ if not is_custom:
+ self.dead_load_custom_input.clear()
+
+ def _on_live_load_mode_changed(self, mode):
+ is_custom = mode == "Custom"
+ self.live_load_custom_input.setEnabled(is_custom)
+ if not is_custom:
+ self.live_load_custom_input.clear()
+
+ def _add_load_section(self, parent_layout, title, rows):
+ title_label = QLabel(title)
+ title_label.setStyleSheet("font-size: 12px; font-weight: 600; color: #3e3e3e; background: transparent; border: none;")
+ parent_layout.addWidget(title_label)
+
+ grid = QGridLayout()
+ grid.setContentsMargins(0, 0, 0, 0)
+ grid.setHorizontalSpacing(12)
+ grid.setVerticalSpacing(12)
+ grid.setColumnMinimumWidth(0, 230)
+ grid.setColumnStretch(0, 0)
+ grid.setColumnStretch(1, 1)
+
+ field_width = 170
+
+ for row_index, (label_text, widget) in enumerate(rows):
+ label = QLabel(label_text)
+ label.setStyleSheet("font-size: 11px; color: #4b4b4b; background: transparent; border: none;")
+ grid.addWidget(label, row_index, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+ if isinstance(widget, QComboBox):
+ widget.setFixedWidth(field_width)
+ elif isinstance(widget, QLineEdit):
+ widget.setFixedWidth(field_width)
+ grid.addWidget(widget, row_index, 1)
+
+ parent_layout.addLayout(grid)
+
+ def _add_checkbox_section(self, parent_layout, title, items):
+ title_label = QLabel(title)
+ title_label.setStyleSheet("font-size: 12px; font-weight: 600; color: #3e3e3e; background: transparent; border: none;")
+ parent_layout.addWidget(title_label)
+
+ grid = QGridLayout()
+ grid.setContentsMargins(0, 0, 0, 0)
+ grid.setHorizontalSpacing(12)
+ grid.setVerticalSpacing(8)
+ grid.setColumnStretch(0, 1)
+
+
+ for row, name in enumerate(items):
+ label = QLabel(name)
+ # Make Vehicle Name entries italic
+ if "Vehicle Name" in name:
+ label.setStyleSheet("font-size: 11px; font-style: italic; color: #4b4b4b; background: transparent; border: none; padding: 0px;")
+ else:
+ label.setStyleSheet("font-size: 11px; color: #4b4b4b; background: transparent; border: none; padding: 0px;")
+ checkbox = QCheckBox()
+ checkbox.setChecked(False)
+ grid.addWidget(label, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ grid.addWidget(checkbox, row, 1, Qt.AlignRight | Qt.AlignVCenter)
+
+ parent_layout.addLayout(grid)
+
+ def _on_footpath_mode_changed(self, mode):
+ is_custom = mode == "User-defined"
+ self.footpath_value_input.setEnabled(is_custom)
+ if not is_custom:
+ self.footpath_value_input.clear()
+
+ def show_custom_vehicle_dialog(self):
+ self.custom_vehicle_dialog.show()
+ self.custom_vehicle_dialog.raise_()
+ self.custom_vehicle_dialog.activateWindow()
+
+ def _create_yes_no_combo(self):
+ combo = QComboBox()
+ combo.addItems(VALUES_YES_NO)
+ combo.setCurrentText("Yes")
+ apply_field_style(combo)
+ return combo
+
+ def _create_line_edit(self):
+ line_edit = QLineEdit()
+ apply_field_style(line_edit)
+ return line_edit
+
+ def _create_card(self):
+ card = QFrame()
+ card.setStyleSheet("QFrame { border: 1px solid #cfcfcf; border-radius: 12px; background-color: #ffffff; }")
+ return card
+
+ def _build_wind_load_tab(self):
+ """Build the Wind Load tab matching reference design"""
+ page = QWidget()
+ page.setStyleSheet("background-color: #f5f5f5;")
+ page_layout = QVBoxLayout(page)
+ page_layout.setContentsMargins(12, 12, 12, 12)
+ page_layout.setSpacing(12)
+
+ content_row = QHBoxLayout()
+ content_row.setContentsMargins(0, 0, 0, 0)
+ content_row.setSpacing(16)
+
+ # Left card with inputs - use scroll area for many fields
+ left_card = self._create_card()
+ left_card.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 10px; background-color: #ffffff; }")
+ left_card_layout = QVBoxLayout(left_card)
+ left_card_layout.setContentsMargins(0, 0, 0, 0)
+ left_card_layout.setSpacing(0)
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setFrameShape(QFrame.NoFrame)
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ scroll.setStyleSheet(
+ "QScrollArea { border: none; background: transparent; }"
+ "QScrollArea > QWidget > QWidget { background: transparent; }"
+ )
+
+ scroll_content = QWidget()
+ scroll_content.setStyleSheet("background: #ffffff;")
+ left_layout = QVBoxLayout(scroll_content)
+ left_layout.setContentsMargins(16, 16, 16, 16)
+ left_layout.setSpacing(12)
+
+ label_style = "font-size: 11px; color: #3a3a3a; background: transparent; border: none;"
+ field_width = 120
+
+ # ===== Wind Load Inputs Box =====
+ wind_inputs_box = QFrame()
+ wind_inputs_box.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 8px; background-color: #ffffff; }")
+ wind_inputs_layout = QVBoxLayout(wind_inputs_box)
+ wind_inputs_layout.setContentsMargins(12, 12, 12, 12)
+ wind_inputs_layout.setSpacing(10)
+
+ wind_title = QLabel("Wind Load (WL) Inputs for Evaluation per IRC6")
+ wind_title.setStyleSheet("font-size: 12px; font-weight: 700; color: #2b2b2b; background: transparent; border: none;")
+ wind_inputs_layout.addWidget(wind_title)
+
+ wind_grid = QGridLayout()
+ wind_grid.setContentsMargins(0, 4, 0, 0)
+ wind_grid.setHorizontalSpacing(12)
+ wind_grid.setVerticalSpacing(8)
+ wind_grid.setColumnMinimumWidth(0, 220)
+
+ row = 0
+
+ # Basic Wind Speed
+ lbl = QLabel("Basic Wind Speed (m/s):")
+ lbl.setStyleSheet(label_style)
+ self.basic_wind_speed_input = QLineEdit()
+ self.basic_wind_speed_input.setFixedWidth(field_width)
+ apply_field_style(self.basic_wind_speed_input)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.basic_wind_speed_input, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Average Exposed Height
+ lbl = QLabel("Average Exposed Height (m):")
+ lbl.setStyleSheet(label_style)
+ self.avg_exposed_height_input = QLineEdit()
+ self.avg_exposed_height_input.setFixedWidth(field_width)
+ apply_field_style(self.avg_exposed_height_input)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.avg_exposed_height_input, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Type of Terrain
+ lbl = QLabel("Type of Terrain:")
+ lbl.setStyleSheet(label_style)
+ self.terrain_type_combo = QComboBox()
+ self.terrain_type_combo.addItems(["Plain", "Hilly", "Coastal"])
+ self.terrain_type_combo.setFixedWidth(field_width)
+ apply_field_style(self.terrain_type_combo)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.terrain_type_combo, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Site Topography
+ lbl = QLabel("Site Topography:")
+ lbl.setStyleSheet(label_style)
+ self.site_topography_combo = QComboBox()
+ self.site_topography_combo.addItems(["Flat", "Hilly", "Ridge", "Valley"])
+ self.site_topography_combo.setFixedWidth(field_width)
+ apply_field_style(self.site_topography_combo)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.site_topography_combo, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Gust Factor, G
+ lbl = QLabel("Gust Factor, G:")
+ lbl.setStyleSheet(label_style)
+ self.gust_factor_combo = QComboBox()
+ self.gust_factor_combo.addItems(["Automatic", "Custom"])
+ self.gust_factor_combo.setFixedWidth(field_width)
+ apply_field_style(self.gust_factor_combo)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.gust_factor_combo, row, 1, Qt.AlignLeft)
+ row += 1
+ self.gust_factor_value = QLineEdit()
+ self.gust_factor_value.setPlaceholderText("Value")
+ self.gust_factor_value.setFixedWidth(field_width)
+ self.gust_factor_value.setEnabled(False)
+ apply_field_style(self.gust_factor_value)
+ wind_grid.addWidget(self.gust_factor_value, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Drag Coefficient, CD
+ lbl = QLabel("Drag Coefficient, CD:")
+ lbl.setStyleSheet(label_style)
+ self.drag_coeff_combo = QComboBox()
+ self.drag_coeff_combo.addItems(["Automatic", "Custom"])
+ self.drag_coeff_combo.setFixedWidth(field_width)
+ apply_field_style(self.drag_coeff_combo)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.drag_coeff_combo, row, 1, Qt.AlignLeft)
+ row += 1
+ self.drag_coeff_value = QLineEdit()
+ self.drag_coeff_value.setPlaceholderText("Custom Value")
+ self.drag_coeff_value.setFixedWidth(field_width)
+ self.drag_coeff_value.setEnabled(False)
+ apply_field_style(self.drag_coeff_value)
+ wind_grid.addWidget(self.drag_coeff_value, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Drag Coefficient against Live Load, CDLL
+ lbl = QLabel("Drag Coefficient against Live Load, CDLL:")
+ lbl.setStyleSheet(label_style)
+ self.drag_coeff_ll_combo = QComboBox()
+ self.drag_coeff_ll_combo.addItems(["Automatic", "Custom"])
+ self.drag_coeff_ll_combo.setFixedWidth(field_width)
+ apply_field_style(self.drag_coeff_ll_combo)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.drag_coeff_ll_combo, row, 1, Qt.AlignLeft)
+ row += 1
+ self.drag_coeff_ll_value = QLineEdit()
+ self.drag_coeff_ll_value.setPlaceholderText("Value")
+ self.drag_coeff_ll_value.setFixedWidth(field_width)
+ self.drag_coeff_ll_value.setEnabled(False)
+ apply_field_style(self.drag_coeff_ll_value)
+ wind_grid.addWidget(self.drag_coeff_ll_value, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Lift Coefficient, CL
+ lbl = QLabel("Lift Coefficient, CL:")
+ lbl.setStyleSheet(label_style)
+ self.lift_coeff_combo = QComboBox()
+ self.lift_coeff_combo.addItems(["Automatic", "Custom"])
+ self.lift_coeff_combo.setFixedWidth(field_width)
+ apply_field_style(self.lift_coeff_combo)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.lift_coeff_combo, row, 1, Qt.AlignLeft)
+ row += 1
+ self.lift_coeff_value = QLineEdit()
+ self.lift_coeff_value.setPlaceholderText("Value")
+ self.lift_coeff_value.setFixedWidth(field_width)
+ self.lift_coeff_value.setEnabled(False)
+ apply_field_style(self.lift_coeff_value)
+ wind_grid.addWidget(self.lift_coeff_value, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Superstructure Area in Elevation
+ lbl = QLabel("Superstructure Area in Elevation (m2):")
+ lbl.setStyleSheet(label_style)
+ self.super_area_elev_combo = QComboBox()
+ self.super_area_elev_combo.addItems(["Automatic", "Custom"])
+ self.super_area_elev_combo.setFixedWidth(field_width)
+ apply_field_style(self.super_area_elev_combo)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.super_area_elev_combo, row, 1, Qt.AlignLeft)
+ row += 1
+ self.super_area_elev_value = QLineEdit()
+ self.super_area_elev_value.setPlaceholderText("Custom Value")
+ self.super_area_elev_value.setFixedWidth(field_width)
+ self.super_area_elev_value.setEnabled(False)
+ apply_field_style(self.super_area_elev_value)
+ wind_grid.addWidget(self.super_area_elev_value, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Superstructure Area in Plain
+ lbl = QLabel("Superstructure Area in Plain (m2):")
+ lbl.setStyleSheet(label_style)
+ self.super_area_plain_combo = QComboBox()
+ self.super_area_plain_combo.addItems(["Automatic", "Custom"])
+ self.super_area_plain_combo.setFixedWidth(field_width)
+ apply_field_style(self.super_area_plain_combo)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.super_area_plain_combo, row, 1, Qt.AlignLeft)
+ row += 1
+ self.super_area_plain_value = QLineEdit()
+ self.super_area_plain_value.setPlaceholderText("Custom Value")
+ self.super_area_plain_value.setFixedWidth(field_width)
+ self.super_area_plain_value.setEnabled(False)
+ apply_field_style(self.super_area_plain_value)
+ wind_grid.addWidget(self.super_area_plain_value, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Exposed Frontal Area of Live Load
+ lbl = QLabel("Exposed Frontal Area of Live Load (m2):")
+ lbl.setStyleSheet(label_style)
+ self.exposed_frontal_area_combo = QComboBox()
+ self.exposed_frontal_area_combo.addItems(["Automatic", "Custom"])
+ self.exposed_frontal_area_combo.setFixedWidth(field_width)
+ apply_field_style(self.exposed_frontal_area_combo)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.exposed_frontal_area_combo, row, 1, Qt.AlignLeft)
+ row += 1
+ self.exposed_frontal_area_value = QLineEdit()
+ self.exposed_frontal_area_value.setPlaceholderText("Custom Value")
+ self.exposed_frontal_area_value.setFixedWidth(field_width)
+ self.exposed_frontal_area_value.setEnabled(False)
+ apply_field_style(self.exposed_frontal_area_value)
+ wind_grid.addWidget(self.exposed_frontal_area_value, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Wind Load Eccentricity from Top of Deck
+ lbl = QLabel("Wind Load Eccentricity from Top of Deck\n(m): Negative for below deck")
+ lbl.setStyleSheet(label_style)
+ self.wind_ecc_deck_combo = QComboBox()
+ self.wind_ecc_deck_combo.addItems(["Automatic", "Custom"])
+ self.wind_ecc_deck_combo.setFixedWidth(field_width)
+ apply_field_style(self.wind_ecc_deck_combo)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.wind_ecc_deck_combo, row, 1, Qt.AlignLeft)
+ row += 1
+ self.wind_ecc_deck_value = QLineEdit()
+ self.wind_ecc_deck_value.setPlaceholderText("Value")
+ self.wind_ecc_deck_value.setFixedWidth(field_width)
+ self.wind_ecc_deck_value.setEnabled(False)
+ apply_field_style(self.wind_ecc_deck_value)
+ wind_grid.addWidget(self.wind_ecc_deck_value, row, 1, Qt.AlignLeft)
+ row += 1
+
+ # Wind on Live Load Eccentricity from Top of Deck
+ lbl = QLabel("Wind on Live Load Eccentricity from Top\nof Deck (m):")
+ lbl.setStyleSheet(label_style)
+ self.wind_ll_ecc_combo = QComboBox()
+ self.wind_ll_ecc_combo.addItems(["Automatic", "Custom"])
+ self.wind_ll_ecc_combo.setFixedWidth(field_width)
+ apply_field_style(self.wind_ll_ecc_combo)
+ wind_grid.addWidget(lbl, row, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ wind_grid.addWidget(self.wind_ll_ecc_combo, row, 1, Qt.AlignLeft)
+ row += 1
+ self.wind_ll_ecc_value = QLineEdit()
+ self.wind_ll_ecc_value.setPlaceholderText("Value")
+ self.wind_ll_ecc_value.setFixedWidth(field_width)
+ self.wind_ll_ecc_value.setEnabled(False)
+ apply_field_style(self.wind_ll_ecc_value)
+ wind_grid.addWidget(self.wind_ll_ecc_value, row, 1, Qt.AlignLeft)
+
+ wind_inputs_layout.addLayout(wind_grid)
+ left_layout.addWidget(wind_inputs_box)
+
+ # ===== Computed Values Box =====
+ computed_box = QFrame()
+ computed_box.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 8px; background-color: #ffffff; }")
+ computed_layout = QGridLayout(computed_box)
+ computed_layout.setContentsMargins(12, 12, 12, 12)
+ computed_layout.setHorizontalSpacing(12)
+ computed_layout.setVerticalSpacing(8)
+ computed_layout.setColumnMinimumWidth(0, 220)
+
+ computed_fields = [
+ ("Hourly Mean Wind Speed (m/s):", "hourly_mean_wind"),
+ ("Hourly Wind Pressure in N/m2:", "hourly_wind_pressure"),
+ ("Transverse Wind Force in N:", "transverse_wind_force"),
+ ("Longitudinal Wind Force in N:", "longitudinal_wind_force"),
+ ("Vertical Wind Force in N:", "vertical_wind_force"),
+ ("Transverse Wind Force on Live\nLoad in N:", "transverse_wind_ll"),
+ ("Longitudinal Wind Force on Live\nLoad in N:", "longitudinal_wind_ll"),
+ ]
+
+ self.wind_computed_fields = {}
+ for idx, (label_text, field_name) in enumerate(computed_fields):
+ lbl = QLabel(label_text)
+ lbl.setStyleSheet(label_style)
+ field = QLineEdit()
+ field.setFixedWidth(field_width)
+ field.setReadOnly(True)
+ apply_field_style(field)
+ computed_layout.addWidget(lbl, idx, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ computed_layout.addWidget(field, idx, 1, Qt.AlignLeft)
+ self.wind_computed_fields[field_name] = field
+
+ left_layout.addWidget(computed_box)
+ left_layout.addStretch()
+
+ scroll.setWidget(scroll_content)
+ left_card_layout.addWidget(scroll)
+
+ # Right description card
+ right_card = self._create_card()
+ right_card.setStyleSheet("QFrame { border: 1px solid #9c9c9c; border-radius: 10px; background-color: #d4d4d4; }")
+ right_card.setMinimumWidth(150)
+ right_card.setMaximumWidth(200)
+ right_layout = QVBoxLayout(right_card)
+ right_layout.setContentsMargins(16, 16, 16, 16)
+ right_layout.setSpacing(10)
+
+ desc_title = QLabel("Description\nBox")
+ desc_title.setAlignment(Qt.AlignCenter)
+ desc_title.setStyleSheet("font-size: 12px; font-weight: 700; color: #2b2b2b; background: transparent; border: none;")
+ right_layout.addWidget(desc_title)
+ right_layout.addStretch()
+
+ content_row.addWidget(left_card, 3)
+ content_row.addWidget(right_card, 1)
+
+ page_layout.addLayout(content_row)
+
+ # Connect signals for enabling/disabling custom inputs
+ self.gust_factor_combo.currentTextChanged.connect(lambda t: self.gust_factor_value.setEnabled(t == "Custom"))
+ self.drag_coeff_combo.currentTextChanged.connect(lambda t: self.drag_coeff_value.setEnabled(t == "Custom"))
+ self.drag_coeff_ll_combo.currentTextChanged.connect(lambda t: self.drag_coeff_ll_value.setEnabled(t == "Custom"))
+ self.lift_coeff_combo.currentTextChanged.connect(lambda t: self.lift_coeff_value.setEnabled(t == "Custom"))
+ self.super_area_elev_combo.currentTextChanged.connect(lambda t: self.super_area_elev_value.setEnabled(t == "Custom"))
+ self.super_area_plain_combo.currentTextChanged.connect(lambda t: self.super_area_plain_value.setEnabled(t == "Custom"))
+ self.exposed_frontal_area_combo.currentTextChanged.connect(lambda t: self.exposed_frontal_area_value.setEnabled(t == "Custom"))
+ self.wind_ecc_deck_combo.currentTextChanged.connect(lambda t: self.wind_ecc_deck_value.setEnabled(t == "Custom"))
+ self.wind_ll_ecc_combo.currentTextChanged.connect(lambda t: self.wind_ll_ecc_value.setEnabled(t == "Custom"))
+
+ return page
+
+ def _build_temperature_load_tab(self):
+ """Build the Temperature Load tab matching reference design"""
+ page = QWidget()
+ page.setStyleSheet("background-color: #f5f5f5;")
+ page_layout = QVBoxLayout(page)
+ page_layout.setContentsMargins(12, 12, 12, 12)
+ page_layout.setSpacing(12)
+
+ content_row = QHBoxLayout()
+ content_row.setContentsMargins(0, 0, 0, 0)
+ content_row.setSpacing(16)
+
+ # Left card with inputs
+ left_card = self._create_card()
+ left_card.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 10px; background-color: #ffffff; }")
+ left_layout = QVBoxLayout(left_card)
+ left_layout.setContentsMargins(16, 16, 16, 16)
+ left_layout.setSpacing(12)
+
+ label_style = "font-size: 11px; color: #3a3a3a; background: transparent; border: none;"
+ field_width = 120
+
+ # ===== Inputs for evaluation per IRC6 Box =====
+ irc6_box = QFrame()
+ irc6_box.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 8px; background-color: #ffffff; }")
+ irc6_layout = QVBoxLayout(irc6_box)
+ irc6_layout.setContentsMargins(12, 12, 12, 12)
+ irc6_layout.setSpacing(10)
+
+ irc6_title = QLabel("Inputs for evaluation per IRC6")
+ irc6_title.setStyleSheet("font-size: 12px; font-weight: 700; color: #2b2b2b; background: transparent; border: none;")
+ irc6_layout.addWidget(irc6_title)
+
+ irc6_grid = QGridLayout()
+ irc6_grid.setContentsMargins(0, 4, 0, 0)
+ irc6_grid.setHorizontalSpacing(12)
+ irc6_grid.setVerticalSpacing(10)
+ irc6_grid.setColumnMinimumWidth(0, 200)
+
+ # Highest Maximum Air Temperature
+ lbl = QLabel("Highest Maximum Air Temperature:")
+ lbl.setStyleSheet(label_style)
+ self.highest_max_temp_input = QLineEdit()
+ self.highest_max_temp_input.setFixedWidth(field_width)
+ apply_field_style(self.highest_max_temp_input)
+ irc6_grid.addWidget(lbl, 0, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ irc6_grid.addWidget(self.highest_max_temp_input, 0, 1, Qt.AlignLeft)
+
+ # Lowest Minimum Air Temperature
+ lbl = QLabel("Lowest Minimum Air Temperature:")
+ lbl.setStyleSheet(label_style)
+ self.lowest_min_temp_input = QLineEdit()
+ self.lowest_min_temp_input.setFixedWidth(field_width)
+ apply_field_style(self.lowest_min_temp_input)
+ irc6_grid.addWidget(lbl, 1, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ irc6_grid.addWidget(self.lowest_min_temp_input, 1, 1, Qt.AlignLeft)
+
+ irc6_layout.addLayout(irc6_grid)
+ left_layout.addWidget(irc6_box)
+
+ # ===== Range of Effective Bridge Temperature Box =====
+ range_box = QFrame()
+ range_box.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 8px; background-color: #ffffff; }")
+ range_layout = QVBoxLayout(range_box)
+ range_layout.setContentsMargins(12, 12, 12, 12)
+ range_layout.setSpacing(10)
+
+ range_title = QLabel("Range of Effective Bridge Temperature:")
+ range_title.setStyleSheet("font-size: 12px; font-weight: 700; color: #2b2b2b; background: transparent; border: none;")
+ range_layout.addWidget(range_title)
+
+ range_grid = QGridLayout()
+ range_grid.setContentsMargins(0, 4, 0, 0)
+ range_grid.setHorizontalSpacing(12)
+ range_grid.setVerticalSpacing(10)
+ range_grid.setColumnMinimumWidth(0, 200)
+
+ # Minimum
+ lbl = QLabel("Minimum:")
+ lbl.setStyleSheet(label_style)
+ self.bridge_temp_min_input = QLineEdit()
+ self.bridge_temp_min_input.setFixedWidth(field_width)
+ apply_field_style(self.bridge_temp_min_input)
+ range_grid.addWidget(lbl, 0, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ range_grid.addWidget(self.bridge_temp_min_input, 0, 1, Qt.AlignLeft)
+
+ # Maximum
+ lbl = QLabel("Maximum:")
+ lbl.setStyleSheet(label_style)
+ self.bridge_temp_max_input = QLineEdit()
+ self.bridge_temp_max_input.setFixedWidth(field_width)
+ apply_field_style(self.bridge_temp_max_input)
+ range_grid.addWidget(lbl, 1, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ range_grid.addWidget(self.bridge_temp_max_input, 1, 1, Qt.AlignLeft)
+
+ range_layout.addLayout(range_grid)
+ left_layout.addWidget(range_box)
+
+ # ===== Coefficient of Thermal Expansion Box =====
+ coeff_box = QFrame()
+ coeff_box.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 8px; background-color: #ffffff; }")
+ coeff_layout = QGridLayout(coeff_box)
+ coeff_layout.setContentsMargins(12, 12, 12, 12)
+ coeff_layout.setHorizontalSpacing(12)
+ coeff_layout.setVerticalSpacing(10)
+ coeff_layout.setColumnMinimumWidth(0, 200)
+
+ lbl = QLabel("Coefficient of Thermal Expansion for Steel:")
+ lbl.setStyleSheet(label_style)
+ self.thermal_coeff_combo = QComboBox()
+ self.thermal_coeff_combo.addItems(["12 × 10⁻⁶ /°C", "11.7 × 10⁻⁶ /°C", "Custom"])
+ self.thermal_coeff_combo.setFixedWidth(field_width)
+ apply_field_style(self.thermal_coeff_combo)
+ coeff_layout.addWidget(lbl, 0, 0, Qt.AlignLeft | Qt.AlignVCenter)
+ coeff_layout.addWidget(self.thermal_coeff_combo, 0, 1, Qt.AlignLeft)
+
+ left_layout.addWidget(coeff_box)
+ left_layout.addStretch()
+
+ # Right description card
+ right_card = self._create_card()
+ right_card.setStyleSheet("QFrame { border: 1px solid #9c9c9c; border-radius: 10px; background-color: #d4d4d4; }")
+ right_card.setMinimumWidth(200)
+ right_card.setMinimumHeight(400)
+ right_layout = QVBoxLayout(right_card)
+ right_layout.setContentsMargins(16, 16, 16, 16)
+ right_layout.setSpacing(10)
+
+ desc_title = QLabel("Description Box")
+ desc_title.setAlignment(Qt.AlignCenter)
+ desc_title.setStyleSheet("font-size: 12px; font-weight: 700; color: #2b2b2b; background: transparent; border: none;")
+ right_layout.addWidget(desc_title)
+ right_layout.addStretch()
+
+ content_row.addWidget(left_card, 3)
+ content_row.addWidget(right_card, 2)
+
+ page_layout.addLayout(content_row)
+
+ return page
+
+ def _build_load_combination_tab(self):
+ """Build the Load Combination tab matching reference design"""
+ page = QWidget()
+ page.setStyleSheet("background-color: #f5f5f5;")
+ page_layout = QVBoxLayout(page)
+ page_layout.setContentsMargins(12, 12, 12, 12)
+ page_layout.setSpacing(12)
+
+ content_row = QHBoxLayout()
+ content_row.setContentsMargins(0, 0, 0, 0)
+ content_row.setSpacing(16)
+
+ label_style = "font-size: 11px; color: #3a3a3a; background: transparent; border: none;"
+
+ # Left card - combination list
+ left_card = self._create_card()
+ left_card.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 10px; background-color: #ffffff; }")
+ left_layout = QVBoxLayout(left_card)
+ left_layout.setContentsMargins(16, 16, 16, 16)
+ left_layout.setSpacing(12)
+
+ # Auto include checkbox row
+ auto_row = QHBoxLayout()
+ auto_row.setContentsMargins(0, 0, 0, 0)
+ auto_row.setSpacing(8)
+ auto_label = QLabel("Auto include all IRC 6 Load Combinations")
+ auto_label.setStyleSheet("font-size: 11px; color: #3a3a3a; background: transparent; border: none;")
+ self.auto_include_checkbox = QCheckBox()
+ auto_row.addWidget(auto_label)
+ auto_row.addWidget(self.auto_include_checkbox)
+ auto_row.addStretch()
+ left_layout.addLayout(auto_row)
+
+ # Combination Name label
+ combo_name_label = QLabel("Combination Name")
+ combo_name_label.setStyleSheet("font-size: 12px; font-weight: 700; color: #2b2b2b; background: transparent; border: none;")
+ left_layout.addWidget(combo_name_label)
+
+ # Combination list area
+ self.combination_list_widget = QWidget()
+ self.combination_list_widget.setStyleSheet("background: #ffffff;")
+ self.combination_list_layout = QVBoxLayout(self.combination_list_widget)
+ self.combination_list_layout.setContentsMargins(0, 8, 0, 8)
+ self.combination_list_layout.setSpacing(8)
+
+ # Add sample combinations
+ sample_combos = ["DL + LL", "1.35 DL + 1.75 LL"]
+ for combo_text in sample_combos:
+ combo_label = QLabel(combo_text)
+ combo_label.setStyleSheet(label_style)
+ self.combination_list_layout.addWidget(combo_label)
+
+ self.combination_list_layout.addStretch()
+ left_layout.addWidget(self.combination_list_widget, 1)
+
+ # Middle card - combination editor
+ middle_card = self._create_card()
+ middle_card.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 10px; background-color: #ffffff; }")
+ middle_layout = QVBoxLayout(middle_card)
+ middle_layout.setContentsMargins(16, 16, 16, 16)
+ middle_layout.setSpacing(12)
+
+ # Combination Name title
+ combo_title = QLabel("Combination Name")
+ combo_title.setStyleSheet("font-size: 12px; font-weight: 700; color: #2b2b2b; background: transparent; border: none;")
+ middle_layout.addWidget(combo_title)
+
+ # Editor area with table and buttons
+ editor_row = QHBoxLayout()
+ editor_row.setContentsMargins(0, 0, 0, 0)
+ editor_row.setSpacing(12)
+
+ # Table for Load Name and Scale Factor
+ table_box = QFrame()
+ table_box.setStyleSheet("QFrame { border: 1px solid #b2b2b2; border-radius: 4px; background-color: #ffffff; }")
+ table_layout = QVBoxLayout(table_box)
+ table_layout.setContentsMargins(0, 0, 0, 0)
+ table_layout.setSpacing(0)
+
+ # Header row
+ header_widget = QWidget()
+ header_widget.setStyleSheet("background: #ffffff; border-bottom: 1px solid #b2b2b2;")
+ header_layout = QHBoxLayout(header_widget)
+ header_layout.setContentsMargins(8, 8, 8, 8)
+ header_layout.setSpacing(8)
+
+ load_name_header = QLabel("Load Name")
+ load_name_header.setStyleSheet("font-size: 11px; font-weight: 600; color: #3a3a3a; background: transparent; border: none;")
+ load_name_header.setMinimumWidth(80)
+ scale_factor_header = QLabel("Scale Factor")
+ scale_factor_header.setStyleSheet("font-size: 11px; font-weight: 600; color: #3a3a3a; background: transparent; border: none;")
+ scale_factor_header.setMinimumWidth(80)
+
+ header_layout.addWidget(load_name_header)
+ header_layout.addWidget(scale_factor_header)
+ table_layout.addWidget(header_widget)
+
+ # Input row
+ input_widget = QWidget()
+ input_widget.setStyleSheet("background: #ffffff;")
+ input_layout = QHBoxLayout(input_widget)
+ input_layout.setContentsMargins(8, 8, 8, 8)
+ input_layout.setSpacing(8)
+
+ self.load_name_combo = QComboBox()
+ self.load_name_combo.addItems(["DL", "LL", "WL", "EL", "TL"])
+ self.load_name_combo.setMinimumWidth(80)
+ apply_field_style(self.load_name_combo)
+
+ self.scale_factor_input = QLineEdit()
+ self.scale_factor_input.setMinimumWidth(80)
+ apply_field_style(self.scale_factor_input)
+
+ input_layout.addWidget(self.load_name_combo)
+ input_layout.addWidget(self.scale_factor_input)
+ table_layout.addWidget(input_widget)
+
+ # Empty space for more rows
+ table_layout.addStretch()
+
+ editor_row.addWidget(table_box, 1)
+
+ # Add/Delete buttons column
+ button_col = QVBoxLayout()
+ button_col.setContentsMargins(0, 0, 0, 0)
+ button_col.setSpacing(8)
+
+ self.add_load_btn = QPushButton("Add")
+ self.add_load_btn.setFixedWidth(60)
+ self.add_load_btn.setStyleSheet(
+ "QPushButton { background: #ffffff; border: 1px solid #b2b2b2; border-radius: 4px; padding: 6px 12px; font-size: 11px; color: #3a3a3a; }"
+ "QPushButton:hover { background: #f0f0f0; }"
+ "QPushButton:pressed { background: #e0e0e0; }"
+ )
+
+ self.delete_load_btn = QPushButton("Delete")
+ self.delete_load_btn.setFixedWidth(60)
+ self.delete_load_btn.setStyleSheet(
+ "QPushButton { background: #ffffff; border: 1px solid #b2b2b2; border-radius: 4px; padding: 6px 12px; font-size: 11px; color: #3a3a3a; }"
+ "QPushButton:hover { background: #f0f0f0; }"
+ "QPushButton:pressed { background: #e0e0e0; }"
+ )
+
+ button_col.addWidget(self.add_load_btn)
+ button_col.addWidget(self.delete_load_btn)
+ button_col.addStretch()
+
+ editor_row.addLayout(button_col)
+ middle_layout.addLayout(editor_row, 1)
+
+ content_row.addWidget(left_card, 2)
+ content_row.addWidget(middle_card, 3)
+
+ page_layout.addLayout(content_row)
+
+ return page
+
+ def _create_placeholder_page(self, title):
+ page = QWidget()
+ page.setStyleSheet("background-color: #f5f5f5;")
+ layout = QVBoxLayout(page)
+ layout.setAlignment(Qt.AlignCenter)
+ layout.setContentsMargins(20, 20, 20, 20)
+
+ label = QLabel(f"{title} inputs will be added soon.")
+ label.setAlignment(Qt.AlignCenter)
+ label.setStyleSheet("font-size: 12px; color: #6a6a6a;")
+ layout.addWidget(label)
+ return page
diff --git a/src/osdagbridge/desktop/ui/dialogs/project_location.py b/src/osdagbridge/desktop/ui/dialogs/project_location.py
new file mode 100644
index 00000000..c263e21f
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/dialogs/project_location.py
@@ -0,0 +1,387 @@
+from PySide6.QtWidgets import (
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QWidget,
+ QCheckBox, QFrame, QPushButton, QComboBox, QSizePolicy, QSizeGrip
+)
+from PySide6.QtCore import Qt
+from osdagbridge.desktop.ui.utils.custom_titlebar import CustomTitleBar
+
+class NoScrollComboBox(QComboBox):
+ def wheelEvent(self, event):
+ event.ignore() # Prevent changing selection on scroll
+
+def apply_field_style(widget):
+ widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ widget.setMinimumHeight(28)
+
+ if isinstance(widget, QComboBox):
+ style = """
+ QComboBox{
+ padding: 1px 7px;
+ border: 1px solid black;
+ border-radius: 5px;
+ background-color: white;
+ color: black;
+ }
+ QComboBox::drop-down{
+ subcontrol-origin: padding;
+ subcontrol-position: top right;
+ border-left: 0px;
+ }
+ QComboBox::down-arrow{
+ image: url(:/vectors/arrow_down_light.svg);
+ width: 20px;
+ height: 20px;
+ margin-right: 8px;
+ }
+ QComboBox::down-arrow:on {
+ image: url(:/vectors/arrow_up_light.svg);
+ width: 20px;
+ height: 20px;
+ margin-right: 8px;
+ }
+ QComboBox QAbstractItemView{
+ background-color: white;
+ border: 1px solid black;
+ outline: none;
+ }
+ QComboBox QAbstractItemView::item{
+ color: black;
+ background-color: white;
+ border: none;
+ border: 1px solid white;
+ border-radius: 0;
+ padding: 2px;
+ }
+ QComboBox QAbstractItemView::item:hover{
+ border: 1px solid #90AF13;
+ background-color: #90AF13;
+ color: black;
+ }
+ QComboBox QAbstractItemView::item:selected{
+ background-color: #90AF13;
+ color: black;
+ border: 1px solid #90AF13;
+ }
+ QComboBox QAbstractItemView::item:selected:hover{
+ background-color: #90AF13;
+ color: black;
+ border: 1px solid #94b816;
+ }
+ """
+ widget.setStyleSheet(style)
+ elif isinstance(widget, QLineEdit):
+ widget.setStyleSheet("""
+ QLineEdit {
+ padding: 1px 7px;
+ border: 1px solid #070707;
+ border-radius: 6px;
+ background-color: white;
+ color: #000000;
+ font-weight: normal;
+ }
+ """)
+
+
+class ProjectLocationDialog(QDialog):
+ """Dialog for selecting project location with multiple input methods"""
+
+ STATE_DISTRICTS = {
+ "Select State": ["Select District"],
+ "Delhi": ["Central Delhi", "East Delhi", "New Delhi", "North Delhi", "North East Delhi",
+ "North West Delhi", "South Delhi", "South East Delhi", "South West Delhi", "West Delhi"],
+ "Maharashtra": ["Mumbai", "Pune", "Nagpur", "Thane", "Nashik", "Aurangabad", "Solapur",
+ "Amravati", "Kolhapur", "Raigad", "Satara", "Sangli"],
+ "Karnataka": ["Bangalore", "Mysore", "Hubli", "Belgaum", "Mangalore", "Gulbarga",
+ "Bellary", "Bijapur", "Shimoga", "Tumkur", "Davangere"],
+ "Tamil Nadu": ["Chennai", "Coimbatore", "Madurai", "Tiruchirappalli", "Salem", "Tirunelveli",
+ "Tiruppur", "Erode", "Vellore", "Thoothukudi", "Dindigul"],
+ "West Bengal": ["Kolkata", "Howrah", "Darjeeling", "Siliguri", "Asansol", "Durgapur",
+ "Bardhaman", "Malda", "Jalpaiguri", "Murshidabad", "Nadia"]
+ }
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setMinimumWidth(850)
+ self.setMinimumHeight(650)
+ self.setObjectName("project_location_dialog")
+ self.setStyleSheet("""
+ QDialog#project_location_dialog {
+ background-color: #FFFFFF;
+ border: 1px solid #90AF13;
+ }
+ """)
+
+ self._setup_ui()
+ self._connect_signals()
+
+ def setupWrapper(self):
+ self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowSystemMenuHint)
+
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(1, 1, 1, 1)
+ main_layout.setSpacing(0)
+
+ self.title_bar = CustomTitleBar()
+ self.title_bar.setTitle("Project Location")
+ main_layout.addWidget(self.title_bar)
+
+ self.content_widget = QWidget(self)
+ main_layout.addWidget(self.content_widget, 1)
+
+ size_grip = QSizeGrip(self)
+ size_grip.setFixedSize(16, 16)
+
+ overlay = QHBoxLayout()
+ overlay.setContentsMargins(0, 0, 4, 4)
+ overlay.addStretch(1)
+ overlay.addWidget(size_grip, 0, Qt.AlignBottom | Qt.AlignRight)
+ main_layout.addLayout(overlay)
+
+ def _setup_ui(self):
+ """Setup the user interface"""
+ self.setupWrapper()
+ main_layout = QVBoxLayout(self.content_widget)
+ main_layout.setContentsMargins(20, 20, 20, 20)
+ main_layout.setSpacing(15)
+
+ # Add sections
+ self._add_coordinates_section(main_layout)
+ self._add_separator(main_layout)
+ self._add_location_name_section(main_layout)
+ self._add_separator(main_layout)
+ self._add_map_section(main_layout)
+ self._add_separator(main_layout)
+ self._add_irc_values_section(main_layout)
+ self._add_separator(main_layout)
+ self._add_custom_params_section(main_layout)
+
+ main_layout.addStretch()
+
+ self._add_buttons(main_layout)
+
+ def _add_coordinates_section(self, layout):
+ """Add the coordinates input section"""
+ coords_row = QHBoxLayout()
+ coords_row.setSpacing(15)
+
+ self.coords_checkbox = QCheckBox("Enter Coordinates")
+ coords_row.addWidget(self.coords_checkbox)
+ coords_row.addStretch()
+
+ lat_label = QLabel("Latitude (°)")
+ lat_label.setStyleSheet("font-size: 11px;")
+ coords_row.addWidget(lat_label)
+
+ self.latitude_input = QLineEdit()
+ self.latitude_input.setMaximumWidth(120)
+ self.latitude_input.setEnabled(False)
+ apply_field_style(self.latitude_input)
+ coords_row.addWidget(self.latitude_input)
+
+ lng_label = QLabel("Longitude (°)")
+ lng_label.setStyleSheet("font-size: 11px;")
+ coords_row.addWidget(lng_label)
+
+ self.longitude_input = QLineEdit()
+ self.longitude_input.setMaximumWidth(120)
+ self.longitude_input.setEnabled(False)
+ apply_field_style(self.longitude_input)
+ coords_row.addWidget(self.longitude_input)
+
+ layout.addLayout(coords_row)
+
+ def _add_location_name_section(self, layout):
+ """Add the location name input section"""
+ location_row = QHBoxLayout()
+ location_row.setSpacing(15)
+
+ self.location_checkbox = QCheckBox("Enter Location Name")
+ location_row.addWidget(self.location_checkbox)
+ location_row.addStretch()
+
+ state_label = QLabel("State")
+ state_label.setStyleSheet("font-size: 11px;")
+ location_row.addWidget(state_label)
+
+ self.state_combo = NoScrollComboBox()
+ self.state_combo.setMaximumWidth(150)
+ self.state_combo.setEnabled(False)
+ self.state_combo.addItems(list(self.STATE_DISTRICTS.keys()))
+ apply_field_style(self.state_combo)
+ location_row.addWidget(self.state_combo)
+
+ district_label = QLabel("District")
+ district_label.setStyleSheet("font-size: 11px;")
+ location_row.addWidget(district_label)
+
+ self.district_combo = NoScrollComboBox()
+ self.district_combo.setMaximumWidth(150)
+ self.district_combo.setEnabled(False)
+ self.district_combo.addItems(["Select District"])
+ apply_field_style(self.district_combo)
+ location_row.addWidget(self.district_combo)
+
+ layout.addLayout(location_row)
+
+ def _add_map_section(self, layout):
+ """Add the map selection section"""
+ map_section = QVBoxLayout()
+ map_section.setSpacing(8)
+
+ self.map_checkbox = QCheckBox("Select on Map")
+ map_section.addWidget(self.map_checkbox)
+
+ # Map placeholder
+ self.map_placeholder = QLabel()
+ self.map_placeholder.setAlignment(Qt.AlignCenter)
+ self.map_placeholder.setMinimumHeight(200)
+ self.map_placeholder.setText("Map Placeholder\n(Will be added later)")
+ self.map_placeholder.setEnabled(False)
+ map_section.addWidget(self.map_placeholder)
+
+ layout.addLayout(map_section)
+
+ def _add_irc_values_section(self, layout):
+ """Add the IRC 6 (2017) values section"""
+ results_section = QVBoxLayout()
+ results_section.setSpacing(8)
+
+ results_title = QLabel("IRC 6 (2017) Values")
+ results_section.addWidget(results_title)
+
+ self.wind_speed_label = QLabel("Basic Wind Speed (m/sec)")
+ results_section.addWidget(self.wind_speed_label)
+
+ self.seismic_zone_label = QLabel("Seismic Zone and Zone Factor")
+ results_section.addWidget(self.seismic_zone_label)
+
+ self.temp_label = QLabel("Shade Air Temperature (°C)")
+ results_section.addWidget(self.temp_label)
+
+ layout.addLayout(results_section)
+
+ def _add_custom_params_section(self, layout):
+ """Add the custom loading parameters checkbox"""
+ self.custom_params_checkbox = QCheckBox("Tabulate Custom Loading Parameters")
+ layout.addWidget(self.custom_params_checkbox)
+
+ def _add_separator(self, layout):
+ """Add a horizontal separator line"""
+ line = QFrame()
+ line.setFrameShape(QFrame.HLine)
+ line.setFrameShadow(QFrame.Sunken)
+ line.setStyleSheet("background-color: #d0d0d0;")
+ layout.addWidget(line)
+
+ def _add_buttons(self, layout):
+ """Add OK and Cancel buttons"""
+ btn_layout = QHBoxLayout()
+ btn_layout.addStretch()
+
+ ok_btn = QPushButton("OK")
+ ok_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ ok_btn.setMinimumWidth(100)
+ ok_btn.clicked.connect(self.accept)
+ btn_layout.addWidget(ok_btn)
+
+ cancel_btn = QPushButton("Cancel")
+ cancel_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ cancel_btn.setMinimumWidth(100)
+ cancel_btn.clicked.connect(self.reject)
+ btn_layout.addWidget(cancel_btn)
+
+ layout.addLayout(btn_layout)
+
+ def _connect_signals(self):
+ """Connect all signal handlers"""
+ # Enable/disable coordinates inputs
+ self.coords_checkbox.stateChanged.connect(
+ lambda state: self._toggle_coordinates_inputs(state == 2)
+ )
+
+ # Enable/disable location inputs
+ self.location_checkbox.stateChanged.connect(
+ lambda state: self._toggle_location_inputs(state == 2)
+ )
+
+ # Handle map checkbox
+ self.map_checkbox.stateChanged.connect(self._on_map_checkbox_changed)
+
+ # Update districts when state changes
+ self.state_combo.currentTextChanged.connect(self._on_state_changed)
+
+ def _toggle_coordinates_inputs(self, enabled):
+ """Enable or disable coordinate input fields"""
+ self.latitude_input.setEnabled(enabled)
+ self.longitude_input.setEnabled(enabled)
+
+ def _toggle_location_inputs(self, enabled):
+ """Enable or disable location input fields"""
+ self.state_combo.setEnabled(enabled)
+ self.district_combo.setEnabled(enabled)
+
+ def _on_state_changed(self, state_name):
+ """Update districts based on selected state"""
+ districts = self.STATE_DISTRICTS.get(state_name, ["Select District"])
+ self.district_combo.clear()
+ self.district_combo.addItems(districts)
+
+ def _on_map_checkbox_changed(self, state):
+ """Handle map checkbox state changes"""
+ enabled = (state == 2)
+ self.map_placeholder.setEnabled(enabled)
+
+ if enabled:
+ self.map_placeholder.setStyleSheet("""
+ QLabel {
+ border: 2px solid #90AF13;
+ background-color: white;
+ padding: 20px;
+ color: #666666;
+ }
+ """)
+ self.map_placeholder.setText(
+ "Map Placeholder\n(Click to select location)\n(Will be implemented later)"
+ )
+ else:
+ self.map_placeholder.setStyleSheet("""
+ QLabel {
+ border: 1px solid #e0e0e0;
+ background-color: #f5f5f5;
+ padding: 20px;
+ color: #999999;
+ }
+ """)
+ self.map_placeholder.setText("Map Placeholder\n(Will be added later)")
+
+ def get_selected_location(self):
+ """
+ Get the selected location data
+
+ Returns:
+ dict: Dictionary containing location information based on selection method
+ """
+ result = {
+ 'method': None,
+ 'data': {}
+ }
+
+ if self.coords_checkbox.isChecked():
+ result['method'] = 'coordinates'
+ result['data'] = {
+ 'latitude': self.latitude_input.text(),
+ 'longitude': self.longitude_input.text()
+ }
+ elif self.location_checkbox.isChecked():
+ result['method'] = 'location_name'
+ result['data'] = {
+ 'state': self.state_combo.currentText(),
+ 'district': self.district_combo.currentText()
+ }
+ elif self.map_checkbox.isChecked():
+ result['method'] = 'map'
+ result['data'] = {} # Will be populated when map is implemented
+
+ result['custom_params'] = self.custom_params_checkbox.isChecked()
+
+ return result
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/docks/input_dock.py b/src/osdagbridge/desktop/ui/docks/input_dock.py
new file mode 100644
index 00000000..cb5d9b2a
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/docks/input_dock.py
@@ -0,0 +1,1640 @@
+import sys
+import os
+import math
+from PySide6.QtWidgets import (
+ QApplication, QWidget, QHBoxLayout, QVBoxLayout, QPushButton,
+ QComboBox, QScrollArea, QLabel, QFormLayout, QLineEdit, QGroupBox, QSizePolicy, QMessageBox, QInputDialog, QDialog, QCheckBox, QFrame,
+ QDialogButtonBox, QStackedWidget
+)
+from PySide6.QtCore import Qt, QRegularExpression, QSize, QTimer, QPoint, QEvent
+from PySide6.QtGui import QPixmap, QDoubleValidator, QRegularExpressionValidator, QIcon
+from PySide6.QtSvgWidgets import *
+from osdagbridge.core.utils.common import *
+from osdagbridge.desktop.ui.dialogs.additional_inputs import AdditionalInputs
+from osdagbridge.desktop.ui.utils.custom_buttons import DockCustomButton
+from osdagbridge.desktop.ui.dialogs.project_location import ProjectLocationDialog
+
+
+STEEL_MEMBER_FIELDS = [
+ "Ultimate Tensile Strength, Fu (MPa)",
+ "Yield Strength, Fy (MPa)",
+ "Modulus of Elasticity, E (GPa)",
+ "Modulus of Rigidity, G (GPa)",
+ "Poisson's Ratio, ν",
+ "Thermal Expansion Coefficient, (×10⁻⁶/°C)",
+]
+
+DECK_MEMBER_FIELDS = [
+ "Characteristic Compressive (Cube) Strength of Concrete, (fck)cu (MPa)",
+ "Mean Tensile Strength of Concrete, fctm (MPa)",
+ "Secant Modulus of Elasticity of Concrete, Ecm (GPa)",
+ "Ecm Multiplication Factor",
+]
+
+STEEL_MODULUS_E_GPA = 200.0
+STEEL_MODULUS_G_GPA = 77.0
+STEEL_POISSON_RATIO = 0.30
+STEEL_THERMAL_COEFF = 11.7
+
+STEEL_GRADE_BASE_VALUES = {
+ 250: {"Fy": 250, "Fu": 410},
+ 275: {"Fy": 275, "Fu": 430},
+ 300: {"Fy": 300, "Fu": 440},
+ 350: {"Fy": 350, "Fu": 490},
+ 410: {"Fy": 410, "Fu": 540},
+ 450: {"Fy": 450, "Fu": 570},
+ 550: {"Fy": 550, "Fu": 650},
+ 600: {"Fy": 600, "Fu": 700},
+ 650: {"Fy": 650, "Fu": 750},
+}
+
+ECM_FACTOR_OPTIONS = [
+ ("Quartzite/granite aggregates = 1", 1.0),
+ ("Limestone aggregates = 0.9", 0.9),
+ ("Sandstone aggregates = 0.7", 0.7),
+ ("Basalt aggregates = 1.2", 1.2),
+ ("Custom", None),
+]
+ECM_FACTOR_LABELS = [text for text, _ in ECM_FACTOR_OPTIONS]
+DEFAULT_ECM_FACTOR_LABEL = ECM_FACTOR_OPTIONS[0][0]
+CUSTOM_ECM_FACTOR_LABEL = "Custom"
+
+
+class NoScrollComboBox(QComboBox):
+ def wheelEvent(self, event):
+ event.ignore() # Prevent changing selection on scroll
+
+def apply_field_style(widget):
+ widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ widget.setMinimumHeight(28)
+
+ if isinstance(widget, QComboBox):
+ style = """
+ QComboBox{
+ padding: 1px 7px;
+ border: 1px solid black;
+ border-radius: 5px;
+ background-color: white;
+ color: black;
+ }
+ QComboBox::drop-down{
+ subcontrol-origin: padding;
+ subcontrol-position: top right;
+ border-left: 0px;
+ }
+ QComboBox::down-arrow{
+ image: url(:/vectors/arrow_down_light.svg);
+ width: 20px;
+ height: 20px;
+ margin-right: 8px;
+ }
+ QComboBox::down-arrow:on {
+ image: url(:/vectors/arrow_up_light.svg);
+ width: 20px;
+ height: 20px;
+ margin-right: 8px;
+ }
+ QComboBox QAbstractItemView{
+ background-color: white;
+ border: 1px solid black;
+ outline: none;
+ }
+ QComboBox QAbstractItemView::item{
+ color: black;
+ background-color: white;
+ border: none;
+ border: 1px solid white;
+ border-radius: 0;
+ padding: 2px;
+ }
+ QComboBox QAbstractItemView::item:hover{
+ border: 1px solid #90AF13;
+ background-color: #90AF13;
+ color: black;
+ }
+ QComboBox QAbstractItemView::item:selected{
+ background-color: #90AF13;
+ color: black;
+ border: 1px solid #90AF13;
+ }
+ QComboBox QAbstractItemView::item:selected:hover{
+ background-color: #90AF13;
+ color: black;
+ border: 1px solid #94b816;
+ }
+ QComboBox:disabled{
+ background: #f1f1f1;
+ color: #666;
+ }
+ """
+ widget.setStyleSheet(style)
+ elif isinstance(widget, QLineEdit):
+ widget.setStyleSheet("""
+ QLineEdit {
+ padding: 1px 7px;
+ border: 1px solid #070707;
+ border-radius: 6px;
+ background-color: white;
+ color: #000000;
+ font-weight: normal;
+ }
+ QLineEdit:disabled{
+ background: #f1f1f1;
+ color: #666;
+ }
+ """)
+
+
+class MaterialPropertiesDialog(QDialog):
+ MEMBER_OPTIONS = ["Girder", "Cross Bracing", "End Diaphragm", "Deck"]
+ STEEL_MEMBERS = {"Girder", "Cross Bracing", "End Diaphragm"}
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Material Properties")
+ self.setMinimumWidth(580)
+ self.setStyleSheet("background-color: white;")
+
+ self.parent_dock = parent
+ self._loading = False
+ self.current_member = None
+ self.member_data = {}
+
+ self.member_combo = NoScrollComboBox()
+ self.member_combo.addItems(self.MEMBER_OPTIONS)
+ apply_field_style(self.member_combo)
+
+ self.material_combo = NoScrollComboBox()
+ apply_field_style(self.material_combo)
+
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(20, 16, 20, 16)
+
+ # Create a container widget for all form fields
+ form_container = QWidget()
+ form_layout = QVBoxLayout(form_container)
+ form_layout.setContentsMargins(0, 0, 0, 0)
+ form_layout.setSpacing(10)
+
+ # Member row
+ member_row = QHBoxLayout()
+ member_row.setContentsMargins(0, 0, 0, 0)
+ member_row.setSpacing(18)
+ member_label = QLabel("Member*:")
+ member_label.setStyleSheet("font-size: 12px; color: #2d2d2d;")
+ member_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ member_label.setFixedWidth(280)
+ self.member_combo.setFixedWidth(242)
+ member_row.addWidget(member_label)
+ member_row.addWidget(self.member_combo)
+ member_row.addStretch()
+ form_layout.addLayout(member_row)
+
+ # Material row
+ material_row = QHBoxLayout()
+ material_row.setContentsMargins(0, 0, 0, 0)
+ material_row.setSpacing(18)
+ material_label = QLabel("Material*:")
+ material_label.setStyleSheet("font-size: 12px; color: #2d2d2d;")
+ material_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ material_label.setFixedWidth(280)
+ self.material_combo.setFixedWidth(242)
+ material_row.addWidget(material_label)
+ material_row.addWidget(self.material_combo)
+ material_row.addStretch()
+ form_layout.addLayout(material_row)
+
+ main_layout.addWidget(form_container)
+
+ self.stack = QStackedWidget()
+ self.stack.setContentsMargins(0, 0, 0, 0)
+ self.steel_page = self._build_steel_form()
+ self.deck_page = self._build_deck_form()
+ self.stack.addWidget(self.steel_page)
+ self.stack.addWidget(self.deck_page)
+ main_layout.addWidget(self.stack)
+
+ # Updated default row with proper alignment
+ default_row = QHBoxLayout()
+ default_row.setContentsMargins(0, 0, 0, 0)
+ default_row.setSpacing(18)
+ default_label = QLabel("Default")
+ default_label.setStyleSheet("font-size: 12px; color: #2d2d2d;")
+ default_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ default_label.setFixedWidth(280)
+ self.default_checkbox = QCheckBox()
+ # Create container for checkbox to align it to the left
+ checkbox_container = QWidget()
+ checkbox_layout = QHBoxLayout(checkbox_container)
+ checkbox_layout.setContentsMargins(0, 0, 0, 0)
+ checkbox_layout.setSpacing(0)
+ checkbox_layout.addWidget(self.default_checkbox)
+ checkbox_layout.addStretch()
+
+ default_row.addWidget(default_label)
+ default_row.addWidget(checkbox_container)
+ main_layout.addLayout(default_row)
+
+ self.member_combo.currentTextChanged.connect(self._on_member_changed)
+ self.material_combo.currentTextChanged.connect(self._on_material_changed)
+ self.default_checkbox.stateChanged.connect(self._on_default_toggled)
+
+ self._initialize_member_data()
+ self._on_member_changed(self.member_combo.currentText())
+
+ def closeEvent(self, event):
+ self._save_current_member_form()
+ super().closeEvent(event)
+
+ def _build_steel_form(self):
+ widget = QWidget()
+ layout = QVBoxLayout(widget)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(10)
+ self.steel_field_inputs = {}
+ for label_text in STEEL_MEMBER_FIELDS:
+ row = QHBoxLayout()
+ row.setContentsMargins(0, 0, 0, 0)
+ row.setSpacing(18)
+ label = QLabel(label_text)
+ label.setStyleSheet("font-size: 12px; color: #2d2d2d;")
+ label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ label.setFixedWidth(280)
+ line_edit = QLineEdit()
+ line_edit.setFixedWidth(242)
+ apply_field_style(line_edit)
+ # Add validator for 1 decimal place
+ line_edit.setValidator(QDoubleValidator(0.0, 99999.0, 1))
+ line_edit.textEdited.connect(self._handle_user_override)
+ self.steel_field_inputs[label_text] = line_edit
+ row.addWidget(label)
+ row.addWidget(line_edit)
+ row.addStretch()
+ layout.addLayout(row)
+ layout.addStretch()
+ return widget
+
+ def _build_deck_form(self):
+ widget = QWidget()
+ layout = QVBoxLayout(widget)
+ layout.setSpacing(10)
+ self.deck_field_inputs = {}
+ for label_text in DECK_MEMBER_FIELDS:
+ row = QHBoxLayout()
+ row.setSpacing(18)
+ label = QLabel(label_text)
+ label.setStyleSheet("font-size: 12px; color: #2d2d2d;")
+ label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ label.setFixedWidth(280)
+ if label_text == "Ecm Multiplication Factor":
+ self.deck_factor_combo = NoScrollComboBox()
+ self.deck_factor_combo.addItems(ECM_FACTOR_LABELS)
+ self.deck_factor_combo.setFixedWidth(242)
+ apply_field_style(self.deck_factor_combo)
+ self.deck_factor_combo.currentTextChanged.connect(self._on_factor_changed)
+
+ self.deck_factor_custom_input = QLineEdit()
+ apply_field_style(self.deck_factor_custom_input)
+ self.deck_factor_custom_input.setPlaceholderText("Custom factor")
+ self.deck_factor_custom_input.setFixedWidth(242)
+ self.deck_factor_custom_input.setVisible(False)
+ self.deck_factor_custom_input.setEnabled(False)
+ self.deck_factor_custom_input.setValidator(QDoubleValidator(0.1, 5.0, 1))
+ self.deck_factor_custom_input.textEdited.connect(self._handle_user_override)
+
+ row.addWidget(label)
+ row.addWidget(self.deck_factor_combo)
+ row.addStretch()
+
+ # Add custom input row (hidden by default)
+ custom_row = QHBoxLayout()
+ custom_row.setContentsMargins(0, 0, 0, 0)
+ custom_row.setSpacing(18)
+ custom_label = QLabel("") # Empty label for alignment
+ custom_label.setFixedWidth(280)
+ custom_row.addWidget(custom_label)
+ custom_row.addWidget(self.deck_factor_custom_input)
+ custom_row.addStretch()
+ layout.addLayout(custom_row)
+
+ self.deck_field_inputs[label_text] = self.deck_factor_combo
+ else:
+ line_edit = QLineEdit()
+ line_edit.setFixedWidth(242)
+ apply_field_style(line_edit)
+ # Add validator for 1 decimal place
+ line_edit.setValidator(QDoubleValidator(0.0, 99999.0, 1))
+ line_edit.textEdited.connect(self._handle_user_override)
+ row.addWidget(label)
+ row.addWidget(line_edit)
+ row.addStretch()
+ self.deck_field_inputs[label_text] = line_edit
+ layout.addLayout(row)
+ layout.addStretch()
+ return widget
+
+ def _initialize_member_data(self):
+ for member in self.MEMBER_OPTIONS:
+ material = self._get_parent_grade(member)
+ fields = self._default_fields_for_member(member, material)
+ self.member_data[member] = {
+ "material": material,
+ "fields": fields,
+ "is_default": True,
+ "factor_label": DEFAULT_ECM_FACTOR_LABEL if member == "Deck" else None,
+ "custom_factor": "1.0" if member == "Deck" else None,
+ }
+
+ def _default_fields_for_member(self, member, material=None, factor_label=None, custom_factor=None):
+ if member == "Deck":
+ grade = material or self._get_parent_grade(member) or (VALUES_DECK_CONCRETE_GRADE[0] if VALUES_DECK_CONCRETE_GRADE else "")
+ factor_label = factor_label or DEFAULT_ECM_FACTOR_LABEL
+ factor_value = self._factor_value_from_label(factor_label, custom_factor)
+ return self._deck_defaults(grade, factor_value)
+ grade = material or self._get_parent_grade(member) or (VALUES_MATERIAL[0] if VALUES_MATERIAL else "")
+ return self._steel_defaults(grade)
+
+ def _steel_defaults(self, grade):
+ grade_value = self._extract_numeric_grade(grade)
+ defaults = STEEL_GRADE_BASE_VALUES.get(grade_value, STEEL_GRADE_BASE_VALUES[250])
+ return {
+ "Ultimate Tensile Strength, Fu (MPa)": "{:.1f}".format(defaults["Fu"]),
+ "Yield Strength, Fy (MPa)": "{:.1f}".format(defaults["Fy"]),
+ "Modulus of Elasticity, E (GPa)": "{:.1f}".format(STEEL_MODULUS_E_GPA),
+ "Modulus of Rigidity, G (GPa)": "{:.1f}".format(STEEL_MODULUS_G_GPA),
+ "Poisson's Ratio, ν": "{:.1f}".format(STEEL_POISSON_RATIO),
+ "Thermal Expansion Coefficient, (×10⁻⁶/°C)": "{:.1f}".format(STEEL_THERMAL_COEFF),
+ }
+
+ def _deck_defaults(self, grade, factor_value):
+ strength = self._extract_numeric_grade(grade, default=25)
+ fck = float(strength)
+ fctm = round(0.7 * math.sqrt(fck), 1)
+ ecm = round(5.0 * math.sqrt(fck) * factor_value, 1)
+ return {
+ "Characteristic Compressive (Cube) Strength of Concrete, (fck)cu (MPa)": "{:.1f}".format(fck),
+ "Mean Tensile Strength of Concrete, fctm (MPa)": "{:.1f}".format(fctm),
+ "Secant Modulus of Elasticity of Concrete, Ecm (GPa)": "{:.1f}".format(ecm),
+ "Ecm Multiplication Factor": "{:.1f}".format(factor_value),
+ }
+
+ def _extract_numeric_grade(self, grade, default=250):
+ digits = ''.join(ch for ch in grade if ch.isdigit())
+ try:
+ return int(digits) if digits else default
+ except ValueError:
+ return default
+
+ def _materials_for_member(self, member):
+ return VALUES_DECK_CONCRETE_GRADE if member == "Deck" else VALUES_MATERIAL
+
+ def _on_member_changed(self, member):
+ if self.current_member:
+ self._save_current_member_form()
+
+ self.current_member = member
+ is_deck = member == "Deck"
+ self.stack.setCurrentWidget(self.deck_page if is_deck else self.steel_page)
+
+ data = self.member_data.get(member)
+ if not data:
+ self.member_data[member] = self._create_default_entry(member)
+ data = self.member_data[member]
+
+ if data.get("is_default"):
+ self._apply_defaults_for_member(member, update_ui=False)
+
+ materials = self._materials_for_member(member)
+ self._loading = True
+ self.material_combo.clear()
+ self.material_combo.addItems(materials)
+ if data["material"] in materials:
+ self.material_combo.setCurrentText(data["material"])
+ elif materials:
+ self.material_combo.setCurrentIndex(0)
+ data["material"] = self.material_combo.currentText()
+
+ self.default_checkbox.setChecked(data.get("is_default", False))
+ if is_deck:
+ self._populate_deck_fields(data)
+ else:
+ self._populate_steel_fields(data)
+ self._loading = False
+
+ def _populate_steel_fields(self, data):
+ for label, widget in self.steel_field_inputs.items():
+ value = data["fields"].get(label, "")
+ # Format to 1 decimal place
+ try:
+ formatted_value = "{:.1f}".format(float(value))
+ widget.setText(formatted_value)
+ except (ValueError, TypeError):
+ widget.setText(value)
+
+ def _populate_deck_fields(self, data):
+ for label, widget in self.deck_field_inputs.items():
+ if label == "Ecm Multiplication Factor":
+ factor_label = data.get("factor_label", DEFAULT_ECM_FACTOR_LABEL)
+ if factor_label not in ECM_FACTOR_LABELS:
+ factor_label = DEFAULT_ECM_FACTOR_LABEL
+ self.deck_factor_combo.blockSignals(True)
+ self.deck_factor_combo.setCurrentText(factor_label)
+ self.deck_factor_combo.blockSignals(False)
+ self._update_custom_factor_visibility(factor_label)
+ self.deck_factor_custom_input.blockSignals(True)
+ custom_val = data.get("custom_factor", "1.0")
+ try:
+ formatted_custom = "{:.1f}".format(float(custom_val))
+ self.deck_factor_custom_input.setText(formatted_custom)
+ except (ValueError, TypeError):
+ self.deck_factor_custom_input.setText(custom_val)
+ self.deck_factor_custom_input.blockSignals(False)
+ else:
+ value = data["fields"].get(label, "")
+ # Format to 1 decimal place
+ try:
+ formatted_value = "{:.1f}".format(float(value))
+ widget.setText(formatted_value)
+ except (ValueError, TypeError):
+ widget.setText(value)
+
+ def _save_current_member_form(self):
+ if not self.current_member:
+ return
+ data = self.member_data.setdefault(self.current_member, self._create_default_entry(self.current_member))
+ data["material"] = self.material_combo.currentText()
+ if self.current_member == "Deck":
+ for label, widget in self.deck_field_inputs.items():
+ if label == "Ecm Multiplication Factor":
+ data["factor_label"] = self.deck_factor_combo.currentText()
+ data["custom_factor"] = self.deck_factor_custom_input.text() or "1.0"
+ else:
+ data["fields"][label] = widget.text()
+ factor_value = self._factor_value_from_label(data["factor_label"], data.get("custom_factor"))
+ data["fields"]["Ecm Multiplication Factor"] = "{:.1f}".format(factor_value)
+ else:
+ for label, widget in self.steel_field_inputs.items():
+ data["fields"][label] = widget.text()
+ data["is_default"] = self.default_checkbox.isChecked()
+
+ def _create_default_entry(self, member):
+ material = self._get_parent_grade(member)
+ return {
+ "material": material,
+ "fields": self._default_fields_for_member(member, material),
+ "is_default": True,
+ "factor_label": DEFAULT_ECM_FACTOR_LABEL if member == "Deck" else None,
+ "custom_factor": "1.0" if member == "Deck" else None,
+ }
+
+ def _apply_defaults_for_member(self, member, update_ui=True):
+ data = self.member_data.setdefault(member, self._create_default_entry(member))
+ grade = self._get_parent_grade(member) or data.get("material")
+ materials = self._materials_for_member(member)
+ if grade not in materials and materials:
+ grade = materials[0]
+ data["material"] = grade
+ if member == "Deck":
+ data["factor_label"] = DEFAULT_ECM_FACTOR_LABEL
+ data["custom_factor"] = "1.0"
+ factor_value = self._factor_value_from_label(DEFAULT_ECM_FACTOR_LABEL)
+ data["fields"] = self._deck_defaults(grade, factor_value)
+ else:
+ data["fields"] = self._steel_defaults(grade)
+ data["is_default"] = True
+
+ if update_ui and member == self.current_member:
+ self._loading = True
+ self.material_combo.setCurrentText(grade)
+ if member == "Deck":
+ self._populate_deck_fields(data)
+ else:
+ self._populate_steel_fields(data)
+ self.default_checkbox.setChecked(True)
+ self._loading = False
+
+ def _factor_value_from_label(self, label, custom_factor=None):
+ for text, value in ECM_FACTOR_OPTIONS:
+ if text == label:
+ if value is None:
+ try:
+ return float(custom_factor) if custom_factor else 1.0
+ except ValueError:
+ return 1.0
+ return value
+ return 1.0
+
+ def _reset_current_member_to_defaults(self):
+ if not self.current_member:
+ return
+
+ self._apply_defaults_for_member(self.current_member, update_ui=False)
+ data = self.member_data.get(self.current_member)
+ if not data:
+ return
+
+ target_material = data.get("material", "")
+ self._loading = True
+ if target_material:
+ index = self.material_combo.findText(target_material)
+ if index >= 0:
+ self.material_combo.setCurrentIndex(index)
+ elif self.material_combo.count() > 0:
+ self.material_combo.setCurrentIndex(0)
+ data["material"] = self.material_combo.currentText()
+ if self.current_member == "Deck":
+ self._populate_deck_fields(data)
+ else:
+ self._populate_steel_fields(data)
+ self._loading = False
+
+ self.default_checkbox.blockSignals(True)
+ self.default_checkbox.setChecked(True)
+ self.default_checkbox.blockSignals(False)
+ self._save_current_member_form()
+
+ def _update_custom_factor_visibility(self, label):
+ is_custom = label == CUSTOM_ECM_FACTOR_LABEL
+ self.deck_factor_custom_input.setVisible(is_custom)
+ self.deck_factor_custom_input.setEnabled(is_custom)
+ self.deck_factor_combo.setVisible(not is_custom)
+
+ def _on_material_changed(self, material):
+ if self._loading:
+ return
+ data = self.member_data.get(self.current_member)
+ if data:
+ data["material"] = material
+ self._handle_user_override()
+
+ def _on_default_toggled(self, state):
+ if self._loading:
+ return
+ try:
+ check_state = Qt.CheckState(state)
+ except ValueError:
+ check_state = Qt.CheckState.Checked if bool(state) else Qt.CheckState.Unchecked
+ if check_state == Qt.CheckState.Checked:
+ self._reset_current_member_to_defaults()
+ else:
+ data = self.member_data.get(self.current_member)
+ if data:
+ data["is_default"] = False
+
+ def _on_factor_changed(self, label):
+ self._update_custom_factor_visibility(label)
+ self._handle_user_override()
+
+ def _handle_user_override(self):
+ if self._loading:
+ return
+ if self.default_checkbox.isChecked():
+ self._loading = True
+ self.default_checkbox.setChecked(False)
+ self._loading = False
+ data = self.member_data.get(self.current_member)
+ if data:
+ data["is_default"] = False
+ self._save_current_member_form()
+
+ def _get_parent_grade(self, member):
+ parent = self.parent_dock
+ if not parent:
+ return ""
+ mapping = {
+ "Girder": getattr(parent, "girder_combo", None),
+ "Cross Bracing": getattr(parent, "cross_bracing_combo", None),
+ "End Diaphragm": getattr(parent, "end_diaphragm_combo", None),
+ "Deck": getattr(parent, "deck_combo", None),
+ }
+ combo = mapping.get(member)
+ return combo.currentText() if combo else ""
+
+ def set_member(self, member):
+ index = self.member_combo.findText(member)
+ if index >= 0:
+ self.member_combo.setCurrentIndex(index)
+
+ def sync_with_parent_defaults(self):
+ for member, data in self.member_data.items():
+ if data.get("is_default"):
+ self._apply_defaults_for_member(member, update_ui=(member == self.current_member))
+
+
+class InputDock(QWidget):
+ def __init__(self, backend, parent):
+ super().__init__()
+ self.parent = parent
+ self.backend = backend
+ self.input_widget = None
+ self.structure_type_combo = None
+ self.project_location_combo = None
+ self.custom_location_input = None
+ self.include_median_combo = None
+ self.footpath_combo = None
+ self.additional_inputs = None
+ self.additional_inputs_widget = None
+ self.material_dialog = None
+ self.additional_inputs_btn = None
+ self.lock_btn = None
+ self.scroll_area = None
+ self.is_locked = False
+
+ self.setStyleSheet("background: transparent;")
+ self.main_layout = QHBoxLayout(self)
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
+ self.main_layout.setSpacing(0)
+
+ self.left_container = QWidget()
+
+ # Get input fields from backend
+ input_field_list = self.backend.input_values()
+
+ self.build_left_panel(input_field_list)
+ self.main_layout.addWidget(self.left_container)
+
+ # Toggle strip
+ self.toggle_strip = QWidget()
+ self.toggle_strip.setStyleSheet("background-color: #90AF13;")
+ self.toggle_strip.setFixedWidth(6)
+ toggle_layout = QVBoxLayout(self.toggle_strip)
+ toggle_layout.setContentsMargins(0, 0, 0, 0)
+ toggle_layout.setSpacing(0)
+ toggle_layout.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
+
+ self.toggle_btn = QPushButton("❮")
+ self.toggle_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ self.toggle_btn.setFixedSize(6, 60)
+ self.toggle_btn.setToolTip("Hide panel")
+ self.toggle_btn.clicked.connect(self.toggle_input_dock)
+ self.toggle_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #6c8408;
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+ padding: 0px;
+ border: none;
+ }
+ QPushButton:hover {
+ background-color: #5e7407;
+ }
+ """)
+ toggle_layout.addStretch()
+ toggle_layout.addWidget(self.toggle_btn)
+ toggle_layout.addStretch()
+ self.main_layout.addWidget(self.toggle_strip)
+
+ def get_validator(self, validator):
+ if validator == 'Int Validator':
+ return QRegularExpressionValidator(QRegularExpression("^(0|[1-9]\\d*)(\\.\\d+)?$"))
+ elif validator == 'Double Validator':
+ return QDoubleValidator()
+ else:
+ return None
+
+ def on_structure_type_changed(self, text):
+ """Handle structure type combo box changes"""
+ if text == "Other":
+ if hasattr(self, 'structure_note'):
+ self.structure_note.setVisible(True)
+ else:
+ if hasattr(self, 'structure_note'):
+ self.structure_note.setVisible(False)
+
+ def show_project_location_dialog(self):
+ """Show Project Location selection dialog"""
+ dialog = ProjectLocationDialog()
+
+ if dialog.exec() == QDialog.Accepted:
+ location_data = dialog.get_selected_location()
+
+ # Process the location data as needed
+ if location_data['method'] == 'coordinates':
+ lat = location_data['data']['latitude']
+ lon = location_data['data']['longitude']
+ print(f"Selected coordinates: {lat}, {lon}")
+
+ elif location_data['method'] == 'location_name':
+ state = location_data['data']['state']
+ district = location_data['data']['district']
+ print(f"Selected location: {district}, {state}")
+
+ elif location_data['method'] == 'map':
+ print("Map selection (to be implemented)")
+
+ if location_data['custom_params']:
+ print("Custom loading parameters requested")
+
+ # Lock-Tooltip-Events-Starts-------------------------------------------------------------------------
+ def eventFilter(self, obj, event):
+ # Check if it's the scroll area and it's a mouse press
+ if obj == self.scroll_area and event.type() == QEvent.MouseButtonPress:
+ if self.is_locked:
+ self.show_lock_tooltip()
+ return True # Block the event
+ return super().eventFilter(obj, event)
+
+ def clear_force_hover(self):
+ if self.lock_btn:
+ self.lock_btn.setProperty("forceHover", False)
+ self.lock_btn.style().polish(self.lock_btn)
+ self.lock_btn.update()
+
+ def show_lock_tooltip(self):
+ # Stop any existing timer first
+ if hasattr(self, 'tooltip_timer') and self.tooltip_timer.isActive():
+ self.tooltip_timer.stop()
+
+ # Position tooltip to the right of the lock button
+ lock_global_pos = self.lock_btn.mapToGlobal(self.lock_btn.rect().topRight())
+ tooltip_pos = lock_global_pos + QPoint(5, 0)
+ self.lock_btn.setProperty("forceHover", True)
+ self.lock_btn.style().polish(self.lock_btn)
+ self.lock_btn.update()
+
+ # Adjust size and position
+ self.lock_btn_tooltip.adjustSize()
+ self.lock_btn_tooltip.move(tooltip_pos)
+ self.lock_btn_tooltip.show()
+ self.lock_btn_tooltip.raise_()
+
+ # Hide after 3 seconds
+ if not hasattr(self, 'tooltip_timer'):
+ self.tooltip_timer = QTimer()
+ self.tooltip_timer.setSingleShot(True)
+ self.tooltip_timer.timeout.connect(self.lock_btn_tooltip.hide)
+ self.tooltip_timer.timeout.connect(self.clear_force_hover)
+
+ self.tooltip_timer.start(3000)
+
+ def toggle_lock(self):
+ self.is_locked = not self.is_locked
+ self.lock_btn.setChecked(self.is_locked)
+ self.scroll_area.setDisabled(self.is_locked)
+ self.update_lock_icon()
+
+ def update_lock_icon(self):
+ if self.lock_btn:
+ if self.is_locked:
+ self.lock_btn.setIcon(QIcon(":/vectors/lock_close.svg"))
+ else:
+ self.lock_btn.setIcon(QIcon(":/vectors/lock_open.svg"))
+
+ def resizeEvent(self, event):
+ super().resizeEvent(event)
+ # Checking hasattr is only meant to prevent errors,
+ # while standalone testing of this widget
+ if self.parent:
+ if self.width() == 0:
+ if hasattr(self.parent, 'update_docking_icons'):
+ self.parent.update_docking_icons(input_is_active=False)
+ elif self.width() > 0:
+ if hasattr(self.parent, 'update_docking_icons'):
+ self.parent.update_docking_icons(input_is_active=True)
+
+
+ def paintEvent(self, event):
+ self.update_lock_icon()
+ return super().paintEvent(event)
+
+ def toggle_input_dock(self):
+ parent = self.parent
+ if hasattr(parent, 'toggle_animate'):
+ is_collapsing = self.width() > 0
+ parent.toggle_animate(show=not is_collapsing, dock='input')
+
+ self.toggle_btn.setText("❯" if is_collapsing else "❮")
+ self.toggle_btn.setToolTip("Show panel" if is_collapsing else "Hide panel")
+
+
+ # Lock-Tooltip-Events-Ends-------------------------------------------------------------------------
+
+ def build_left_panel(self, field_list):
+ left_layout = QVBoxLayout(self.left_container)
+ left_layout.setContentsMargins(0, 0, 0, 0)
+ left_layout.setSpacing(0)
+
+ self.left_panel = QWidget()
+ self.left_panel.setStyleSheet("background-color: white;")
+ panel_layout = QVBoxLayout(self.left_panel)
+ panel_layout.setContentsMargins(15, 10, 15, 10)
+ panel_layout.setSpacing(0)
+
+ # Top Bar with buttons
+ top_bar = QHBoxLayout()
+ top_bar.setSpacing(8)
+ top_bar.setContentsMargins(0, 0, 0, 15)
+
+ input_dock_btn = QPushButton("Basic Inputs")
+ input_dock_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #90AF13;
+ color: white;
+ font-weight: bold;
+ font-size: 13px;
+ border: none;
+ border-radius: 4px;
+ padding: 7px 20px;
+ min-width: 80px;
+ }
+ """)
+ input_dock_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ top_bar.addWidget(input_dock_btn)
+
+ self.additional_inputs_btn = QPushButton("Additional Inputs")
+ self.additional_inputs_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ self.additional_inputs_btn.setStyleSheet("""
+ QPushButton {
+ background-color: white;
+ color: black;
+ font-weight: bold;
+ font-size: 13px;
+ border-radius: 5px;
+ border: 1px solid black;
+ padding: 7px 20px;
+ text-align: center;
+ }
+ QPushButton:hover {
+ background-color: #90AF13;
+ border: 1px solid #90AF13;
+ color: white;
+ }
+ QPushButton:pressed {
+ color: black;
+ background-color: white;
+ border: 1px solid black;
+ }
+ """)
+ self.additional_inputs_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ self.additional_inputs_btn.clicked.connect(self.show_additional_inputs)
+ top_bar.addWidget(self.additional_inputs_btn)
+
+ # Lock button
+ self.lock_btn = QPushButton()
+ self.lock_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #f4f4f4;
+ border: none;
+ padding: 7px;
+ border-radius: 4px;
+ }
+ QPushButton:hover {
+ background-color: #e0e0e0;
+ }
+ QPushButton:checked {
+ background-color: #FFA500;
+ }
+ QPushButton:unchecked {
+ background-color: #f4f4f4;
+ }
+ QPushButton:unchecked:hover {
+ background-color: #e0e0e0;
+ }
+ QPushButton:checked:hover {
+ background-color: #fa7a02;
+ }
+ """)
+ self.lock_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ self.lock_btn.setObjectName("lock_btn")
+ self.lock_btn.setCheckable(True)
+ self.lock_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+ self.lock_btn.clicked.connect(self.toggle_lock)
+ top_bar.addWidget(self.lock_btn)
+ panel_layout.addLayout(top_bar)
+
+ #-Lock-ToolTip--------------------------------------
+ self.lock_btn_tooltip = QLabel("Unlock to Edit")
+ self.lock_btn_tooltip.setStyleSheet("""
+ QLabel{
+ background-color: #f1f1f1;
+ color: #000000;
+ border: 1px solid #90AF13;
+ padding: 4px;
+ font-size: 15px;
+ border-radius: 0px;
+ qproperty-alignment: AlignVCenter;
+ }
+ """)
+ self.lock_btn_tooltip.setObjectName("lock_btn_tooltip")
+ self.lock_btn_tooltip.setWindowFlags(Qt.ToolTip)
+ self.lock_btn_tooltip.hide()
+ #--------------------------------------------------
+
+ # Scroll area
+ scroll_area = QScrollArea()
+ self.scroll_area = scroll_area
+ scroll_area.setWidgetResizable(True)
+ scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+ scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ scroll_area.installEventFilter(self)
+ scroll_area.setStyleSheet("""
+ QScrollArea {
+ background: transparent;
+ padding: 0px 5px;
+ border-top: 1px solid #909090;
+ border-bottom: 1px solid #909090;
+ }
+
+ QScrollArea QScrollBar:vertical {
+ border: none;
+ background: #f0f0f0;
+ width: 8px;
+ margin-left: 2px;
+ }
+
+ QScrollArea QScrollBar::handle:vertical {
+ background: #c0c0c0;
+ border-radius: 4px;
+ min-height: 20px;
+ }
+
+ QScrollArea QScrollBar::handle:vertical:hover {
+ background: #a0a0a0;
+ }
+
+ QScrollArea QScrollBar::handle:vertical:pressed {
+ background: #808080;
+ }
+
+ QScrollArea QScrollBar::add-line:vertical,
+ QScrollArea QScrollBar::sub-line:vertical {
+ border: none;
+ background: none;
+ }
+
+ QScrollArea QScrollBar::add-page:vertical,
+ QScrollArea QScrollBar::sub-page:vertical {
+ background: none;
+ }
+ """)
+
+ group_container = QWidget()
+ self.input_widget = group_container
+ group_container_layout = QVBoxLayout(group_container)
+ group_container_layout.setContentsMargins(0, 0, 0, 0)
+ group_container_layout.setSpacing(12)
+
+ self.section_contexts = {}
+ self.superstructure_body_layout = None
+
+ self._build_basic_inputs(field_list, group_container_layout)
+ self._add_substructure_section(group_container_layout)
+
+ group_container_layout.addStretch()
+ scroll_area.setWidget(group_container)
+
+ self.data = {}
+ panel_layout.addWidget(scroll_area)
+
+ # Bottom buttons
+ btn_button_layout = QHBoxLayout()
+ btn_button_layout.setContentsMargins(0, 15, 0, 0)
+ btn_button_layout.setSpacing(10)
+
+ save_input_btn = DockCustomButton("Save Input", ":/vectors/save.svg")
+ save_input_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ btn_button_layout.addWidget(save_input_btn)
+
+ design_btn = DockCustomButton("Design", ":/vectors/design.svg")
+ design_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ btn_button_layout.addWidget(design_btn)
+
+ panel_layout.addLayout(btn_button_layout)
+
+ # Horizontal scroll area
+ h_scroll_area = QScrollArea()
+ h_scroll_area.setWidgetResizable(True)
+ h_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+ h_scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ h_scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ h_scroll_area.setStyleSheet("""
+ QScrollArea{
+ background: transparent;
+ }
+ QScrollBar:horizontal{
+ background: #E0E0E0;
+ height: 8px;
+ margin: 3px 0px 0px 0px;
+ border-radius: 2px;
+ }
+ QScrollBar::handle:horizontal{
+ background: #A0A0A0;
+ min-width: 30px;
+ border-radius: 2px;
+ }
+ QScrollBar::handle:horizontal:hover{
+ background: #707070;
+ }
+ QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal{
+ width: 0px;
+ }
+ QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal{
+ background: none;
+ }
+ """)
+ h_scroll_area.setWidget(self.left_panel)
+
+ left_layout.addWidget(h_scroll_area)
+ self._apply_lock_state()
+
+ def _build_basic_inputs(self, field_definitions, root_layout):
+ current_section_id = None
+ for definition in field_definitions:
+ key, label, field_type, values, required, validator, metadata = self._normalize_definition(definition)
+ if field_type == TYPE_MODULE:
+ continue
+ if field_type == TYPE_TITLE:
+ section_id = key or label
+ section_context = self._create_section_context(section_id, label, metadata, root_layout)
+ current_section_id = section_context["id"]
+ continue
+ if current_section_id is None:
+ continue
+ section_context = self.section_contexts.get(current_section_id)
+ if not section_context:
+ continue
+ self._create_field_row(section_context, key, label, field_type, values, validator, metadata)
+
+ self._finalize_section_contexts()
+ self._update_carriageway_placeholder()
+
+ def _normalize_definition(self, definition):
+ if len(definition) == 6:
+ return (*definition, {})
+ return definition
+
+ def _create_section_context(self, section_id, title, metadata, root_layout):
+ container_key = (metadata or {}).get("container", "main")
+ parent_layout = self._get_container_layout(container_key, root_layout)
+
+ show_title = metadata.get("show_group_title", True) if metadata else True
+ group_title = title if show_title and title else ""
+ group_box = QGroupBox(group_title) if group_title else QGroupBox()
+ group_box.setStyleSheet(self._section_groupbox_style())
+
+ layout = QVBoxLayout(group_box)
+ layout.setContentsMargins(8, 8, 8, 8)
+ layout.setSpacing(8)
+
+ if metadata and metadata.get("custom_content") == "project_location":
+ self._add_project_location_controls(layout, metadata)
+
+ parent_layout.addWidget(group_box)
+ context = {
+ "id": section_id,
+ "layout": layout,
+ "metadata": metadata or {},
+ "group_box": group_box,
+ }
+ self.section_contexts[section_id] = context
+ return context
+
+ def _section_groupbox_style(self):
+ return (
+ "QGroupBox {\n"
+ " border: 1px solid #90AF13;\n"
+ " border-radius: 4px;\n"
+ " background-color: white;\n"
+ " padding: 8px;\n"
+ " margin-top: 12px;\n"
+ " font-size: 10px;\n"
+ " font-weight: bold;\n"
+ " color: #333;\n"
+ "}\n"
+ "QGroupBox::title {\n"
+ " subcontrol-origin: margin;\n"
+ " subcontrol-position: top left;\n"
+ " left: 8px;\n"
+ " padding: 0 4px;\n"
+ " margin-top: 4px;\n"
+ " background-color: white;\n"
+ " color: #333;\n"
+ "}"
+ )
+
+ def _get_container_layout(self, container_key, root_layout):
+ if container_key == "superstructure":
+ if self.superstructure_body_layout is None:
+ self.superstructure_body_layout = self._create_superstructure_group(root_layout)
+ return self.superstructure_body_layout
+ return root_layout
+
+ def _create_superstructure_group(self, root_layout):
+ structure_group = QGroupBox()
+ structure_group.setStyleSheet(
+ "QGroupBox {\n"
+ " border: 1px solid #90AF13;\n"
+ " border-radius: 5px;\n"
+ " margin-top: 0px;\n"
+ " padding-top: 5px;\n"
+ " background-color: white;\n"
+ "}"
+ )
+ structure_layout = QVBoxLayout()
+ structure_layout.setContentsMargins(10, 10, 10, 10)
+ structure_layout.setSpacing(10)
+
+ header = QHBoxLayout()
+ title = QLabel("Superstructure")
+ title.setStyleSheet("font-size: 13px; font-weight: bold; color: #333;")
+ header.addWidget(title)
+ header.addStretch()
+
+ toggle_btn = QPushButton()
+ toggle_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ toggle_btn.setCheckable(True)
+ toggle_btn.setChecked(True)
+ toggle_btn.setIcon(QIcon(":/vectors/arrow_up_light.svg"))
+ toggle_btn.setIconSize(QSize(20, 20))
+ toggle_btn.setStyleSheet(
+ "QPushButton {\n"
+ " background: transparent;\n"
+ " border: none;\n"
+ " padding: 2px;\n"
+ "}\n"
+ "QPushButton:hover {\n"
+ " background: transparent;\n"
+ "}\n"
+ "QPushButton:pressed {\n"
+ " background: transparent;\n"
+ "}"
+ )
+ header.addWidget(toggle_btn)
+ structure_layout.addLayout(header)
+
+ structure_body = QFrame()
+ structure_body.setFrameShape(QFrame.NoFrame)
+ body_layout = QVBoxLayout(structure_body)
+ body_layout.setContentsMargins(0, 0, 0, 0)
+ body_layout.setSpacing(10)
+ structure_body.setVisible(True)
+ structure_layout.addWidget(structure_body)
+
+ def _toggle(checked):
+ structure_body.setVisible(checked)
+ toggle_btn.setIcon(QIcon(":/vectors/arrow_up_light.svg" if checked else ":/vectors/arrow_down_light.svg"))
+
+ toggle_btn.toggled.connect(_toggle)
+
+ structure_group.setLayout(structure_layout)
+ root_layout.addWidget(structure_group)
+ return body_layout
+
+ def _add_project_location_controls(self, layout, metadata):
+ label_text = metadata.get("header_label") or "Project Location*"
+ button_rows = metadata.get("button_rows")
+ if button_rows:
+ for row_entry in button_rows:
+ row_config = self._prepare_button_row_config(row_entry, {"label": label_text})
+ if row_config:
+ self._add_button_row(layout, row_config)
+ return
+
+ fallback_row = self._prepare_button_row_config("project_location", {"label": label_text})
+ self._add_button_row(layout, fallback_row)
+
+ def _section_label_style(self):
+ return (
+ "QLabel {\n"
+ " color: #000000;\n"
+ " font-size: 12px;\n"
+ " background: transparent;\n"
+ "}"
+ )
+
+ def _default_action_button_style(self):
+ return (
+ "QPushButton {\n"
+ " background-color: #90AF13;\n"
+ " color: white;\n"
+ " font-weight: bold;\n"
+ " border: none;\n"
+ " border-radius: 4px;\n"
+ " padding: 8px 20px;\n"
+ " font-size: 11px;\n"
+ " min-width: 80px;\n"
+ "}\n"
+ "QPushButton:hover {\n"
+ " background-color: #7a9a12;\n"
+ "}\n"
+ "QPushButton:disabled{\n"
+ " background: #D0D0D0;\n"
+ " color: #666;\n"
+ "}"
+ )
+
+ def _default_row_config(self, row_type):
+ mapping = {
+ "project_location": {
+ "label": "Project Location*",
+ "buttons": [
+ {"text": "Add Here", "action": "show_project_location_dialog"},
+ ],
+ },
+ "additional_geometry": {
+ "label": "Additional Geometry",
+ "buttons": [
+ {"text": "Modify Here", "action": "show_additional_inputs"},
+ ],
+ },
+ "material_properties": {
+ "label": "Properties",
+ "buttons": [
+ {"text": "Modify Here", "action": "show_material_properties_dialog"},
+ ],
+ },
+ }
+ return mapping.get(row_type, {})
+
+ def _prepare_button_row_config(self, config_entry, fallback_defaults=None):
+ fallback_defaults = fallback_defaults or {}
+ if isinstance(config_entry, str):
+ config = {"type": config_entry}
+ else:
+ config = dict(config_entry or {})
+
+ row_type = config.get("type")
+ defaults = self._default_row_config(row_type)
+
+ resolved = {}
+ resolved.update(defaults)
+ resolved.update(fallback_defaults)
+ resolved.update(config)
+
+ if not resolved.get("buttons"):
+ extra_defaults = self._default_row_config(resolved.get("type"))
+ if extra_defaults:
+ resolved.setdefault("buttons", extra_defaults.get("buttons"))
+ resolved.setdefault("label", extra_defaults.get("label"))
+
+ return resolved if resolved.get("buttons") else None
+
+ def _add_button_row(self, layout, config):
+ if not config:
+ return
+
+ row = QHBoxLayout()
+ row.setContentsMargins(0, 0, 0, 0)
+ row.setSpacing(8)
+
+ label_text = config.get("label")
+ if label_text:
+ field_label = QLabel(label_text)
+ field_label.setStyleSheet(self._section_label_style())
+ field_label.setMinimumWidth(config.get("label_min_width", 110))
+ row.addWidget(field_label)
+
+ buttons = config.get("buttons", [])
+ for button_config in buttons:
+ button = self._create_action_button(button_config)
+ stretch = button_config.get("stretch", 1 if len(buttons) == 1 else 0)
+ row.addWidget(button, stretch)
+
+ if config.get("add_stretch", True):
+ row.addStretch()
+
+ layout.addLayout(row)
+
+ def _create_action_button(self, config):
+ button = QPushButton(config.get("text", "Action"))
+ button.setCursor(Qt.CursorShape.PointingHandCursor)
+ if config.get("size_policy") == "fixed":
+ button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+ else:
+ button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+
+ icon_path = config.get("icon")
+ if icon_path:
+ button.setIcon(QIcon(icon_path))
+ icon_size = config.get("icon_size")
+ if isinstance(icon_size, (list, tuple)) and len(icon_size) == 2:
+ button.setIconSize(QSize(icon_size[0], icon_size[1]))
+
+ style = config.get("style") or self._default_action_button_style()
+ button.setStyleSheet(style)
+
+ tooltip = config.get("tooltip")
+ if tooltip:
+ button.setToolTip(tooltip)
+
+ action_name = config.get("action")
+ callback = getattr(self, action_name, None) if action_name else None
+ if callable(callback):
+ button.clicked.connect(callback)
+ else:
+ button.setEnabled(False)
+
+ return button
+
+ def _create_field_row(self, section_context, key, label, field_type, values, validator, metadata):
+ widget = self._create_input_widget(key, field_type, values, validator, metadata)
+ if widget is None:
+ return
+ row = QHBoxLayout()
+ row.setContentsMargins(0, 0, 0, 0)
+ row.setSpacing(8)
+ display_label = (metadata or {}).get("label") if metadata else None
+ field_label = QLabel(display_label or label)
+ field_label.setStyleSheet(self._section_label_style())
+ field_label.setMinimumWidth(110)
+ row.addWidget(field_label)
+ row.addWidget(widget, 1)
+ if metadata.get("add_stretch"):
+ row.addStretch()
+ section_context["layout"].addLayout(row)
+
+ def _create_input_widget(self, key, field_type, values, validator, metadata):
+ if field_type == TYPE_COMBOBOX:
+ widget = NoScrollComboBox()
+ apply_field_style(widget)
+ if values:
+ widget.addItems(values)
+ default_value = (metadata or {}).get("default")
+ if default_value:
+ idx = widget.findText(default_value)
+ if idx >= 0:
+ widget.setCurrentIndex(idx)
+ elif field_type == TYPE_TEXTBOX:
+ widget = QLineEdit()
+ apply_field_style(widget)
+ validator_instance = self.get_validator(validator)
+ if validator_instance:
+ widget.setValidator(validator_instance)
+ else:
+ return None
+
+ if key:
+ widget.setObjectName(key)
+ self._register_input_widget(key, widget)
+ self._apply_field_specific_config(key, widget, metadata or {})
+ return widget
+
+ def _register_input_widget(self, key, widget):
+ if key == KEY_STRUCTURE_TYPE:
+ self.structure_type_combo = widget
+ elif key == KEY_SPAN:
+ self.span_input = widget
+ elif key == KEY_CARRIAGEWAY_WIDTH:
+ self.carriageway_input = widget
+ elif key == KEY_INCLUDE_MEDIAN:
+ self.include_median_combo = widget
+ elif key == KEY_FOOTPATH:
+ self.footpath_combo = widget
+ elif key == KEY_SKEW_ANGLE:
+ self.skew_input = widget
+ elif key == KEY_GIRDER:
+ self.girder_combo = widget
+ elif key == KEY_CROSS_BRACING:
+ self.cross_bracing_combo = widget
+ elif key == KEY_END_DIAPHRAGM:
+ self.end_diaphragm_combo = widget
+ elif key == KEY_DECK_CONCRETE_GRADE_BASIC:
+ self.deck_combo = widget
+
+ def _apply_field_specific_config(self, key, widget, metadata):
+ if not key or widget is None:
+ return
+ if key == KEY_STRUCTURE_TYPE and hasattr(widget, "currentTextChanged"):
+ widget.currentTextChanged.connect(self.on_structure_type_changed)
+ elif key == KEY_SPAN and isinstance(widget, QLineEdit):
+ widget.setValidator(QDoubleValidator(SPAN_MIN, SPAN_MAX, 2))
+ widget.setPlaceholderText(f"{SPAN_MIN}-{SPAN_MAX} m")
+ elif key == KEY_CARRIAGEWAY_WIDTH and isinstance(widget, QLineEdit):
+ widget.setValidator(QDoubleValidator(0.0, 100.0, 2))
+ widget.editingFinished.connect(self.validate_carriageway_width)
+ elif key == KEY_INCLUDE_MEDIAN and hasattr(widget, "currentTextChanged"):
+ widget.currentTextChanged.connect(self.on_include_median_changed)
+ default_value = metadata.get("default")
+ if default_value:
+ idx = widget.findText(default_value)
+ if idx >= 0:
+ widget.setCurrentIndex(idx)
+ elif key == KEY_FOOTPATH and hasattr(widget, "currentTextChanged"):
+ widget.currentTextChanged.connect(self.on_footpath_changed)
+ default_value = metadata.get("default")
+ if default_value:
+ idx = widget.findText(default_value)
+ if idx >= 0:
+ widget.setCurrentIndex(idx)
+ elif key == KEY_SKEW_ANGLE and isinstance(widget, QLineEdit):
+ widget.setValidator(QDoubleValidator(SKEW_ANGLE_MIN, SKEW_ANGLE_MAX, 1))
+ widget.setPlaceholderText(f"{SKEW_ANGLE_MIN} - {SKEW_ANGLE_MAX}°")
+ elif key == KEY_DECK_CONCRETE_GRADE_BASIC and hasattr(widget, "findText"):
+ default_value = metadata.get("default")
+ if default_value:
+ idx = widget.findText(default_value)
+ if idx >= 0:
+ widget.setCurrentIndex(idx)
+
+ def _finalize_section_contexts(self):
+ for context in self.section_contexts.values():
+ metadata = context.get("metadata", {})
+ note_config = metadata.get("post_note")
+ if note_config:
+ self._add_section_note(context, note_config)
+
+ for row_entry in metadata.get("post_rows", []):
+ row_config = self._prepare_button_row_config(row_entry)
+ if row_config:
+ self._add_button_row(context["layout"], row_config)
+
+ def _add_section_note(self, context, note_config):
+ note_label = QLabel(note_config.get("text", ""))
+ note_label.setStyleSheet(self._section_label_style())
+ note_label.setVisible(False)
+ context["layout"].addWidget(note_label)
+ attr_name = note_config.get("attr")
+ if attr_name:
+ setattr(self, attr_name, note_label)
+
+
+ def _add_substructure_section(self, parent_layout):
+ sub_group = QGroupBox()
+ sub_group.setStyleSheet(
+ "QGroupBox {\n"
+ " border: 1px solid #90AF13;\n"
+ " border-radius: 5px;\n"
+ " margin-top: 8px;\n"
+ " padding-top: 5px;\n"
+ " background-color: white;\n"
+ "}"
+ )
+ sub_layout = QVBoxLayout()
+ sub_layout.setContentsMargins(10, 10, 10, 10)
+ sub_layout.setSpacing(8)
+
+ header = QHBoxLayout()
+ header.setContentsMargins(0, 0, 0, 0)
+ header.setSpacing(8)
+ title = QLabel("Substructure")
+ title.setStyleSheet("font-size: 13px; font-weight: bold; color: #333;")
+ header.addWidget(title)
+ header.addStretch()
+
+ toggle_btn = QPushButton()
+ toggle_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ toggle_btn.setCheckable(True)
+ toggle_btn.setChecked(True)
+ toggle_btn.setIcon(QIcon(":/vectors/arrow_up_light.svg"))
+ toggle_btn.setIconSize(QSize(20, 20))
+ toggle_btn.setStyleSheet(
+ "QPushButton {\n"
+ " background: transparent;\n"
+ " border: none;\n"
+ " padding: 2px;\n"
+ "}\n"
+ "QPushButton:hover {\n"
+ " background: transparent;\n"
+ "}\n"
+ "QPushButton:pressed {\n"
+ " background: transparent;\n"
+ "}"
+ )
+ header.addWidget(toggle_btn)
+ sub_layout.addLayout(header)
+
+ sub_body = QFrame()
+ sub_body.setFrameShape(QFrame.NoFrame)
+ sub_body_layout = QVBoxLayout(sub_body)
+ sub_body_layout.setContentsMargins(0, 0, 0, 0)
+ sub_body_layout.setSpacing(6)
+ sub_body.setVisible(True)
+ sub_layout.addWidget(sub_body)
+
+ def _toggle(checked):
+ sub_body.setVisible(checked)
+ toggle_btn.setIcon(QIcon(":/vectors/arrow_up_light.svg" if checked else ":/vectors/arrow_down_light.svg"))
+
+ toggle_btn.toggled.connect(_toggle)
+
+ sub_group.setLayout(sub_layout)
+ parent_layout.addWidget(sub_group)
+
+ def show_additional_inputs(self):
+ """Show Additional Inputs dialog"""
+ footpath_value = self.footpath_combo.currentText() if self.footpath_combo else "None"
+
+ carriageway_width = self._get_effective_carriageway_width()
+
+ self.additional_inputs = AdditionalInputs(footpath_value, carriageway_width)
+ self.additional_inputs.show()
+
+ def _apply_lock_state(self):
+ self.update_lock_icon()
+
+ enabled = not self.is_locked
+ if self.scroll_area:
+ self.scroll_area.setEnabled(enabled)
+ if self.input_widget:
+ self.input_widget.setEnabled(enabled)
+ self._set_additional_inputs_enabled(enabled)
+
+ if self.material_dialog:
+ self.material_dialog.setEnabled(enabled)
+
+ def _set_additional_inputs_enabled(self, enabled):
+ if self.additional_inputs_widget:
+ self.additional_inputs_widget.setEnabled(enabled)
+
+ def _handle_additional_inputs_closed(self):
+ self.additional_inputs = None
+ self.additional_inputs_widget = None
+
+ def on_footpath_changed(self, footpath_value):
+ """Update additional inputs when footpath changes"""
+ if self.additional_inputs and self.additional_inputs.isVisible():
+ if hasattr(self, 'additional_inputs_widget'):
+ self.additional_inputs_widget.update_footpath_value(footpath_value)
+
+ def on_include_median_changed(self, _value):
+ self._update_carriageway_placeholder()
+ # Re-validate silently so previously entered values honor the new limits
+ self.validate_carriageway_width(show_message=False)
+
+ def _carriageway_limits(self):
+ include_median = self._is_median_included()
+ min_width = CARRIAGEWAY_WIDTH_MIN_WITH_MEDIAN if include_median else CARRIAGEWAY_WIDTH_MIN
+ return min_width, CARRIAGEWAY_WIDTH_MAX_LIMIT
+
+ def _update_carriageway_placeholder(self):
+ if not hasattr(self, "carriageway_input") or self.carriageway_input is None:
+ return
+ min_width, max_width = self._carriageway_limits()
+ suffix = " per side" if self._is_median_included() else ""
+ self.carriageway_input.setPlaceholderText(f"{min_width:.2f} - {max_width:.1f} m{suffix}")
+
+ def validate_carriageway_width(self, show_message=True):
+ if not self.carriageway_input:
+ return
+ text = self.carriageway_input.text().strip()
+ if not text:
+ return
+ try:
+ value = float(text)
+ except ValueError:
+ self.carriageway_input.clear()
+ if show_message:
+ QMessageBox.warning(self, "Carriageway Width", "Please enter a numeric carriageway width.")
+ return
+
+ min_width, max_width = self._carriageway_limits()
+ include_median = self._is_median_included()
+ message = None
+
+ if value < min_width:
+ if include_median:
+ message = "IRC 5 Clause 104.3.1 requires minimum carriageway width on both sides of the median to be at least 7.5 m."
+ else:
+ message = "IRC 5 Clause 104.3.1 requires minimum carriageway width of 4.25 m."
+ value = min_width
+ elif value > max_width:
+ message = "Software limits carriageway width upto 23.6 m"
+ value = max_width
+
+ self.carriageway_input.setText(f"{value:.2f}")
+ if message and show_message:
+ QMessageBox.warning(self, "Carriageway Width", message)
+
+ def _get_effective_carriageway_width(self):
+ min_width, max_width = self._carriageway_limits()
+ width = min_width
+ if self.carriageway_input and self.carriageway_input.text():
+ try:
+ width = float(self.carriageway_input.text())
+ except ValueError:
+ width = min_width
+ width = max(min_width, min(width, max_width))
+ if self._is_median_included():
+ return width * 2.0 # Two carriageways, one on each side of the median
+ return width
+
+ def _is_median_included(self):
+ if not self.include_median_combo:
+ return False
+ return self.include_median_combo.currentText().lower() == "yes"
+
+ def show_material_properties_dialog(self):
+ """Open the material properties dialog with the relevant member selected."""
+ if self.material_dialog is None:
+ self.material_dialog = MaterialPropertiesDialog(self)
+
+ member = "Girder"
+ focus_widget = QApplication.focusWidget()
+ focus_map = {
+ getattr(self, 'girder_combo', None): "Girder",
+ getattr(self, 'deck_combo', None): "Deck",
+ getattr(self, 'cross_bracing_combo', None): "Cross Bracing",
+ getattr(self, 'end_diaphragm_combo', None): "End Diaphragm",
+ }
+ for widget, name in focus_map.items():
+ if widget is not None and widget is focus_widget:
+ member = name
+ break
+
+ self.material_dialog.sync_with_parent_defaults()
+ self.material_dialog.set_member(member)
+ self.material_dialog.show()
+ self.material_dialog.raise_()
+ self.material_dialog.activateWindow()
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/docks/log_dock.py b/src/osdagbridge/desktop/ui/docks/log_dock.py
new file mode 100644
index 00000000..37f28d33
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/docks/log_dock.py
@@ -0,0 +1,88 @@
+"""
+Log dock widget for Osdag GUI.
+Displays log messages and status updates.
+"""
+from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QLabel
+from PySide6.QtCore import Qt, QDateTime
+
+class LogDock(QWidget):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ # Ensures automatic deletion when closed
+ self.setAttribute(Qt.WA_DeleteOnClose, True)
+ self.is_visible = True
+ self.setObjectName("logs_dock")
+ self.init_ui()
+ self.adjust_size()
+
+ def init_ui(self):
+ # Create layout for the log dock
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(5, 2, 5, 0)
+ layout.setSpacing(0)
+
+ # Create a top strip for "Log Window"
+ self.log_window_title = QLabel("Log Window")
+ self.log_window_title.setAlignment(Qt.AlignLeft)
+ layout.addWidget(self.log_window_title)
+
+ # Create log display area
+ self.log_display = QTextEdit()
+ self.log_display.setObjectName("textEdit")
+ self.log_display.setReadOnly(True)
+ self.log_display.setOverwriteMode(True)
+ layout.addWidget(self.log_display)
+
+ # Add init log text matching
+ self.append_log(f"[{QDateTime.currentDateTime().toString('yyyy-MM-dd hh:mm:ss')}] Log initialized", "info")
+
+ self.setLayout(layout)
+ self.show() # Show init text
+
+ def append_log(self, message, log_level="info"):
+ """Append a message to the log display with specified color."""
+ if log_level == "error":
+ color = "#FF0000" # Red for errors
+ elif log_level == "info":
+ color = "#A6A6A6" # Black for info
+ elif log_level == "success":
+ color = "#008000" # Green for success
+
+ formatted_message = f"{message}"
+ self.log_display.append(formatted_message)
+ self.log_display.ensureCursorVisible()
+
+ def toggle_log_dock(self):
+ """Toggle the visibility of the log dock."""
+ self.is_visible = not self.is_visible
+ if self.is_visible:
+ self.show()
+ self.adjust_size()
+ self.move(0, self.parent().height() - self.height())
+ else:
+ self.hide()
+
+ def adjust_size(self):
+ """Adjust the size of the log dock based on input and output dock states."""
+ parent = self.parent()
+ if not parent:
+ return
+ if parent.input_dock is None or parent.output_dock is None:
+ return
+
+ input_dock = parent.input_dock
+ output_dock = parent.output_dock
+
+ # Calculate available width
+ parent_width = parent.width()
+ input_dock_width = input_dock.width() if input_dock.isVisible() else 0
+ output_dock_width = output_dock.width() if output_dock.isVisible() else 0
+ available_width = parent_width - input_dock_width - output_dock_width
+
+ # Set log dock size
+ default_height = 150 # Fixed height for log dock
+ self.setFixedSize(available_width, default_height)
+
+ # Update position if visible
+ if self.is_visible:
+ self.move(0, parent.height() - default_height)
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/docks/output_dock.py b/src/osdagbridge/desktop/ui/docks/output_dock.py
new file mode 100644
index 00000000..9ec492fc
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/docks/output_dock.py
@@ -0,0 +1,578 @@
+from PySide6.QtWidgets import (
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy,
+ QPushButton, QGroupBox, QCheckBox, QScrollArea, QFrame, QComboBox, QLineEdit
+)
+from PySide6.QtCore import Qt, QSize
+from PySide6.QtGui import QIcon
+
+from osdagbridge.desktop.ui.utils.custom_buttons import DockCustomButton
+from osdagbridge.core.utils.common import TYPE_TITLE
+
+class NoScrollComboBox(QComboBox):
+ def wheelEvent(self, event):
+ event.ignore() # Prevent changing selection on scroll
+
+def apply_field_style(widget):
+ widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ widget.setMinimumHeight(28)
+ if isinstance(widget, QComboBox):
+ style = """
+ QComboBox{
+ padding: 1px 7px;
+ border: 1px solid black;
+ border-radius: 5px;
+ background-color: white;
+ color: black;
+ }
+ QComboBox::drop-down{
+ subcontrol-origin: padding;
+ subcontrol-position: top right;
+ border-left: 0px;
+ }
+ QComboBox::down-arrow{
+ image: url(:/vectors/arrow_down_light.svg);
+ width: 20px;
+ height: 20px;
+ margin-right: 8px;
+ }
+ QComboBox::down-arrow:on {
+ image: url(:/vectors/arrow_up_light.svg);
+ width: 20px;
+ height: 20px;
+ margin-right: 8px;
+ }
+ QComboBox QAbstractItemView{
+ background-color: white;
+ border: 1px solid black;
+ outline: none;
+ }
+ QComboBox QAbstractItemView::item{
+ color: black;
+ background-color: white;
+ border: none;
+ border: 1px solid white;
+ border-radius: 0;
+ padding: 2px;
+ }
+ QComboBox QAbstractItemView::item:hover{
+ border: 1px solid #90AF13;
+ background-color: #90AF13;
+ color: black;
+ }
+ QComboBox QAbstractItemView::item:selected{
+ background-color: #90AF13;
+ color: black;
+ border: 1px solid #90AF13;
+ }
+ QComboBox QAbstractItemView::item:selected:hover{
+ background-color: #90AF13;
+ color: black;
+ border: 1px solid #94b816;
+ }
+ """
+ widget.setStyleSheet(style)
+ elif isinstance(widget, QLineEdit):
+ widget.setStyleSheet("""
+ QLineEdit {
+ padding: 1px 7px;
+ border: 1px solid #070707;
+ border-radius: 6px;
+ background-color: white;
+ color: #000000;
+ font-weight: normal;
+ }
+ """)
+
+class OutputDock(QWidget):
+ """Output dock with collapsible design controls and scrollable layout."""
+
+ def __init__(self, backend=None, parent=None):
+ super().__init__()
+ self.parent = parent
+ self.backend = backend
+ self.setStyleSheet("background: transparent;")
+ configs = self._load_configs()
+ self.analysis_config = configs.get("analysis")
+ # Configurable button rows per section; populated from backend ui_fields
+ self.section_configs = configs.get("design", [])
+ self.init_ui()
+
+ def toggle_output_dock(self):
+ parent = self.parent
+ if hasattr(parent, 'toggle_animate'):
+ is_collapsing = self.width() > 0
+ parent.toggle_animate(show=not is_collapsing, dock='output')
+
+ self.toggle_btn.setText("❮" if is_collapsing else "❯")
+ self.toggle_btn.setToolTip("Show panel" if is_collapsing else "Hide panel")
+
+ def resizeEvent(self, event):
+ super().resizeEvent(event)
+ # Checking hasattr is only meant to prevent errors
+ if self.parent:
+ if self.width() == 0:
+ if hasattr(self.parent, 'update_docking_icons'):
+ self.parent.update_docking_icons(output_is_active=False)
+ elif self.width() > 0:
+ if hasattr(self.parent, 'update_docking_icons'):
+ self.parent.update_docking_icons(output_is_active=True)
+
+
+ def init_ui(self):
+ # Main horizontal layout to hold toggle strip and content
+ self.main_layout = QHBoxLayout(self)
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
+ self.main_layout.setSpacing(0)
+
+ # Toggle strip on the left
+ self.toggle_strip = QWidget()
+ self.toggle_strip.setStyleSheet("background-color: #90AF13;")
+ self.toggle_strip.setFixedWidth(6)
+ toggle_layout = QVBoxLayout(self.toggle_strip)
+ toggle_layout.setContentsMargins(0, 0, 0, 0)
+ toggle_layout.setSpacing(0)
+ toggle_layout.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
+
+ self.toggle_btn = QPushButton("❯")
+ self.toggle_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ self.toggle_btn.setFixedSize(6, 60)
+ self.toggle_btn.setToolTip("Hide panel")
+ self.toggle_btn.clicked.connect(self.toggle_output_dock)
+ self.toggle_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #7a9a12;
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+ padding: 0px;
+ border: none;
+ }
+ QPushButton:hover {
+ background-color: #6a8a10;
+ }
+ """)
+ toggle_layout.addStretch()
+ toggle_layout.addWidget(self.toggle_btn)
+ toggle_layout.addStretch()
+ self.main_layout.addWidget(self.toggle_strip)
+
+ # Content container
+ content_container = QWidget()
+ content_container.setStyleSheet("background-color: white;")
+ content_layout = QVBoxLayout(content_container)
+ content_layout.setContentsMargins(8, 8, 8, 8)
+ content_layout.setSpacing(10)
+
+ # Top Bar with buttons
+ top_bar = QHBoxLayout()
+ top_bar.setSpacing(8)
+ top_bar.setContentsMargins(0, 0, 0, 15)
+
+ input_dock_btn = QPushButton("Output Dock")
+ input_dock_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #90AF13;
+ color: white;
+ font-weight: bold;
+ font-size: 13px;
+ border: none;
+ border-radius: 4px;
+ padding: 7px 20px;
+ min-width: 80px;
+ }
+ """)
+ input_dock_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ top_bar.addWidget(input_dock_btn)
+ top_bar.addStretch()
+ content_layout.addLayout(top_bar)
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ scroll.setStyleSheet("QScrollArea { border: none; background: white; }")
+
+ scroll_content = QWidget()
+ scroll_layout = QVBoxLayout(scroll_content)
+ scroll_layout.setContentsMargins(0, 0, 0, 0)
+ scroll_layout.setSpacing(10)
+
+ analysis_group = self._build_analysis_group()
+ if analysis_group:
+ scroll_layout.addWidget(analysis_group)
+
+ design_group = QGroupBox("Design")
+ design_group.setStyleSheet(
+ """
+ QGroupBox {
+ font-weight: bold;
+ font-size: 11px;
+ color: #333;
+ border: 1px solid #90AF13;
+ border-radius: 4px;
+ margin-top: 8px;
+ padding-top: 12px;
+ background-color: white;
+ }
+ QGroupBox::title {
+ subcontrol-origin: margin;
+ subcontrol-position: top left;
+ left: 8px;
+ padding: 0 4px;
+ background-color: white;
+ }
+ """
+ )
+ design_layout = QVBoxLayout(design_group)
+ design_layout.setContentsMargins(10, 8, 10, 10)
+ design_layout.setSpacing(8)
+
+ # Dynamic design sections
+ for section_cfg in self.section_configs:
+ section_group = self._create_toggle_group(section_cfg)
+ design_layout.addWidget(section_group)
+
+ scroll_layout.addWidget(design_group)
+ scroll_layout.addStretch()
+
+ scroll.setWidget(scroll_content)
+ content_layout.addWidget(scroll)
+
+ h_layout = QHBoxLayout()
+ h_layout.setSpacing(5)
+ h_layout.setContentsMargins(0, 0, 0, 0)
+
+ results_btn = DockCustomButton("Generate Results Table", ":/vectors/design_report.svg")
+ results_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ h_layout.addWidget(results_btn)
+
+ report_btn = DockCustomButton("Generate Report", ":/vectors/design_report.svg")
+ report_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+
+ h_layout.addWidget(report_btn)
+ content_layout.addLayout(h_layout)
+
+ # Add content container to main layout
+ self.main_layout.addWidget(content_container)
+
+ def show_additional_inputs(self):
+ """Handle showing additional geometry inputs."""
+ # Implement your logic here
+ print("Show additional inputs clicked")
+
+ # --- Helpers for dynamic section/button rendering ---
+ def _load_configs(self):
+ if self.backend and hasattr(self.backend, "output_values"):
+ try:
+ cfg = self.backend.output_values(flag=None)
+ if cfg is not None:
+ return self._normalize_section_configs(cfg)
+ except Exception:
+ pass
+ return {"analysis": None, "design": []}
+
+ def _normalize_section_configs(self, cfg):
+ result = {"analysis": None, "design": []}
+ if not cfg:
+ return result
+
+ # Already structured dict
+ if isinstance(cfg, dict):
+ result["analysis"] = cfg.get("analysis")
+ if isinstance(cfg.get("design"), list):
+ result["design"] = cfg.get("design")
+ return result
+
+ # Legacy dict list: treat as design-only
+ if isinstance(cfg, list) and all(isinstance(item, dict) for item in cfg):
+ result["design"] = cfg
+ return result
+
+ # Tuple-based definitions similar to input_values
+ if isinstance(cfg, list) and all(isinstance(item, tuple) for item in cfg):
+ for item in cfg:
+ if len(item) < 7:
+ continue
+ _, display_name, ui_type, _, is_visible, _, metadata = item
+ if ui_type != TYPE_TITLE or not is_visible:
+ continue
+ metadata = metadata or {}
+ kind = metadata.get("kind", "design")
+ if kind == "analysis":
+ result["analysis"] = {
+ "title": display_name,
+ "fields": metadata.get("fields", []),
+ }
+ continue
+ rows = metadata.get("rows") or metadata.get("post_rows") or []
+ if not isinstance(rows, list):
+ rows = []
+ result["design"].append({"title": display_name, "rows": rows})
+ return result
+
+ return result
+
+ def _default_analysis_config(self):
+ return {
+ "title": "Analysis Results",
+ "fields": [
+ {"type": "combobox", "label": "Member:", "values": ["All"]},
+ {
+ "type": "combobox",
+ "label": "Load Combination:",
+ "values": ["Envelope"],
+ },
+ {
+ "type": "checkbox_grid",
+ "columns": [["Fx", "Mx", "Dx"], ["Fy", "My", "Dy"], ["Fz", "Mz", "Dz"]],
+ },
+ {
+ "type": "checkbox_row",
+ "label": "Display Options:",
+ "options": ["Max", "Min"],
+ },
+ {"type": "checkbox", "label": "Controlling Utilization Ratio"},
+ ],
+ }
+
+ def _analysis_group_style(self):
+ return (
+ "QGroupBox {\n"
+ " font-weight: bold;\n"
+ " font-size: 11px;\n"
+ " color: #333;\n"
+ " border: 1px solid #90AF13;\n"
+ " border-radius: 4px;\n"
+ " margin-top: 8px;\n"
+ " padding-top: 12px;\n"
+ " background-color: white;\n"
+ "}\n"
+ "QGroupBox::title {\n"
+ " subcontrol-origin: margin;\n"
+ " subcontrol-position: top left;\n"
+ " left: 8px;\n"
+ " padding: 0 4px;\n"
+ " background-color: white;\n"
+ "}"
+ )
+
+ def _build_analysis_group(self):
+ cfg = self.analysis_config or self._default_analysis_config()
+ if not cfg:
+ return None
+
+ group = QGroupBox(cfg.get("title", "Analysis Results"))
+ group.setStyleSheet(self._analysis_group_style())
+ layout = QVBoxLayout(group)
+ layout.setContentsMargins(10, 8, 10, 10)
+ layout.setSpacing(8)
+
+ for field_cfg in cfg.get("fields", []):
+ self._add_analysis_field(layout, field_cfg)
+
+ return group
+
+ def _normalize_field_cfg(self, field_cfg):
+ if isinstance(field_cfg, dict):
+ return field_cfg
+ if isinstance(field_cfg, tuple) and len(field_cfg) >= 7:
+ key, label, field_type, values, is_visible, _validator, metadata = field_cfg
+ if not is_visible:
+ return None
+ meta = metadata or {}
+ normalized = {
+ "key": key,
+ "label": label,
+ "type": field_type,
+ "values": values,
+ }
+ normalized.update(meta)
+ return normalized
+ return None
+
+ def _add_analysis_field(self, layout, field_cfg):
+ cfg = self._normalize_field_cfg(field_cfg)
+ if not cfg:
+ return
+ field_type = cfg.get("type")
+ if field_type == "combobox":
+ row = QHBoxLayout()
+ row.setContentsMargins(0, 0, 0, 0)
+ row.setSpacing(8)
+ label = QLabel(cfg.get("label", ""))
+ label.setStyleSheet("font-size: 10px; color: #333; font-weight: normal;")
+ label.setMinimumWidth(cfg.get("label_min_width", 100))
+ combo = NoScrollComboBox()
+ values = cfg.get("values") or []
+ combo.addItems(values)
+ default = cfg.get("default")
+ if default and default in values:
+ combo.setCurrentText(default)
+ apply_field_style(combo)
+ row.addWidget(label)
+ row.addWidget(combo)
+ layout.addLayout(row)
+ elif field_type == "checkbox_grid":
+ columns = cfg.get("columns") or cfg.get("values") or []
+ grid = QHBoxLayout()
+ grid.setContentsMargins(0, 0, 0, 0)
+ grid.setSpacing(8)
+ for col_items in columns:
+ col_layout = QVBoxLayout()
+ col_layout.setContentsMargins(0, 0, 0, 0)
+ col_layout.setSpacing(2)
+ for text in col_items or []:
+ cb = QCheckBox(str(text))
+ col_layout.addWidget(cb)
+ grid.addLayout(col_layout)
+ if cfg.get("add_stretch", True):
+ grid.addStretch()
+ layout.addLayout(grid)
+ elif field_type == "checkbox_row":
+ row = QHBoxLayout()
+ row.setContentsMargins(0, 0, 0, 0)
+ row.setSpacing(12)
+ label = cfg.get("label")
+ if label:
+ lbl = QLabel(label)
+ lbl.setStyleSheet("font-size: 10px; color: #333; font-weight: normal; margin-top: 4px;")
+ row.addWidget(lbl)
+ options = cfg.get("options") or cfg.get("values") or []
+ for text in options:
+ cb = QCheckBox(str(text))
+ row.addWidget(cb)
+ if cfg.get("add_stretch", True):
+ row.addStretch()
+ layout.addLayout(row)
+ elif field_type == "checkbox":
+ cb = QCheckBox(cfg.get("label", ""))
+ layout.addWidget(cb)
+
+ def _section_label_style(self):
+ return (
+ "QLabel {\n"
+ " color: #000000;\n"
+ " font-size: 12px;\n"
+ " background: transparent;\n"
+ "}"
+ )
+
+ def _default_action_button_style(self):
+ return (
+ "QPushButton {\n"
+ " background-color: #90AF13;\n"
+ " color: white;\n"
+ " font-weight: bold;\n"
+ " border: none;\n"
+ " border-radius: 4px;\n"
+ " padding: 8px 20px;\n"
+ " font-size: 11px;\n"
+ " min-width: 80px;\n"
+ "}\n"
+ "QPushButton:hover {\n"
+ " background-color: #7a9a12;\n"
+ "}\n"
+ )
+
+ def _create_action_button(self, cfg):
+ btn = QPushButton(cfg.get("text", "Action"))
+ btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ style = cfg.get("style") or self._default_action_button_style()
+ btn.setStyleSheet(style)
+ if cfg.get("icon"):
+ btn.setIcon(QIcon(cfg["icon"]))
+ icon_size = cfg.get("icon_size")
+ if isinstance(icon_size, (list, tuple)) and len(icon_size) == 2:
+ btn.setIconSize(QSize(icon_size[0], icon_size[1]))
+ cb_name = cfg.get("action")
+ cb = getattr(self, cb_name, None) if cb_name else None
+ if callable(cb):
+ btn.clicked.connect(cb)
+ else:
+ btn.setEnabled(False)
+ return btn
+
+ def _add_button_row(self, parent_layout, row_cfg):
+ row = QHBoxLayout()
+ row.setContentsMargins(0, 0, 0, 0)
+ row.setSpacing(8)
+
+ label_text = row_cfg.get("label")
+ if label_text:
+ label = QLabel(label_text)
+ label.setStyleSheet(self._section_label_style())
+ label.setMinimumWidth(row_cfg.get("label_min_width", 110))
+ row.addWidget(label)
+
+ buttons = row_cfg.get("buttons", [])
+ for cfg in buttons:
+ btn = self._create_action_button(cfg)
+ row.addWidget(btn, cfg.get("stretch", 1 if len(buttons) == 1 else 0))
+
+ if row_cfg.get("add_stretch", True):
+ row.addStretch()
+
+ parent_layout.addLayout(row)
+
+ def _create_toggle_group(self, section_cfg):
+ group = QGroupBox()
+ group.setStyleSheet(
+ "QGroupBox {\n"
+ " border: 1px solid #90AF13;\n"
+ " border-radius: 5px;\n"
+ " margin-top: 0px;\n"
+ " padding-top: 5px;\n"
+ " background-color: white;\n"
+ "}"
+ )
+ layout = QVBoxLayout()
+ layout.setContentsMargins(10, 10, 10, 10)
+ layout.setSpacing(10)
+
+ header = QHBoxLayout()
+ title = QLabel(section_cfg.get("title", ""))
+ title.setStyleSheet("font-size: 13px; font-weight: bold; color: #333;")
+ header.addWidget(title)
+ header.addStretch()
+
+ toggle_btn = QPushButton()
+ toggle_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ toggle_btn.setCheckable(True)
+ toggle_btn.setChecked(True)
+ toggle_btn.setIcon(QIcon(":/vectors/arrow_up_light.svg"))
+ toggle_btn.setIconSize(QSize(20, 20))
+ toggle_btn.setStyleSheet(
+ "QPushButton {\n"
+ " background: transparent;\n"
+ " border: none;\n"
+ " padding: 2px;\n"
+ "}\n"
+ "QPushButton:hover {\n"
+ " background: transparent;\n"
+ "}\n"
+ "QPushButton:pressed {\n"
+ " background: transparent;\n"
+ "}"
+ )
+ header.addWidget(toggle_btn)
+ layout.addLayout(header)
+
+ body = QFrame()
+ body.setFrameShape(QFrame.NoFrame)
+ body_layout = QVBoxLayout(body)
+ body_layout.setContentsMargins(0, 0, 0, 0)
+ body_layout.setSpacing(10)
+ body.setVisible(True)
+
+ for row_cfg in section_cfg.get("rows", []):
+ self._add_button_row(body_layout, row_cfg)
+
+ layout.addWidget(body)
+
+ def _toggle(checked):
+ body.setVisible(checked)
+ toggle_btn.setIcon(QIcon(":/vectors/arrow_up_light.svg" if checked else ":/vectors/arrow_down_light.svg"))
+
+ toggle_btn.toggled.connect(_toggle)
+ group.setLayout(layout)
+ return group
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/main_window.ui b/src/osdagbridge/desktop/ui/main_window.ui
deleted file mode 100644
index b52aea53..00000000
--- a/src/osdagbridge/desktop/ui/main_window.ui
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/src/osdagbridge/desktop/ui/template_page.py b/src/osdagbridge/desktop/ui/template_page.py
new file mode 100644
index 00000000..1cf942fc
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/template_page.py
@@ -0,0 +1,683 @@
+import sys
+from PySide6.QtWidgets import (
+ QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
+ QMenuBar, QSplitter, QSizePolicy, QPushButton, QScrollArea, QFrame,
+)
+from PySide6.QtSvgWidgets import QSvgWidget
+from PySide6.QtCore import Qt, QFile, QTextStream, Signal
+from PySide6.QtGui import QIcon, QAction, QKeySequence
+
+from osdagbridge.desktop.ui.docks.input_dock import InputDock
+from osdagbridge.desktop.ui.docks.output_dock import OutputDock
+from osdagbridge.desktop.ui.docks.log_dock import LogDock
+
+from osdagbridge.core.bridge_types.plate_girder.ui_fields import FrontendData
+from osdagbridge.core.utils.common import *
+
+class DummyCADWidget(QWidget):
+ """Placeholder for CAD widget"""
+
+ def __init__(self):
+ super().__init__()
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(5, 2, 5, 0)
+ layout.setSpacing(0)
+ label = QLabel("CAD Window\n(Placeholder)")
+ label.setAlignment(Qt.AlignCenter)
+ label.setStyleSheet(
+ """
+ QLabel {
+ background-color: #f0f0f0;
+ border: 1px solid #999;
+ padding: 40px;
+ font-size: 18px;
+ color: #666;
+ }
+ """
+ )
+ layout.addWidget(label)
+
+class CustomWindow(QWidget):
+ def __init__(self, title: str, backend: object, parent=None):
+ super().__init__()
+ self.parent = parent
+ self.backend = backend()
+
+ self.setWindowTitle(title)
+ self.setStyleSheet(
+ """
+ QWidget {
+ background-color: #ffffff;
+ margin: 0px;
+ padding: 0px;
+ }
+ QMenuBar {
+ background-color: #F4F4F4;
+ color: #000000;
+ padding: 0px;
+ }
+ QMenuBar::item {
+ padding: 5px 10px;
+ background: transparent;
+ }
+ QMenuBar::item:selected {
+ background: #FFFFFF;
+ }
+ """
+ )
+ self.input_dock = None
+ self.output_dock = None
+
+ self.init_ui()
+
+ def init_ui(self):
+ # Docking icons Parent class
+ class ClickableSvgWidget(QSvgWidget):
+ clicked = Signal() # Define a custom clicked signal
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setCursor(Qt.CursorShape.PointingHandCursor)
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.MouseButton.LeftButton:
+ self.clicked.emit() # Emit the clicked signal on left-click
+ super().mousePressEvent(event)
+
+ main_v_layout = QVBoxLayout(self)
+ main_v_layout.setContentsMargins(0, 0, 0, 0)
+ main_v_layout.setSpacing(0)
+
+ menu_h_layout = QHBoxLayout()
+ menu_h_layout.setContentsMargins(0, 0, 0, 0)
+ menu_h_layout.setSpacing(0)
+
+ self.menu_bar = QMenuBar(self)
+ self.menu_bar.setObjectName("template_page_menu_bar")
+ self.menu_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
+ self.menu_bar.setFixedHeight(28)
+ self.menu_bar.setContentsMargins(0, 0, 0, 0)
+ menu_h_layout.addWidget(self.menu_bar)
+
+ # Control buttons
+ control_btn_widget = QWidget()
+ control_btn_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+ control_btn_widget.setObjectName("control_btn_widget")
+ control_button_layout = QHBoxLayout(control_btn_widget)
+ control_button_layout.setSpacing(10)
+ control_button_layout.setContentsMargins(5,5,5,5)
+
+ self.input_dock_control = ClickableSvgWidget()
+ self.input_dock_control.setFixedSize(18, 18)
+ self.input_dock_control.load(":/vectors/input_dock_active_light.svg")
+ self.input_dock_control.clicked.connect(self.input_dock_toggle)
+ self.input_dock_active = True
+ control_button_layout.addWidget(self.input_dock_control)
+
+ self.log_dock_control = ClickableSvgWidget()
+ self.log_dock_control.load(":/vectors/logs_dock_inactive_light.svg")
+ self.log_dock_control.setFixedSize(18, 18)
+ self.log_dock_control.clicked.connect(self.logs_dock_toggle)
+ self.log_dock_active = False
+ control_button_layout.addWidget(self.log_dock_control)
+
+ self.output_dock_control = ClickableSvgWidget()
+ self.output_dock_control.load(":/vectors/output_dock_inactive_light.svg")
+ self.output_dock_control.setFixedSize(18, 18)
+ self.output_dock_control.clicked.connect(self.output_dock_toggle)
+ self.output_dock_active = False
+ control_button_layout.addWidget(self.output_dock_control)
+
+ menu_h_layout.addWidget(control_btn_widget)
+ main_v_layout.addLayout(menu_h_layout)
+ self.create_menu_bar_items()
+
+ self.body_widget = QWidget()
+ self.layout = QHBoxLayout(self.body_widget)
+ self.layout.setContentsMargins(0, 0, 0, 0)
+ self.layout.setSpacing(0)
+
+ self.splitter = QSplitter(Qt.Horizontal, self.body_widget)
+ self.splitter.setHandleWidth(2)
+ self.input_dock = InputDock(backend=self.backend, parent=self)
+ input_dock_width = self.input_dock.sizeHint().width()
+ self._input_dock_default_width = input_dock_width
+ self.splitter.addWidget(self.input_dock)
+
+ central_widget = QWidget()
+ central_H_layout = QHBoxLayout(central_widget)
+
+ # Add dock indicator labels
+ self.input_dock_label = InputDockIndicator(parent=self)
+ self.input_dock_label.setVisible(False)
+ central_H_layout.setContentsMargins(0, 0, 0, 0)
+ central_H_layout.setSpacing(0)
+ central_H_layout.addWidget(self.input_dock_label, 1)
+
+ central_V_layout = QVBoxLayout()
+ central_V_layout.setContentsMargins(0, 0, 0, 0)
+ central_V_layout.setSpacing(0)
+
+ # Add cad component checkboxes
+ self.cad_comp_widget = DummyCADWidget()
+ central_V_layout.addWidget(self.cad_comp_widget)
+
+ self.cad_log_splitter = QSplitter(Qt.Vertical)
+ self.cad_log_splitter.setHandleWidth(2)
+ # Add Cad Model Widget
+ self.cad_log_splitter.addWidget(self.cad_comp_widget)
+
+ self.logs_dock = LogDock(parent=self)
+ self.logs_dock.setVisible(False)
+ # log text
+ self.textEdit = self.logs_dock.log_display
+ self.cad_log_splitter.addWidget(self.logs_dock)
+
+ # Prefer stretch factors so ratio persists on resize
+ self.cad_log_splitter.setStretchFactor(0, 8)
+ self.cad_log_splitter.setStretchFactor(1, 1)
+ # Seed an initial 8:1 split; will be refined after first show
+ self.cad_log_splitter.setSizes([8, 1])
+
+ central_V_layout.addWidget(self.cad_log_splitter)
+ central_H_layout.addLayout(central_V_layout, 6)
+
+ # Add output dock indicator label
+ self.output_dock_label = OutputDockIndicator(parent=self)
+ self.output_dock_label.setVisible(True)
+ central_H_layout.addWidget(self.output_dock_label, 1)
+ self.splitter.addWidget(central_widget)
+
+ # root is the greatest level of parent that is the MainWindow
+ self.output_dock = OutputDock(backend=self.backend, parent=self)
+ self.splitter.addWidget(self.output_dock)
+ # self.output_dock.setStyleSheet(self.output_dock.styleSheet())
+ self.output_dock.hide()
+
+ self.layout.addWidget(self.splitter)
+
+ total_width = self.width() - self.splitter.contentsMargins().left() - self.splitter.contentsMargins().right()
+ target_sizes = [0] * self.splitter.count()
+ target_sizes[0] = input_dock_width
+ target_sizes[2] = 0
+ remaining_width = total_width - input_dock_width
+ target_sizes[1] = max(0, remaining_width)
+ self.splitter.setSizes(target_sizes)
+ self.layout.activate()
+ main_v_layout.addWidget(self.body_widget)
+
+ #---------------------------------Docking-Icons-Functionality-START----------------------------------------------
+
+ def input_dock_toggle(self):
+ self.input_dock.toggle_input_dock()
+
+ def output_dock_toggle(self):
+ self.output_dock.toggle_output_dock()
+
+ def logs_dock_toggle(self):
+ self.log_dock_active = not self.log_dock_active
+ self.logs_dock.setVisible(self.log_dock_active)
+ if self.log_dock_active:
+ self.log_dock_control.load(":/vectors/logs_dock_active_light.svg")
+ else:
+ self.log_dock_control.load(":/vectors/logs_dock_inactive_light.svg")
+
+ def update_docking_icons(self, input_is_active=None, log_is_active=None, output_is_active=None):
+
+ if(input_is_active is not None):
+ self.input_dock_active = input_is_active
+ # Update and save control state
+ self.input_dock_active = input_is_active
+ if self.input_dock_active:
+ self.input_dock_control.load(":/vectors/input_dock_active_light.svg")
+ else:
+ self.input_dock_control.load(":/vectors/input_dock_inactive_light.svg")
+
+ # Update output dock icon
+ if(output_is_active is not None):
+ # Update and save control state
+ self.output_dock_active = output_is_active
+ if self.output_dock_active:
+ self.output_dock_control.load(":/vectors/output_dock_active_light.svg")
+ else:
+ self.output_dock_control.load(":/vectors/output_dock_inactive_light.svg")
+
+ # Update log dock icon
+ if(log_is_active is not None):
+ self.log_dock_active = log_is_active
+ # Update and save control state
+ self.logs_dock_active = log_is_active
+ if self.log_dock_active:
+ self.log_dock_control.load(":/vectors/logs_dock_active_light.svg")
+ else:
+ self.log_dock_control.load(":/vectors/logs_dock_inactive_light.svg")
+
+ def toggle_animate(self, show: bool, dock: str = 'output', on_finished=None):
+ sizes = self.splitter.sizes()
+ n = self.splitter.count()
+ if dock == 'input':
+ dock_index = 0
+
+ elif dock == 'output':
+ dock_index = n - 1
+ elif dock == 'log':
+ self.logs_dock.setVisible(show)
+ if on_finished:
+ on_finished()
+ return
+ else:
+ print(f"[Error] Invalid dock: {dock}")
+ return
+
+ dock_widget = self.splitter.widget(dock_index)
+ if show:
+ dock_widget.show()
+
+ self.splitter.setMinimumWidth(0)
+ self.splitter.setCollapsible(dock_index, True)
+ for i in range(n):
+ self.splitter.widget(i).setMinimumWidth(0)
+ self.splitter.widget(i).setMaximumWidth(16777215)
+
+ target_sizes = sizes[:]
+ total_width = self.width() - self.splitter.contentsMargins().left() - self.splitter.contentsMargins().right()
+ input_dock = self.splitter.widget(0)
+ output_dock = self.splitter.widget(n - 1)
+
+ if dock == 'input':
+ if show:
+ target_sizes[0] = input_dock.sizeHint().width()
+ self.input_dock_label.setVisible(False)
+ else:
+ target_sizes[0] = 0
+ self.input_dock_label.setVisible(True)
+ target_sizes[2] = sizes[2]
+ remaining_width = total_width - target_sizes[0] - target_sizes[2]
+ target_sizes[1] = max(0, remaining_width)
+ else:
+ if show:
+ target_sizes[2] = output_dock.sizeHint().width()
+ self.output_dock_label.setVisible(False)
+ else:
+ target_sizes[2] = 0
+ self.output_dock_label.setVisible(True)
+ target_sizes[0] = sizes[0]
+ remaining_width = total_width - target_sizes[0] - target_sizes[2]
+ target_sizes[1] = max(0, remaining_width)
+
+ if sizes == target_sizes:
+ if not show:
+ dock_widget.hide()
+ if on_finished:
+ on_finished()
+ return
+
+ def after_anim():
+ self.finalize_dock_toggle(show, dock_widget, target_sizes)
+ if on_finished:
+ on_finished()
+
+ # User requested "one step animation" with "no delay"
+ self.animate_splitter_sizes(
+ self.splitter,
+ sizes,
+ target_sizes,
+ duration=0,
+ on_finished=after_anim
+ )
+
+ def animate_splitter_sizes(self, splitter, start_sizes, end_sizes, duration, on_finished=None):
+ if duration <= 0:
+ # Instant update
+ splitter.setSizes(end_sizes)
+ splitter.refresh()
+ if splitter.parentWidget() and splitter.parentWidget().layout():
+ splitter.parentWidget().layout().activate()
+ splitter.update()
+ if splitter.parentWidget():
+ splitter.parentWidget().update()
+ self.update()
+ for i in range(splitter.count()):
+ widget = splitter.widget(i)
+ if widget:
+ widget.update()
+
+ if on_finished:
+ on_finished()
+ return
+
+ # Target 60 FPS -> ~16ms interval
+ interval = 16
+ steps = max(1, duration // interval)
+
+ current_step = 0
+
+ def ease_out_quad(t):
+ return t * (2 - t)
+
+ def update_step():
+ nonlocal current_step
+ if current_step <= steps:
+ progress = current_step / steps
+ # Apply easing
+ eased_progress = ease_out_quad(progress)
+
+ sizes = [
+ int(start + (end - start) * eased_progress)
+ for start, end in zip(start_sizes, end_sizes)
+ ]
+
+ splitter.setSizes(sizes)
+ splitter.refresh()
+ if splitter.parentWidget() and splitter.parentWidget().layout():
+ splitter.parentWidget().layout().activate()
+ splitter.update()
+ if splitter.parentWidget():
+ splitter.parentWidget().update()
+ self.update()
+ for i in range(splitter.count()):
+ widget = splitter.widget(i)
+ if widget:
+ widget.update()
+
+ current_step += 1
+ else:
+ timer.stop()
+ if on_finished:
+ on_finished()
+
+ timer = QTimer(self)
+ timer.timeout.connect(update_step)
+ timer.start(interval)
+ self._splitter_anim = timer
+
+ def finalize_dock_toggle(self, show, dock_widget, target_sizes):
+ self.splitter.setSizes(target_sizes)
+ if not show:
+ dock_widget.hide()
+ self.splitter.refresh()
+ self.splitter.parentWidget().layout().activate()
+ self.splitter.update()
+ self.splitter.parentWidget().update()
+ self.update()
+ for i in range(self.splitter.count()):
+ self.splitter.widget(i).update()
+
+ #---------------------------------Docking-Icons-Functionality-END----------------------------------------------
+
+ def resizeEvent(self, event):
+
+ """Override resizeEvent with safety check."""
+ # Check if being deleted
+ if not self.isVisible() or self.signalsBlocked():
+ return
+
+ # Check if splitter exists and has children
+ try:
+ if not hasattr(self, 'splitter') or self.splitter is None:
+ return
+ if self.splitter.count() < 3:
+ return
+
+ if self.input_dock.isVisible():
+ input_dock_width = self.input_dock.sizeHint().width()
+ else:
+ input_dock_width = 0
+
+ if self.output_dock.isVisible():
+ output_dock_width = self.output_dock.sizeHint().width()
+ else:
+ output_dock_width = 0
+ total_width = self.width() - self.splitter.contentsMargins().left() - self.splitter.contentsMargins().right()
+ self.splitter.setMinimumWidth(0)
+ self.splitter.setCollapsible(0, True)
+ self.splitter.setCollapsible(1, True)
+ self.splitter.setCollapsible(2, True)
+ for i in range(self.splitter.count()):
+ self.splitter.widget(i).setMinimumWidth(0)
+ self.splitter.widget(i).setMaximumWidth(16777215)
+ target_sizes = [0] * self.splitter.count()
+ target_sizes[0] = input_dock_width
+ target_sizes[2] = output_dock_width
+ remaining_width = total_width - input_dock_width - output_dock_width
+ target_sizes[1] = max(0, remaining_width)
+ self.splitter.setSizes(target_sizes)
+ self.splitter.refresh()
+ self.body_widget.layout().activate()
+ self.splitter.update()
+ super().resizeEvent(event)
+
+ except (IndexError, RuntimeError, AttributeError):
+ # Being deleted, ignore
+ return
+
+ def create_menu_bar_items(self):
+ # File Menus
+ file_menu = self.menu_bar.addMenu("File")
+
+ load_input_action = QAction("Load Input", self)
+ load_input_action.setShortcut(QKeySequence("Ctrl+L"))
+ file_menu.addAction(load_input_action)
+
+ file_menu.addSeparator()
+
+ save_input_action = QAction("Save Input", self)
+ save_input_action.setShortcut(QKeySequence("Ctrl+S"))
+ file_menu.addAction(save_input_action)
+
+ save_log_action = QAction("Save Log Messages", self)
+ save_log_action.setShortcut(QKeySequence("Alt+M"))
+ file_menu.addAction(save_log_action)
+
+ create_report_action = QAction("Create Design Report", self)
+ create_report_action.setShortcut(QKeySequence("Alt+C"))
+ file_menu.addAction(create_report_action)
+
+ file_menu.addSeparator()
+
+ save_3d_action = QAction("Save 3D Model", self)
+ save_3d_action.setShortcut(QKeySequence("Alt+3"))
+ file_menu.addAction(save_3d_action)
+
+ save_cad_action = QAction("Save CAD Image", self)
+ save_cad_action.setShortcut(QKeySequence("Alt+I"))
+ file_menu.addAction(save_cad_action)
+
+ file_menu.addSeparator()
+
+ quit_action = QAction("Quit", self)
+ quit_action.setShortcut(QKeySequence("Shift+Q"))
+ file_menu.addAction(quit_action)
+
+ # Edit Menus
+ edit_menu = self.menu_bar.addMenu("Edit")
+
+ design_prefs_action = QAction("Additional Inputs", self)
+ design_prefs_action.setShortcut(QKeySequence("Alt+P"))
+ edit_menu.addAction(design_prefs_action)
+
+ graphics_menu = self.menu_bar.addMenu("Graphics")
+ zoom_in_action = QAction("Zoom In", self)
+ zoom_in_action.setShortcut(QKeySequence("Ctrl+I"))
+ graphics_menu.addAction(zoom_in_action)
+
+ zoom_out_action = QAction("Zoom Out", self)
+ zoom_out_action.setShortcut(QKeySequence("Ctrl+O"))
+ graphics_menu.addAction(zoom_out_action)
+
+ pan_action = QAction("Pan", self)
+ pan_action.setShortcut(QKeySequence("Ctrl+P"))
+ graphics_menu.addAction(pan_action)
+
+ rotate_3d_action = QAction("Rotate 3D Model", self)
+ rotate_3d_action.setShortcut(QKeySequence("Ctrl+R"))
+ graphics_menu.addAction(rotate_3d_action)
+
+ graphics_menu.addSeparator()
+
+ front_view_action = QAction("Show Front View", self)
+ front_view_action.setShortcut(QKeySequence("Alt+Shift+F"))
+ graphics_menu.addAction(front_view_action)
+
+ top_view_action = QAction("Show Top View", self)
+ top_view_action.setShortcut(QKeySequence("Alt+Shift+T"))
+ graphics_menu.addAction(top_view_action)
+
+ side_view_action = QAction("Show Side View", self)
+ side_view_action.setShortcut(QKeySequence("Alt+Shift+S"))
+ graphics_menu.addAction(side_view_action)
+
+ # Database Menu
+ database_menu = self.menu_bar.addMenu("Database")
+
+ input_csv_action = QAction("Save Inputs (.csv)", self)
+ database_menu.addAction(input_csv_action)
+
+ output_csv_action = QAction("Save Outputs (.csv)", self)
+ database_menu.addAction(output_csv_action)
+
+ input_osi_action = QAction("Save Inputs (.osi)", self)
+ database_menu.addAction(input_osi_action)
+
+ download_database_menu = database_menu.addMenu("Download Database")
+
+ download_column_action = QAction("Column", self)
+ download_database_menu.addAction(download_column_action)
+
+ download_bolt_action = QAction("Beam", self)
+ download_database_menu.addAction(download_bolt_action)
+
+ download_weld_action = QAction("Channel", self)
+ download_database_menu.addAction(download_weld_action)
+
+ download_angle_action = QAction("Angle", self)
+ download_database_menu.addAction(download_angle_action)
+
+ database_menu.addSeparator()
+
+ reset_action = QAction("Reset", self)
+ reset_action.setShortcut(QKeySequence("Alt+R"))
+ database_menu.addAction(reset_action)
+
+ # Help Menu
+ help_menu = self.menu_bar.addMenu("Help")
+
+ video_tutorials_action = QAction("Video Tutorials", self)
+ help_menu.addAction(video_tutorials_action)
+
+ design_examples_action = QAction("Design Examples", self)
+ help_menu.addAction(design_examples_action)
+
+ help_menu.addSeparator()
+
+ ask_question_action = QAction("Ask Us a Question", self)
+ help_menu.addAction(ask_question_action)
+
+ about_osdag_action = QAction("About Osdag", self)
+ help_menu.addAction(about_osdag_action)
+
+ help_menu.addSeparator()
+
+ check_update_action = QAction("Check For Update", self)
+ help_menu.addAction(check_update_action)
+
+
+class InputDockIndicator(QWidget):
+ def __init__(self, parent):
+ super().__init__(parent)
+ # Ensures automatic deletion when closed
+ self.setAttribute(Qt.WA_DeleteOnClose, True)
+ self.parent = parent
+ self.setObjectName("input_dock_indicator")
+ self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) # Fixed width, expanding height
+
+ input_layout = QHBoxLayout(self)
+ input_layout.setContentsMargins(6,0,0,0)
+ input_layout.setSpacing(0)
+
+ self.input_label = QSvgWidget(":/vectors/inputs_label_light.svg")
+ input_layout.addWidget(self.input_label)
+ self.input_label.setFixedWidth(32)
+
+ self.toggle_strip = QWidget()
+ self.toggle_strip.setObjectName("toggle_strip")
+ self.toggle_strip.setFixedWidth(6) # Always visible
+ self.toggle_strip.setStyleSheet("background-color: #90AF13;")
+ toggle_layout = QVBoxLayout(self.toggle_strip)
+ toggle_layout.setContentsMargins(0, 0, 0, 0)
+ toggle_layout.setSpacing(0)
+ toggle_layout.setAlignment(Qt.AlignVCenter | Qt.AlignRight) # Align to right for input dock
+
+ self.toggle_btn = QPushButton("❯") # Right-pointing chevron for input dock
+ self.toggle_btn.setFixedSize(6, 60)
+ self.toggle_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ self.toggle_btn.clicked.connect(self.parent.input_dock_toggle)
+ self.toggle_btn.setToolTip("Show input panel")
+ self.toggle_btn.setObjectName("toggle_strip_button")
+ self.toggle_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #6c8408;
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+ padding: 0px;
+ border: none;
+ }
+ QPushButton:hover {
+ background-color: #5e7407;
+ }
+ """)
+ toggle_layout.addStretch()
+ toggle_layout.addWidget(self.toggle_btn)
+ toggle_layout.addStretch()
+ input_layout.addWidget(self.toggle_strip)
+
+class OutputDockIndicator(QWidget):
+ def __init__(self, parent):
+ super().__init__(parent)
+ # Ensures automatic deletion when closed
+ self.setAttribute(Qt.WA_DeleteOnClose, True)
+ self.parent = parent
+ self.setObjectName("output_dock_indicator")
+ self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) # Fixed width, expanding height
+
+ output_layout = QHBoxLayout(self)
+ output_layout.setContentsMargins(0,0,0,0)
+ output_layout.setSpacing(0)
+
+ self.toggle_strip = QWidget()
+ self.toggle_strip.setFixedWidth(6) # Always visible
+ self.toggle_strip.setObjectName("toggle_strip")
+ self.toggle_strip.setStyleSheet("background-color: #90AF13;")
+ toggle_layout = QVBoxLayout(self.toggle_strip)
+ toggle_layout.setContentsMargins(0, 0, 0, 0)
+ toggle_layout.setSpacing(0)
+ toggle_layout.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
+
+ self.toggle_btn = QPushButton("❮") # Show state initially
+ self.toggle_btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ self.toggle_btn.setFixedSize(6, 60)
+ self.toggle_btn.clicked.connect(self.parent.output_dock_toggle)
+ self.toggle_btn.setToolTip("Show panel")
+ self.toggle_btn.setObjectName("toggle_strip_button")
+ self.toggle_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #6c8408;
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+ padding: 0px;
+ border: none;
+ }
+ QPushButton:hover {
+ background-color: #5e7407;
+ }
+ """)
+ toggle_layout.addStretch()
+ toggle_layout.addWidget(self.toggle_btn)
+ toggle_layout.addStretch()
+ output_layout.addWidget(self.toggle_strip)
+
+ self.output_label = QSvgWidget(":/vectors/outputs_label_light.svg")
+ output_layout.addWidget(self.output_label)
+ self.output_label.setFixedWidth(28)
+
diff --git a/src/osdagbridge/desktop/ui/utils/custom_buttons.py b/src/osdagbridge/desktop/ui/utils/custom_buttons.py
new file mode 100644
index 00000000..6588616c
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/utils/custom_buttons.py
@@ -0,0 +1,71 @@
+"""
+Custom button widgets for Osdag GUI.
+Includes menu and action buttons with custom styles.
+"""
+from PySide6.QtWidgets import (
+ QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QApplication, QGridLayout,
+ QLabel, QMainWindow, QSizePolicy, QFrame
+)
+from PySide6.QtSvgWidgets import QSvgWidget
+from PySide6.QtCore import Qt, Signal, QSize, QEvent, QRect, QPropertyAnimation, QEasingCurve
+from PySide6.QtGui import QFont, QIcon, QPainter
+
+class DockCustomButton(QPushButton):
+ def __init__(self, text: str, icon_path: str, parent=None):
+ super().__init__(parent)
+ self.setCursor(Qt.PointingHandCursor)
+ self.setObjectName("dock_custom_button")
+ self.setStyleSheet("""
+ QPushButton {
+ background-color: #90AF13;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 10px;
+ font-size: 11px;
+ font-weight: bold;
+ }
+ QPushButton:hover {
+ background-color: #7a9a12;
+ }
+ """)
+
+ # Layout for icons and text
+ layout = QHBoxLayout(self)
+ layout.setContentsMargins(10, 0, 10, 0)
+ layout.setSpacing(0)
+
+ # Left icon
+ left_icon = QSvgWidget()
+ left_icon.load(icon_path)
+ left_icon.setFixedSize(18, 18)
+ left_icon.setObjectName("button_icon")
+ left_icon.setStyleSheet("""
+ QSvgWidget {
+ background: transparent;
+ }
+ """)
+ layout.addWidget(left_icon)
+
+ # Center text
+ text_label = QLabel(text)
+ text_label.setAlignment(Qt.AlignCenter)
+ text_label.setObjectName("button_label")
+ text_label.setStyleSheet("""
+ QLabel {
+ background: transparent;
+ color: white;
+ }
+ """)
+ layout.addWidget(text_label)
+
+ layout.setAlignment(Qt.AlignVCenter)
+ self.setLayout(layout)
+
+ # Calculate minimum width to prevent overlap
+ text_width = text_label.sizeHint().width()
+ icon_width = 18
+ margins = layout.contentsMargins().left() + layout.contentsMargins().right()
+ padding = 20
+ min_width = text_width + icon_width + margins + padding
+ self.setMinimumWidth(min_width)
\ No newline at end of file
diff --git a/src/osdagbridge/desktop/ui/utils/custom_titlebar.py b/src/osdagbridge/desktop/ui/utils/custom_titlebar.py
new file mode 100644
index 00000000..be46a2e4
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/utils/custom_titlebar.py
@@ -0,0 +1,168 @@
+from PySide6.QtWidgets import QWidget, QLabel, QToolButton, QHBoxLayout, QSizePolicy, QVBoxLayout
+from PySide6.QtCore import Qt, QPoint, QEvent
+from PySide6.QtGui import QMouseEvent, QFont
+
+class CustomTitleBar(QWidget):
+ def __init__(self, max_res_btn: bool = False, min_res_btn:bool = False, parent=None):
+ super().__init__(parent)
+ # Ensures automatic deletion when closed
+ self.setAttribute(Qt.WA_DeleteOnClose, True)
+ self._drag_pos = QPoint()
+ self.setObjectName("CustomTitleBar")
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ self.setFixedHeight(32) # Set consistent height
+ self.setAttribute(Qt.WA_StyledBackground, True)
+
+ # Add Osdag logo icon to the title bar
+ from PySide6.QtSvgWidgets import QSvgWidget
+ self.logo_label = QSvgWidget(":/vectors/Osdag_logo.svg", self)
+ self.logo_label.setObjectName("LogoLabel")
+ self.logo_label.setFixedSize(20, 20)
+
+ # Title label
+ self.title_label = QLabel("Osdag", self)
+ self.title_label.setObjectName("TitleLabel")
+ self.title_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
+
+ # Set font for title
+ title_font = QFont()
+ title_font.setPointSize(9)
+ title_font.setWeight(QFont.Weight.Medium)
+ self.title_label.setFont(title_font)
+
+ # Minimize button (optional)
+ self.btn_minimize = None
+ if min_res_btn:
+ self.btn_minimize = QToolButton(self)
+ self.btn_minimize.setObjectName("MinimizeButton")
+ self.btn_minimize.setToolTip("Minimize")
+ self.btn_minimize.setText("–")
+ self.btn_minimize.setFixedSize(46, 32)
+ self.btn_minimize.clicked.connect(self._minimize_parent)
+
+ # Maximize/Restore button (optional)
+ self.btn_max_restore = None
+ if max_res_btn:
+ self.btn_max_restore = QToolButton(self)
+ self.btn_max_restore.setObjectName("MaxRestoreButton")
+ self.btn_max_restore.setToolTip("Maximize")
+ self.btn_max_restore.setText("□")
+ self.btn_max_restore.setFixedSize(46, 32)
+ self.btn_max_restore.clicked.connect(self._toggle_max_restore)
+
+ # Close button
+ self.btn_close = QToolButton(self)
+ self.btn_close.setObjectName("CloseButton")
+ self.btn_close.setToolTip("Close")
+ self.btn_close.setText("✕") # Better multiplication symbol
+ self.btn_close.setFixedSize(46, 32)
+ self.btn_close.clicked.connect(self._close_parent)
+
+ # Title bar layout
+ outer_layout = QVBoxLayout(self)
+ outer_layout.setContentsMargins(0, 0, 0, 0)
+ outer_layout.setSpacing(0)
+
+ row_widget = QWidget(self)
+ row_layout = QHBoxLayout(row_widget)
+ row_layout.setContentsMargins(8, 0, 0, 0)
+ row_layout.setSpacing(8)
+ row_layout.addWidget(self.logo_label, 0)
+ row_layout.addWidget(self.title_label, 1)
+ if self.btn_minimize is not None:
+ row_layout.addWidget(self.btn_minimize, 0)
+ if self.btn_max_restore is not None:
+ row_layout.addWidget(self.btn_max_restore, 0)
+ row_layout.addWidget(self.btn_close, 0)
+ outer_layout.addWidget(row_widget)
+
+ self.bottom_line = QWidget(self)
+ self.bottom_line.setObjectName("BottomLine")
+ self.bottom_line.setFixedHeight(1)
+ outer_layout.addWidget(self.bottom_line)
+
+ # Keep the maximize/restore button state in sync with the window state
+ if self.btn_max_restore is not None and self.parent() is not None:
+ self.parent().installEventFilter(self)
+ self._update_max_restore_icon()
+
+ def setTitle(self, title):
+ """Set the title displayed in the title bar."""
+ self.title_label.setText(title)
+
+ def _close_parent(self):
+ """Close the parent widget."""
+ if self.parent():
+ self.parent().close()
+
+ def _minimize_parent(self):
+ """Minimize the parent widget."""
+ if self.parent():
+ self.parent().showMinimized()
+
+ def _toggle_max_restore(self):
+ """Toggle between maximizing and restoring the parent window."""
+ window = self.parent()
+ if not window:
+ return
+ if window.isMaximized():
+ window.showNormal()
+ else:
+ window.showMaximized()
+ self._update_max_restore_icon()
+
+ def _update_max_restore_icon(self):
+ """Update the icon/text and tooltip of the maximize/restore button based on window state."""
+ if self.btn_max_restore is None:
+ return
+ window = self.parent()
+ if not window:
+ return
+ if window.isMaximized():
+ self.btn_max_restore.setText("❐") # Restore
+ self.btn_max_restore.setToolTip("Restore")
+ else:
+ self.btn_max_restore.setText("□") # Maximize
+ self.btn_max_restore.setToolTip("Maximize")
+
+ def eventFilter(self, obj, event):
+ # Update button when window state changes (e.g., via system controls or double-click)
+ if obj is self.parent() and event.type() == QEvent.WindowStateChange:
+ self._update_max_restore_icon()
+ return super().eventFilter(obj, event)
+
+ def mousePressEvent(self, event: QMouseEvent):
+ """Handle mouse press for dragging."""
+ if event.button() == Qt.LeftButton:
+ if self.parent() and self.parent().isWindow():
+ self._drag_pos = event.globalPosition().toPoint() - self.parent().frameGeometry().topLeft()
+ event.accept()
+
+ def mouseMoveEvent(self, event: QMouseEvent):
+ """Handle mouse move for dragging."""
+ if (event.buttons() & Qt.LeftButton and
+ not self._drag_pos.isNull() and
+ self.parent() and
+ self.parent().isWindow()):
+ self.parent().move(event.globalPosition().toPoint() - self._drag_pos)
+ event.accept()
+
+ def mouseReleaseEvent(self, event: QMouseEvent):
+ """Reset drag position on mouse release."""
+ if event.button() == Qt.LeftButton:
+ self._drag_pos = QPoint()
+ event.accept()
+
+ def mouseDoubleClickEvent(self, event: QMouseEvent):
+ """Handle double-click to maximize/restore window."""
+ if event.button() == Qt.LeftButton and self.parent() and self.parent().isWindow():
+ if self.parent().isMaximized():
+ self.parent().showNormal()
+ else:
+ self.parent().showMaximized()
+ # Keep button state in sync
+ if self.btn_max_restore is not None:
+ self._update_max_restore_icon()
+ event.accept()
+ else:
+ super().mouseDoubleClickEvent(event)
diff --git a/src/osdagbridge/desktop/ui/utils/rolled_section_preview.py b/src/osdagbridge/desktop/ui/utils/rolled_section_preview.py
new file mode 100644
index 00000000..b580abaa
--- /dev/null
+++ b/src/osdagbridge/desktop/ui/utils/rolled_section_preview.py
@@ -0,0 +1,756 @@
+"""Interactive beam preview widget with CAD-style annotations."""
+
+from __future__ import annotations
+
+import math
+from typing import Dict, Optional
+
+from PySide6.QtCore import QPointF, QRectF, Qt
+from PySide6.QtGui import QColor, QFont, QPainter, QPaintEvent, QPainterPath, QPen, QTextDocument
+from PySide6.QtWidgets import QSizePolicy, QWidget
+
+from osdagbridge.core.bridge_components.super_structure.girder.properties import BeamSection
+
+
+OSDAG_BRAND_GREEN = QColor("#90AF13")
+OSDAG_FONT_FAMILY = "Ubuntu Sans"
+
+
+class RolledSectionPreview(QWidget):
+ """Render a rolled or welded section with CAD-style dimension annotations."""
+
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
+ super().__init__(parent)
+ self._section: Optional[BeamSection] = None
+ self._dimensions: Dict[str, float] = {}
+
+ self._outline_color = QColor("#1b1b1b")
+ self._outline_width = 3.0
+ self._brand_color = QColor(OSDAG_BRAND_GREEN)
+ self._dimension_color = QColor(OSDAG_BRAND_GREEN)
+ self._dimension_keys = ("tfw", "tft", "bfw", "bft", "d", "wt")
+ self._dimension_palette = {key: QColor(OSDAG_BRAND_GREEN) for key in self._dimension_keys}
+ self._label_bg = QColor(255, 255, 255, 230)
+ self._text_color = QColor("#0f0f0f")
+ self._brand_font_family = OSDAG_FONT_FAMILY
+ self._show_welds = False
+
+ self._outer_margin = 16
+ self._annotation_margin_top = 36
+ self._annotation_margin_bottom = 28
+ self._annotation_margin_left = 52
+ self._annotation_margin_right = 74
+ self._dim_gap = 12
+ self._arrow_size = 9
+
+ # Minimum radii keep rolled sections visibly curved even when the
+ # catalogue omits R1/R2.
+ self._min_root_radius_px = 8.0
+ self._min_toe_radius_px = 4.0
+
+ self.setMinimumSize(360, 260)
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+
+ # ------------------------------------------------------------------
+ # Public API
+ # ------------------------------------------------------------------
+ def set_section(self, section: Optional[BeamSection]) -> None:
+ """Update the preview to show the supplied ``BeamSection`` (assumed rolled)."""
+
+ self._section = section
+ if section is None:
+ self._dimensions = {}
+ else:
+ self._dimensions = {
+ "depth": float(section.depth_mm),
+ "top_flange_width": float(section.flange_width_mm),
+ "bottom_flange_width": float(section.flange_width_mm),
+ "web_thickness": float(section.web_thickness_mm),
+ "top_flange_thickness": float(section.flange_thickness_mm),
+ "bottom_flange_thickness": float(section.flange_thickness_mm),
+ "root_radius_mm": float(section.root_radius_r1_mm or 0.0),
+ "toe_radius_mm": float(section.root_radius_r2_mm or 0.0),
+ }
+ # Rolled sections do not show weld symbols.
+ self._show_welds = False
+ self.update()
+
+ def set_dimensions(
+ self,
+ *,
+ depth_mm: float,
+ flange_width_mm: float,
+ web_thickness_mm: float,
+ flange_thickness_mm: float,
+ bottom_flange_width_mm: Optional[float] = None,
+ bottom_flange_thickness_mm: Optional[float] = None,
+ show_welds: bool = False,
+ ) -> None:
+ """Feed custom dimensions directly (e.g., for welded sections)."""
+
+ self._section = None
+ self._dimensions = {
+ "depth": float(depth_mm),
+ "top_flange_width": float(flange_width_mm),
+ "bottom_flange_width": float(bottom_flange_width_mm or flange_width_mm),
+ "web_thickness": float(web_thickness_mm),
+ "top_flange_thickness": float(flange_thickness_mm),
+ "bottom_flange_thickness": float(bottom_flange_thickness_mm or flange_thickness_mm),
+ # Welded sections have no root/toe radii.
+ "root_radius_mm": 0.0,
+ "toe_radius_mm": 0.0,
+ }
+ self._show_welds = show_welds
+ self.update()
+
+ def clear(self) -> None:
+ """Reset the preview to an empty placeholder."""
+
+ self._section = None
+ self._dimensions = {}
+ self._show_welds = False
+ self.update()
+
+ # ------------------------------------------------------------------
+ # QWidget overrides
+ # ------------------------------------------------------------------
+ def paintEvent(self, event: QPaintEvent) -> None: # noqa: D401 - Qt override
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.Antialiasing, True)
+ painter.fillRect(self.rect(), self.palette().window())
+
+ if not self._dimensions:
+ self._draw_placeholder(painter)
+ return
+
+ dims = self._dimensions
+ depth = max(dims.get("depth", 0.0), 1.0)
+ top_width = max(dims.get("top_flange_width", 0.0), 1.0)
+ bottom_width = max(dims.get("bottom_flange_width", top_width), 1.0)
+ web_thickness = max(dims.get("web_thickness", 0.0), 0.5)
+ top_thickness = max(dims.get("top_flange_thickness", 0.0), 0.5)
+ bottom_thickness = max(dims.get("bottom_flange_thickness", top_thickness), 0.5)
+
+ usable_rect = self.rect().adjusted(
+ self._outer_margin + 20, # extra left space for labels
+ self._outer_margin,
+ -self._outer_margin,
+ -self._outer_margin,
+ )
+ beam_rect = QRectF(
+ usable_rect.left() + self._annotation_margin_left,
+ usable_rect.top() + self._annotation_margin_top,
+ max(20.0, usable_rect.width() - self._annotation_margin_left - self._annotation_margin_right),
+ max(20.0, usable_rect.height() - self._annotation_margin_top - self._annotation_margin_bottom),
+ )
+
+ max_width = max(top_width, bottom_width)
+ scale_w = beam_rect.width() / max_width
+ scale_h = beam_rect.height() / depth
+ scale = min(scale_w, scale_h) * 0.92
+
+ beam_height = depth * scale
+ top_height = min(top_thickness * scale, beam_height * 0.4)
+ bottom_height = min(bottom_thickness * scale, beam_height * 0.4)
+ web_height = max(beam_height - top_height - bottom_height, scale * 2.0)
+
+ center_x = beam_rect.center().x()
+ top_y = beam_rect.center().y() - (top_height + web_height + bottom_height) / 2.0
+
+ top_flange = QRectF(
+ center_x - (top_width * scale) / 2.0,
+ top_y,
+ top_width * scale,
+ top_height,
+ )
+ web = QRectF(
+ center_x - (web_thickness * scale) / 2.0,
+ top_flange.bottom(),
+ max(1.0, web_thickness * scale),
+ web_height,
+ )
+ bottom_flange = QRectF(
+ center_x - (bottom_width * scale) / 2.0,
+ web.bottom(),
+ bottom_width * scale,
+ bottom_height,
+ )
+
+ root_radius_px = float(dims.get("root_radius_mm", 0.0)) * scale
+ toe_radius_px = float(dims.get("toe_radius_mm", 0.0)) * scale
+
+ section_path = self._build_section_path(
+ top_flange,
+ web,
+ bottom_flange,
+ root_radius_px,
+ toe_radius_px,
+ )
+
+ painter.save()
+ outline_pen = QPen(self._outline_color, self._outline_width)
+ outline_pen.setJoinStyle(Qt.MiterJoin)
+ painter.setPen(outline_pen)
+ painter.setBrush(QColor("#fefefe"))
+ if section_path is not None:
+ painter.drawPath(section_path)
+ else:
+ painter.drawRect(top_flange)
+ painter.drawRect(web)
+ painter.drawRect(bottom_flange)
+ painter.restore()
+
+ if self._show_welds:
+ self._draw_welds(painter, top_flange, web, bottom_flange)
+
+ font = QFont(self.font())
+ font.setFamily(self._brand_font_family)
+ font.setPointSizeF(max(9.0, font.pointSizeF()))
+ painter.setFont(font)
+
+ # --- Top flange width dimension ---
+ tfw_color = self._set_dimension_pen(painter, "tfw")
+ width_dim_y = self._snap_coordinate(top_flange.top() - self._dim_gap)
+ left_extension_end = QPointF(self._snap_coordinate(top_flange.left()), width_dim_y)
+ right_extension_end = QPointF(self._snap_coordinate(top_flange.right()), width_dim_y)
+ painter.drawLine(QPointF(left_extension_end.x(), top_flange.top()), left_extension_end)
+ painter.drawLine(QPointF(right_extension_end.x(), top_flange.top()), right_extension_end)
+ self._draw_dimension_line(
+ painter,
+ left_extension_end,
+ right_extension_end,
+ tfw_color,
+ )
+ self._draw_label(
+ painter,
+ self._format_label_markup("tfw", top_width),
+ QPointF(top_flange.center().x(), width_dim_y - 6),
+ Qt.AlignHCenter | Qt.AlignBottom,
+ with_background=False,
+ color=tfw_color,
+ )
+
+ # --- Top flange thickness dimension ---
+ tft_color = self._set_dimension_pen(painter, "tft")
+ self._draw_vertical_thickness_dimension(
+ painter,
+ top_flange.left(),
+ top_flange.top(),
+ top_flange.bottom(),
+ top_thickness,
+ tft_color,
+ label_symbol="tft",
+ label_align=Qt.AlignRight | Qt.AlignVCenter,
+ )
+
+ # --- Bottom flange width dimension ---
+ bfw_color = self._set_dimension_pen(painter, "bfw")
+ bottom_width_dim_y = self._snap_coordinate(bottom_flange.bottom() + self._dim_gap)
+ bottom_left_extension = QPointF(self._snap_coordinate(bottom_flange.left()), bottom_width_dim_y)
+ bottom_right_extension = QPointF(self._snap_coordinate(bottom_flange.right()), bottom_width_dim_y)
+ painter.drawLine(QPointF(bottom_left_extension.x(), bottom_flange.bottom()), bottom_left_extension)
+ painter.drawLine(QPointF(bottom_right_extension.x(), bottom_flange.bottom()), bottom_right_extension)
+ self._draw_dimension_line(
+ painter,
+ bottom_left_extension,
+ bottom_right_extension,
+ bfw_color,
+ )
+ self._draw_label(
+ painter,
+ self._format_label_markup("bfw", bottom_width),
+ QPointF(bottom_flange.center().x(), bottom_width_dim_y + 6),
+ Qt.AlignHCenter | Qt.AlignTop,
+ with_background=False,
+ color=bfw_color,
+ )
+
+ # --- Bottom flange thickness dimension ---
+ bft_color = self._set_dimension_pen(painter, "bft")
+ self._draw_vertical_thickness_dimension(
+ painter,
+ bottom_flange.left(),
+ bottom_flange.top(),
+ bottom_flange.bottom(),
+ bottom_thickness,
+ bft_color,
+ label_symbol="bft",
+ label_align=Qt.AlignRight | Qt.AlignVCenter,
+ )
+
+ # --- Overall depth dimension ---
+ depth_color = self._set_dimension_pen(painter, "d")
+ anchor_x = self._snap_coordinate(bottom_flange.right())
+ depth_dim_x = self._snap_coordinate(anchor_x + self._dim_gap)
+ top_depth_extension = QPointF(depth_dim_x, self._snap_coordinate(top_flange.top()))
+ bottom_depth_extension = QPointF(depth_dim_x, self._snap_coordinate(bottom_flange.bottom()))
+ painter.drawLine(QPointF(anchor_x, top_depth_extension.y()), top_depth_extension)
+ painter.drawLine(QPointF(anchor_x, bottom_depth_extension.y()), bottom_depth_extension)
+ self._draw_dimension_line(
+ painter,
+ top_depth_extension,
+ bottom_depth_extension,
+ depth_color,
+ )
+ self._draw_label(
+ painter,
+ self._format_label_markup("d", depth),
+ QPointF(depth_dim_x + 10, (top_flange.top() + bottom_flange.bottom()) / 2.0),
+ Qt.AlignLeft | Qt.AlignVCenter,
+ with_background=False,
+ color=depth_color,
+ )
+
+ # --- Web thickness dimension ---
+ wt_color = self._set_dimension_pen(painter, "wt")
+ self._draw_web_thickness_dimension(
+ painter,
+ web.left(),
+ web.right(),
+ web.center().y(),
+ web_thickness,
+ wt_color,
+ label_symbol="wt",
+ )
+
+
+ # ------------------------------------------------------------------
+ # Drawing helpers
+ # ------------------------------------------------------------------
+ def _build_section_path(
+ self,
+ top_flange: QRectF,
+ web: QRectF,
+ bottom_flange: QRectF,
+ root_radius: float,
+ toe_radius: float,
+ ) -> Optional[QPainterPath]:
+ """
+ Builds the section path.
+ - For rolled sections: SHARP outer corners, CURVED inner corners/roots.
+ - For welded sections: ALL corners are SHARP (radii are 0).
+ """
+ if min(top_flange.width(), bottom_flange.width(), web.width()) <= 0:
+ return None
+
+ tf_left, tf_right = top_flange.left(), top_flange.right()
+ tf_top, tf_bottom = top_flange.top(), top_flange.bottom()
+ bf_left, bf_right = bottom_flange.left(), bottom_flange.right()
+ bf_top, bf_bottom = bottom_flange.top(), bottom_flange.bottom()
+ web_left, web_right = web.left(), web.right()
+
+ # Determine effective radii.
+ # For welded sections, these will be 0.0.
+ # For rolled sections, minimum visual values are enforced if DB values are missing.
+ top_root = self._effective_root_radius_px(root_radius, top_flange, web)
+ bottom_root = self._effective_root_radius_px(root_radius, bottom_flange, web)
+ top_toe = self._effective_toe_radius_px(toe_radius, top_flange)
+ bottom_toe = self._effective_toe_radius_px(toe_radius, bottom_flange)
+
+ path = QPainterPath()
+
+ # --- TOP FLANGE ---
+
+ # 1. Start at Top-Left Corner (Sharp)
+ path.moveTo(tf_left, tf_top)
+
+ # 2. Top Edge -> Top-Right Corner (Sharp)
+ path.lineTo(tf_right, tf_top)
+
+ # 3. Top-Right Vertical Face -> Start of Toe Curve
+ path.lineTo(tf_right, tf_bottom - top_toe)
+
+ # 4. Top-Right Inner Toe Curve (R2)
+ if top_toe > 0:
+ rect = QRectF(tf_right - 2*top_toe, tf_bottom - 2*top_toe, 2*top_toe, 2*top_toe)
+ path.arcTo(rect, 0, -90)
+ else:
+ path.lineTo(tf_right, tf_bottom)
+
+ # 5. Underside -> Start of Root Curve (R1)
+ path.lineTo(web_right + top_root, tf_bottom)
+
+ # 6. Top-Right Root Fillet (R1)
+ if top_root > 0:
+ rect = QRectF(web_right, tf_bottom, 2*top_root, 2*top_root)
+ path.arcTo(rect, 90, 90)
+ else:
+ path.lineTo(web_right, tf_bottom)
+
+ # 7. Web Right Side -> Bottom Root
+ path.lineTo(web_right, bf_top - bottom_root)
+
+ # 8. Bottom-Right Root Fillet (R1)
+ if bottom_root > 0:
+ rect = QRectF(web_right, bf_top - 2*bottom_root, 2*bottom_root, 2*bottom_root)
+ path.arcTo(rect, 180, 90)
+ else:
+ path.lineTo(web_right, bf_top)
+
+ # 9. Bottom Flange Top Side -> Inner Toe
+ path.lineTo(bf_right - bottom_toe, bf_top)
+
+ # 10. Bottom-Right Inner Toe Curve (R2)
+ if bottom_toe > 0:
+ rect = QRectF(bf_right - 2*bottom_toe, bf_top, 2*bottom_toe, 2*bottom_toe)
+ path.arcTo(rect, 90, -90)
+ else:
+ path.lineTo(bf_right, bf_top)
+
+ # 11. Bottom-Right Vertical Face -> Bottom-Right Corner (Sharp)
+ path.lineTo(bf_right, bf_bottom)
+
+ # 12. Bottom Edge -> Bottom-Left Corner (Sharp)
+ path.lineTo(bf_left, bf_bottom)
+
+ # 13. Bottom-Left Vertical Face -> Inner Toe
+ path.lineTo(bf_left, bf_top + bottom_toe)
+
+ # 14. Bottom-Left Inner Toe Curve (R2)
+ if bottom_toe > 0:
+ rect = QRectF(bf_left, bf_top, 2*bottom_toe, 2*bottom_toe)
+ path.arcTo(rect, 180, -90)
+ else:
+ path.lineTo(bf_left, bf_top)
+
+ # 15. Top Side -> Root
+ path.lineTo(web_left - bottom_root, bf_top)
+
+ # 16. Bottom-Left Root Fillet (R1)
+ if bottom_root > 0:
+ rect = QRectF(web_left - 2*bottom_root, bf_top - 2*bottom_root, 2*bottom_root, 2*bottom_root)
+ path.arcTo(rect, 270, 90)
+ else:
+ path.lineTo(web_left, bf_top)
+
+ # 17. Web Left Side -> Top Root
+ path.lineTo(web_left, tf_bottom + top_root)
+
+ # 18. Top-Left Root Fillet (R1)
+ if top_root > 0:
+ rect = QRectF(web_left - 2*top_root, tf_bottom, 2*top_root, 2*top_root)
+ path.arcTo(rect, 0, 90)
+ else:
+ path.lineTo(web_left, tf_bottom)
+
+ # 19. Underside -> Inner Toe
+ path.lineTo(tf_left + top_toe, tf_bottom)
+
+ # 20. Top-Left Inner Toe Curve (R2)
+ if top_toe > 0:
+ rect = QRectF(tf_left, tf_bottom - 2*top_toe, 2*top_toe, 2*top_toe)
+ path.arcTo(rect, 270, -90)
+ else:
+ path.lineTo(tf_left, tf_bottom)
+
+ # 21. Left Face -> Back to Start (Sharp)
+ path.lineTo(tf_left, tf_top)
+
+ path.closeSubpath()
+ return path
+
+ def _snap_coordinate(self, value: float, *, precision: float = 0.5) -> float:
+ """Quantize coordinates to reduce anti-alias fuzz on shared anchors."""
+
+ if not precision or precision <= 0:
+ return value
+ return round(value / precision) * precision
+
+ def _effective_toe_radius_px(self, requested: float, flange: QRectF) -> float:
+ # If this is a welded section, radii must be sharp.
+ if self._show_welds:
+ return 0.0
+
+ max_radius = max(0.0, min(flange.width() / 2.0, flange.height()))
+ if max_radius == 0.0:
+ return 0.0
+
+ # Enforce Minimum Visual Radius (e.g. 4px) if requested is 0/missing for rolled sections
+ val = max(requested, self._min_toe_radius_px)
+ return self._snap_coordinate(min(val, max_radius))
+
+ def _effective_root_radius_px(self, requested: float, flange: QRectF, web: QRectF) -> float:
+ # If this is a welded section, radii must be sharp.
+ if self._show_welds:
+ return 0.0
+
+ flange_overhang = (flange.width() - web.width()) / 2.0
+ max_radius = max(0.0, min(flange_overhang, web.height() / 2.0))
+
+ if max_radius == 0.0:
+ return 0.0
+
+ # Enforce Minimum Visual Radius (e.g. 8px) if requested is 0/missing for rolled sections
+ val = max(requested, self._min_root_radius_px)
+ return self._snap_coordinate(min(val, max_radius))
+
+ def _draw_welds(self, painter: QPainter, top_flange: QRectF, web: QRectF, bottom_flange: QRectF) -> None:
+ painter.save()
+ painter.setRenderHint(QPainter.Antialiasing, True)
+ fill_color = QColor("#0f0f0f")
+ outline_pen = QPen(QColor("#0f0f0f"), 0.9)
+ outline_pen.setCosmetic(True)
+ painter.setBrush(fill_color)
+ painter.setPen(outline_pen)
+
+ horizontal_leg = max(4.0, min(web.width() * 0.8, 24.0))
+ top_vertical_leg = max(4.0, min(top_flange.height() * 0.9, 22.0))
+ bottom_vertical_leg = max(4.0, min(bottom_flange.height() * 0.9, 22.0))
+
+ for sign in (-1, 1):
+ top_corner_x = web.left() if sign < 0 else web.right()
+ top_corner = QPointF(top_corner_x, top_flange.bottom())
+ painter.drawPath(
+ self._build_fillet_path(
+ top_corner,
+ horizontal_leg,
+ top_vertical_leg,
+ horizontal_sign=sign,
+ vertical_sign=1.0,
+ )
+ )
+
+ bottom_corner_x = web.left() if sign < 0 else web.right()
+ bottom_corner = QPointF(bottom_corner_x, bottom_flange.top())
+ painter.drawPath(
+ self._build_fillet_path(
+ bottom_corner,
+ horizontal_leg,
+ bottom_vertical_leg,
+ horizontal_sign=sign,
+ vertical_sign=-1.0,
+ )
+ )
+
+ painter.restore()
+
+ def _build_fillet_path(
+ self,
+ corner: QPointF,
+ horizontal_leg: float,
+ vertical_leg: float,
+ *,
+ horizontal_sign: float,
+ vertical_sign: float,
+ ) -> QPainterPath:
+ leg_x = horizontal_leg * horizontal_sign
+ leg_y = vertical_leg * vertical_sign
+ path = QPainterPath(corner)
+ path.lineTo(QPointF(corner.x() + leg_x, corner.y()))
+ control = QPointF(corner.x() + leg_x * 0.55, corner.y() + leg_y * 0.55)
+ path.quadTo(control, QPointF(corner.x(), corner.y() + leg_y))
+ path.closeSubpath()
+ return path
+
+ def _draw_placeholder(self, painter: QPainter) -> None:
+ painter.save()
+ pen = QPen(QColor("#b7b7b7"), 1.2, Qt.DashLine)
+ pen.setCosmetic(True)
+ painter.setPen(pen)
+ painter.drawRect(self.rect().adjusted(12, 12, -12, -12))
+ painter.setPen(QColor("#6f6f6f"))
+ font = QFont(self.font())
+ font.setFamily(self._brand_font_family)
+ font.setPointSizeF(max(font.pointSizeF(), 10.0))
+ painter.setFont(font)
+ painter.drawText(self.rect(), Qt.AlignCenter, "Select a section to preview")
+ painter.restore()
+
+ def _set_dimension_pen(self, painter: QPainter, key: str) -> QColor:
+ color = self._dimension_palette.get(key, self._dimension_color)
+ pen = QPen(color, 1.6)
+ pen.setCosmetic(True)
+ pen.setCapStyle(Qt.FlatCap)
+ painter.setPen(pen)
+ return color
+
+ def _draw_vertical_thickness_dimension(
+ self,
+ painter: QPainter,
+ flange_edge_x: float,
+ top_y: float,
+ bottom_y: float,
+ thickness: Optional[float],
+ color: QColor,
+ *,
+ label_symbol: str,
+ label_align: Qt.Alignment,
+ ) -> None:
+ extension = self._dim_gap * 0.9
+ anchor_x = self._snap_coordinate(flange_edge_x)
+ dimension_x = self._snap_coordinate(anchor_x - extension)
+ snapped_top_y = self._snap_coordinate(top_y)
+ snapped_bottom_y = self._snap_coordinate(bottom_y)
+ top_extension = QPointF(dimension_x, snapped_top_y)
+ bottom_extension = QPointF(dimension_x, snapped_bottom_y)
+
+ painter.drawLine(QPointF(anchor_x, snapped_top_y), top_extension)
+ painter.drawLine(QPointF(anchor_x, snapped_bottom_y), bottom_extension)
+ painter.drawLine(top_extension, bottom_extension)
+ self._draw_arrow_head(painter, top_extension, QPointF(0, -1), color)
+ self._draw_arrow_head(painter, bottom_extension, QPointF(0, 1), color)
+
+ label_anchor = QPointF(
+ dimension_x - self._dim_gap * 0.4,
+ (snapped_top_y + snapped_bottom_y) / 2.0,
+ )
+ self._draw_label(
+ painter,
+ self._format_label_markup(label_symbol, thickness),
+ label_anchor,
+ label_align,
+ with_background=False,
+ color=color,
+ )
+
+ def _draw_web_thickness_dimension(
+ self,
+ painter: QPainter,
+ left_x: float,
+ right_x: float,
+ mid_y: float,
+ thickness: Optional[float],
+ color: QColor,
+ *,
+ label_symbol: str,
+ ) -> None:
+ snapped_mid_y = self._snap_coordinate(mid_y)
+ left_point = QPointF(self._snap_coordinate(left_x), snapped_mid_y)
+ right_point = QPointF(self._snap_coordinate(right_x), snapped_mid_y)
+ painter.drawLine(left_point, right_point)
+ self._draw_arrow_head(painter, left_point, QPointF(-1, 0), color)
+ self._draw_arrow_head(painter, right_point, QPointF(1, 0), color)
+
+ label_offset = self._dim_gap * 0.6
+ label_anchor = QPointF(left_point.x() - label_offset, snapped_mid_y)
+ self._draw_label(
+ painter,
+ self._format_label_markup(label_symbol, thickness),
+ label_anchor,
+ Qt.AlignRight | Qt.AlignVCenter,
+ with_background=False,
+ color=color,
+ )
+
+ def _draw_dimension_line(
+ self,
+ painter: QPainter,
+ start: QPointF,
+ end: QPointF,
+ color: QColor,
+ *,
+ external: bool = False,
+ ) -> None:
+ direction = QPointF(end.x() - start.x(), end.y() - start.y())
+ length = math.hypot(direction.x(), direction.y())
+ if length == 0:
+ return
+ painter.drawLine(start, end)
+ self._draw_arrow_head(painter, start, direction, color)
+ self._draw_arrow_head(painter, end, QPointF(-direction.x(), -direction.y()), color)
+
+ def _draw_arrow_head(self, painter: QPainter, tip: QPointF, direction: QPointF, color: QColor) -> None:
+ length = math.hypot(direction.x(), direction.y())
+ if length == 0:
+ return
+
+ unit = QPointF(direction.x() / length, direction.y() / length)
+ normal = QPointF(-unit.y(), unit.x())
+
+ arrow = self._arrow_size
+
+ # --- Adjusted Geometry for Narrow/Tall Arrows ---
+ # 1.3 makes the arrow longer/taller (previously 0.9)
+ arrow_length = arrow * 1.3
+
+ # 0.25 makes the base narrower (previously 0.45)
+ # This creates a sharp, technical drafting look.
+ arrow_half_width = arrow * 0.25
+
+ base = tip + unit * arrow_length
+ left = base + normal * arrow_half_width
+ right = base - normal * arrow_half_width
+
+ painter.save()
+ painter.setBrush(color)
+ painter.setPen(Qt.NoPen)
+ painter.drawPolygon([tip, left, right])
+ painter.restore()
+
+ def _draw_label(
+ self,
+ painter: QPainter,
+ text: str,
+ anchor: QPointF,
+ align: Qt.Alignment,
+ *,
+ with_background: bool = True,
+ color: Optional[QColor] = None,
+ ) -> None:
+ text_color = color or self._text_color
+ html_text = text if color is None else f'{text}'
+
+ doc = QTextDocument()
+ doc.setDefaultFont(painter.font())
+ doc.setHtml(html_text)
+ text_size = doc.size()
+ padding_x = 6
+ padding_y = 4
+ rect = QRectF(0, 0, text_size.width() + padding_x * 2, text_size.height() + padding_y * 2)
+
+ if align & Qt.AlignLeft:
+ rect.moveLeft(anchor.x())
+ elif align & Qt.AlignRight:
+ rect.moveRight(anchor.x())
+ else:
+ rect.moveCenter(QPointF(anchor.x(), rect.center().y()))
+
+ if align & Qt.AlignTop:
+ rect.moveTop(anchor.y())
+ elif align & Qt.AlignBottom:
+ rect.moveBottom(anchor.y())
+ else:
+ rect.moveCenter(QPointF(rect.center().x(), anchor.y()))
+
+ content_rect = QRectF(
+ rect.left() + padding_x,
+ rect.top() + padding_y,
+ text_size.width(),
+ text_size.height(),
+ )
+
+ if with_background:
+ painter.save()
+ painter.setPen(Qt.NoPen)
+ painter.setBrush(self._label_bg)
+ painter.drawRoundedRect(rect, 4, 4)
+ painter.restore()
+
+ painter.save()
+ painter.translate(content_rect.topLeft())
+ doc.drawContents(painter)
+ painter.restore()
+
+ def _format_label_markup(self, symbol: str, value: Optional[float] = None) -> str:
+ formatted_symbol = self._format_symbol_markup(symbol)
+ if value is None:
+ return formatted_symbol
+ return f"{formatted_symbol} = {self._format_mm(value)}"
+
+ @staticmethod
+ def _format_symbol_markup(symbol: str) -> str:
+ clean = symbol.lower().strip()
+ if len(clean) <= 1:
+ return clean or symbol
+ return f"{clean[0]}{clean[1:]}"
+
+ @staticmethod
+ def _color_to_hex(color: QColor) -> str:
+ return color.name(QColor.HexRgb)
+
+ @staticmethod
+ def _format_mm(value: float) -> str:
+ rounded = round(value)
+ if abs(value - rounded) < 0.01:
+ return f"{rounded} mm"
+ return f"{value:.1f} mm"
\ No newline at end of file
diff --git a/tests/unit/test_girder_properties.py b/tests/unit/test_girder_properties.py
new file mode 100644
index 00000000..5c03ec51
--- /dev/null
+++ b/tests/unit/test_girder_properties.py
@@ -0,0 +1,29 @@
+from osdagbridge.core.bridge_components.super_structure.girder import properties
+
+
+def test_get_beam_profile_reads_from_resource_db():
+ profile = properties.get_beam_profile("WB 500")
+
+ assert profile is not None
+ assert profile.depth_mm == 500.0
+ assert profile.flange_width_mm == 250.0
+ assert profile.web_thickness_mm == 9.9
+
+
+def test_get_rolled_section_returns_outline_dict():
+ outline = properties.get_rolled_section("WB 500")
+
+ assert outline == {
+ "designation": "WB 500",
+ "depth_mm": 500.0,
+ "top_flange_width_mm": 250.0,
+ "bottom_flange_width_mm": 250.0,
+ "web_thickness_mm": 9.9,
+ "top_flange_thickness_mm": 14.7,
+ "bottom_flange_thickness_mm": 14.7,
+ }
+
+
+def test_unknown_section_returns_none():
+ assert properties.get_beam_profile("NON_EXISTENT") is None
+ assert properties.get_rolled_section("NON_EXISTENT") is None