"""
Options Component module.
Contains the OptionsComponent for handling processing options functionality.
"""
from pathlib import Path
import i18n
from nicegui import ui
from loguru import logger
from .protocols import ConfigManagerProtocol, EventHandlerProtocol
LOGGER = logger.bind(name="CSB-Processing.Options")
[docs]
class OptionsComponent:
"""Component for processing options."""
def __init__(
self,
config_path: Path,
config_manager: ConfigManagerProtocol,
ui_event_handler: EventHandlerProtocol,
) -> None:
self.config_path = config_path
self.output_input = None
self.config_input = None
self.vessel_input = None
self.vessel_name_input = None
self.waterline_input = None
self.output_warning_label = None
self.apply_water_level_checkbox = None
self.already_at_chart_datum_warning = None
self.ui_event_handler = ui_event_handler
self.config_manager = config_manager
[docs]
def _handle_output_path_change(self, e):
"""Handle output path change."""
self.ui_event_handler.update_output_path(e.args[0])
if self.output_warning_label:
if e.args[0] and e.args[0].strip():
self.output_warning_label.style("visibility: hidden")
else:
self.output_warning_label.style("visibility: visible")
[docs]
def _handle_config_path_change(self, e):
"""Handle config path change."""
self.ui_event_handler.update_config_path(e.args[0])
[docs]
async def _handle_select_output_directory(self):
"""Handle output directory selection."""
try:
selected_path = await self.ui_event_handler.select_output_directory()
if selected_path and self.output_input:
self.output_input.value = selected_path
if self.output_warning_label and selected_path.strip():
self.output_warning_label.style("visibility: hidden")
except Exception as ex:
LOGGER.error(
i18n.t(
"app.component.options_component.error_log_select_output",
error=str(ex),
)
)
ui.notification(
i18n.t(
"app.component.options_component.error_open_dir_dialog",
error=str(ex),
),
type="negative",
)
[docs]
async def _handle_select_config_file(self) -> None:
"""Handle config file selection."""
try:
selected_path = await self.ui_event_handler.select_config_file()
if selected_path and self.config_input:
self.config_input.value = selected_path
except Exception as ex:
LOGGER.error(
i18n.t(
"app.component.options_component.error_log_select_config",
error=str(ex),
)
)
ui.notification(
i18n.t(
"app.component.options_component.error_open_file_dialog",
error=str(ex),
),
type="negative",
)
[docs]
def _handle_already_at_chart_datum_toggle(self, e) -> None:
"""Handle 'Already at chart datum' toggle: disable/enable Apply water level accordingly."""
is_checked: bool = e.value
if is_checked:
self.config_manager.apply_water_level = False
if self.apply_water_level_checkbox:
self.apply_water_level_checkbox.value = False
self.apply_water_level_checkbox.disable()
if self.already_at_chart_datum_warning:
self.already_at_chart_datum_warning.style("visibility: visible")
else:
if self.apply_water_level_checkbox:
self.apply_water_level_checkbox.enable()
if self.already_at_chart_datum_warning:
self.already_at_chart_datum_warning.style("visibility: hidden")
[docs]
def _handle_vessel_toggle(self):
"""Handle vessel option toggle."""
try:
waterline_disabled = self.ui_event_handler.toggle_vessel()
if waterline_disabled:
ui.notification(
i18n.t("app.component.options_component.waterline_option_disabled"),
type="info",
)
if self.waterline_input:
self.waterline_input.style("visibility: hidden")
if self.vessel_input:
if self.config_manager.use_vessel:
self.vessel_input.style("visibility: visible")
else:
self.vessel_input.style("visibility: hidden")
except Exception as ex:
LOGGER.error(
i18n.t(
"app.component.options_component.error_log_vessel_toggle",
error=str(ex),
)
)
ui.notification(
i18n.t(
"app.component.options_component.error_notif_vessel_toggle",
error=str(ex),
),
type="negative",
)
[docs]
def _handle_waterline_toggle(self):
"""Handle waterline option toggle."""
try:
vessel_disabled = self.ui_event_handler.toggle_waterline()
if vessel_disabled:
ui.notification(
i18n.t("app.component.options_component.vessel_option_disabled"),
type="info",
)
if self.vessel_input:
self.vessel_input.style("visibility: hidden")
if self.waterline_input:
if self.config_manager.use_waterline:
self.waterline_input.style("visibility: visible")
else:
self.waterline_input.style("visibility: hidden")
except Exception as ex:
LOGGER.error(
i18n.t(
"app.component.options_component.error_log_waterline_toggle",
error=str(ex),
)
)
ui.notification(
i18n.t(
"app.component.options_component.error_notif_waterline_toggle",
error=str(ex),
),
type="negative",
)
[docs]
def create(self):
"""Create the options section."""
ui.separator()
ui.label(i18n.t("app.component.options_component.section_title")).classes(
"text-lg font-bold mt-4"
)
with ui.row().classes("w-full gap-8"):
self._create_left_column()
self._create_right_column()
if self.config_manager:
self._create_water_vessel_rows()
if self.config_manager.already_at_chart_datum:
self.apply_water_level_checkbox.value = False
self.apply_water_level_checkbox.disable()
self.already_at_chart_datum_warning.style("visibility: visible")
else:
self.already_at_chart_datum_warning.style("visibility: hidden")
self._create_filter_section()
[docs]
def _create_water_vessel_rows(self):
"""
Three paired rows with matching flex-1 halves inside a column with uniform gap:
Row 1 — Apply water level (left) | Specify waterline + input (right)
Row 2 — Already at chart datum + warning (left) | Use vessel identifier + input (right)
Row 3 — Merge files (left) | Vessel name input (right)
Each checkbox is wrapped in a fixed-width div (13rem) so both inputs always
start at the same horizontal position regardless of label length.
All rows share the same vertical gap via the wrapping column (gap-4).
"""
with ui.column().classes("w-full gap-4"):
# ── Row 1 ────────────────────────────────────────────────────────────────
with ui.row().classes("w-full items-center gap-8"):
with ui.element("div").classes("flex-1"):
self.apply_water_level_checkbox = (
ui.checkbox(
i18n.t("app.component.options_component.apply_water_level")
)
.bind_value(self.config_manager, "apply_water_level")
.tooltip(
i18n.t(
"app.component.options_component.apply_water_level_tooltip"
)
)
)
with ui.row().classes("flex-1 items-center gap-0"):
with ui.element("div").style("width: 13rem; flex-shrink: 0"):
ui.checkbox(
i18n.t("app.component.options_component.specify_waterline"),
on_change=self._handle_waterline_toggle,
).bind_value(self.config_manager, "use_waterline").tooltip(
i18n.t(
"app.component.options_component.specify_waterline_tooltip"
)
)
self.waterline_input = (
ui.number(
i18n.t(
"app.component.options_component.waterline_input_label"
),
min=0.0,
step=0.01,
format="%.3f",
)
.bind_value(self.config_manager, "waterline_value")
.classes("flex-1")
)
if not self.config_manager.use_waterline:
self.waterline_input.style("visibility: hidden")
# ── Row 2 ────────────────────────────────────────────────────────────────
with ui.row().classes("w-full items-start gap-8"):
with ui.element("div").classes("flex-1"):
ui.checkbox(
i18n.t(
"app.component.options_component.already_at_chart_datum"
),
on_change=self._handle_already_at_chart_datum_toggle,
).bind_value(self.config_manager, "already_at_chart_datum").tooltip(
i18n.t(
"app.component.options_component.already_at_chart_datum_tooltip"
)
)
# Warning label — always occupies space (visibility: hidden keeps layout stable)
self.already_at_chart_datum_warning = (
ui.label(
i18n.t(
"app.component.options_component.already_at_chart_datum_warning"
)
)
.classes("text-sm text-orange-600")
.style("visibility: hidden")
)
with ui.row().classes("flex-1 items-center gap-0"):
with ui.element("div").style("width: 13rem; flex-shrink: 0"):
ui.checkbox(
i18n.t(
"app.component.options_component.use_vessel_identifier"
),
on_change=self._handle_vessel_toggle,
).bind_value(self.config_manager, "use_vessel").tooltip(
i18n.t(
"app.component.options_component.use_vessel_identifier_tooltip"
)
)
self.vessel_input = (
ui.input(
i18n.t(
"app.component.options_component.vessel_identifier_input"
)
)
.bind_value(self.config_manager, "vessel_id")
.classes("flex-1")
)
if not self.config_manager.use_vessel:
self.vessel_input.style("visibility: hidden")
# ── Row 3 — Merge files + Vessel name ────────────────────────────────────
with ui.row().classes("w-full items-center gap-8"):
with ui.element("div").classes("flex-1"):
ui.checkbox(
i18n.t("app.component.options_component.merge_files")
).bind_value(self.config_manager, "merge_files").tooltip(
i18n.t("app.component.options_component.merge_files_tooltip")
)
with ui.element("div").classes("flex-1"):
self.vessel_name_input = (
ui.input(
i18n.t("app.component.options_component.vessel_name_input"),
placeholder=i18n.t(
"app.component.options_component.vessel_name_placeholder"
),
)
.bind_value(self.config_manager, "vessel_name")
.classes("w-full")
.tooltip(
i18n.t(
"app.component.options_component.vessel_name_tooltip"
)
)
)
[docs]
def _create_filter_section(self):
"""Create the filter checkboxes section."""
ui.separator()
ui.label(i18n.t("app.component.options_component.filters_title")).classes(
"text-base font-bold mt-2"
)
ui.label(i18n.t("app.component.options_component.filters_hint")).classes(
"text-sm text-gray-500 mb-1"
)
with ui.row().classes("gap-6 flex-wrap"):
ui.checkbox(
i18n.t("app.component.options_component.filter_depth")
).bind_value(self.config_manager, "filter_depth").tooltip(
i18n.t("app.component.options_component.filter_depth_tooltip")
)
ui.checkbox(
i18n.t("app.component.options_component.filter_speed")
).bind_value(self.config_manager, "filter_speed").tooltip(
i18n.t("app.component.options_component.filter_speed_tooltip")
)
ui.checkbox(
i18n.t("app.component.options_component.filter_latitude")
).bind_value(self.config_manager, "filter_latitude").tooltip(
i18n.t("app.component.options_component.filter_latitude_tooltip")
)
ui.checkbox(
i18n.t("app.component.options_component.filter_longitude")
).bind_value(self.config_manager, "filter_longitude").tooltip(
i18n.t("app.component.options_component.filter_longitude_tooltip")
)
ui.checkbox(
i18n.t("app.component.options_component.filter_time")
).bind_value(self.config_manager, "filter_time").tooltip(
i18n.t("app.component.options_component.filter_time_tooltip")
)
[docs]
def _create_left_column(self):
"""Create left column: output directory path."""
with ui.column().classes("flex-1"):
ui.label(
i18n.t("app.component.options_component.output_dir_label")
).classes("font-bold text-red-600")
with ui.row().classes("w-full gap-2"):
self.output_input = (
ui.input(
placeholder=i18n.t(
"app.component.options_component.output_dir_placeholder"
),
validation={
i18n.t(
"app.component.options_component.output_dir_validation_required"
): lambda value: bool(value and value.strip())
},
)
.classes("flex-1")
.on("update:model-value", self._handle_output_path_change)
)
if self.config_manager and self.config_manager.output_path != Path():
self.output_input.value = str(self.config_manager.output_path)
ui.button(
icon="folder",
on_click=self._handle_select_output_directory,
).props("color=primary outline").tooltip(
i18n.t("app.component.options_component.output_dir_select_tooltip")
)
# CSS visibility preserves layout space — no element shift on hide
self.output_warning_label = ui.label(
i18n.t("app.component.options_component.output_dir_warning")
).classes("text-sm text-red-500")
[docs]
def _create_right_column(self):
"""Create right column: configuration file path."""
with ui.column().classes("flex-1"):
ui.label(
i18n.t("app.component.options_component.config_file_label")
).classes("font-bold")
with ui.row().classes("w-full gap-2"):
self.config_input = (
ui.input(
value=str(self.config_path),
placeholder=i18n.t(
"app.component.options_component.config_file_placeholder"
),
)
.classes("flex-1")
.on("update:model-value", self._handle_config_path_change)
)
ui.button(
icon="settings",
on_click=self._handle_select_config_file,
).props("color=secondary outline").tooltip(
i18n.t("app.component.options_component.config_file_select_tooltip")
)
ui.label(
i18n.t("app.component.options_component.config_file_hint")
).classes("text-sm text-gray-500")