From d44b1d915962f525b9429ca7f919c22ae3444ed9 Mon Sep 17 00:00:00 2001 From: AJ Kolenc Date: Fri, 10 Jan 2025 10:44:38 +0100 Subject: [PATCH 01/12] Progress on custom tables --- source/package/pathways_app/main.py | 130 +++++++++ .../pathways_app/controls/actions_panel.py | 54 ++-- .../pathways_app/controls/editable_cell.py | 10 +- .../pathways_app/controls/metrics_panel.py | 42 +-- .../pathways_app/controls/pathways_panel.py | 127 ++++----- .../pathways_app/controls/scenarios_panel.py | 31 ++- .../pathways_app/controls/sortable_header.py | 35 ++- .../pathways_app/controls/styled_table.py | 258 +++++++++++++++--- .../pathways_app/controls/unit_cell.py | 4 +- .../pathways_app/pathways_app/theme.py | 2 +- 10 files changed, 499 insertions(+), 194 deletions(-) diff --git a/source/package/pathways_app/main.py b/source/package/pathways_app/main.py index b80c27a..782474e 100644 --- a/source/package/pathways_app/main.py +++ b/source/package/pathways_app/main.py @@ -2,6 +2,136 @@ import flet as ft from pathways_app.cli.app import main +import theme +from controls.actions_panel import ActionsPanel +from controls.graph_panel import GraphPanel +from controls.header import SectionHeader +from controls.menu_bar import MenuBar +from controls.metrics_panel import MetricsPanel +from controls.panel import Panel +from controls.pathways_panel import PathwaysPanel +from controls.scenarios_panel import ScenariosPanel +from controls.tabbed_panel import TabbedPanel + + +locale.setlocale(locale.LC_ALL, "") + + +def main(page: ft.Page): + page.theme = theme.theme + page.theme_mode = ft.ThemeMode.LIGHT + + # bitdojo_window could make a custom title bar + # page.window.frameless = True + + page.window.width = 1200 + page.window.height = 800 + page.window.resizable = True + + page.title = "Pathways Generator" + page.fonts = theme.fonts + page.bgcolor = theme.colors.primary_darker + page.padding = 1 + page.spacing = 0 + + project = example.project + + page.appbar = MenuBar(project) + metrics_panel = MetricsPanel(project) + metrics_tab = (SectionHeader(ft.icons.TUNE, "Metrics"), metrics_panel) + + actions_panel = ActionsPanel(project) + actions_tab = ( + SectionHeader(ft.icons.CONSTRUCTION_OUTLINED, "Actions"), + actions_panel, + ) + + scenarios_panel = ScenariosPanel(project) + scenarios_tab = ( + SectionHeader(ft.icons.PUBLIC, "Scenarios"), + scenarios_panel, + ) + + graph_panel = GraphPanel(project) + pathways_panel = PathwaysPanel(project) + + def on_metrics_changed(): + metrics_panel.redraw() + scenarios_panel.redraw() + actions_panel.redraw() + pathways_panel.redraw() + graph_panel.redraw() + + def on_scenarios_changed(): + scenarios_panel.redraw() + graph_panel.redraw() + + def on_actions_changed(): + actions_panel.redraw() + pathways_panel.redraw() + graph_panel.redraw() + + def on_pathways_changed(): + pathways_panel.redraw() + graph_panel.redraw() + + def on_action_color_changed(): + pathways_panel.redraw() + graph_panel.redraw() + + # def on_graph_changed(): + # graph_panel.redraw() + + project.on_conditions_changed.append(on_metrics_changed) + project.on_criteria_changed.append(on_metrics_changed) + project.on_scenarios_changed.append(on_scenarios_changed) + project.on_actions_changed.append(on_actions_changed) + project.on_action_color_changed.append(on_action_color_changed) + project.on_pathways_changed.append(on_pathways_changed) + + page.add( + ft.Container( + expand=True, + padding=theme.variables.panel_spacing, + content=ft.Row( + expand=True, + spacing=theme.variables.panel_spacing, + controls=[ + ft.Column( + expand=2, + spacing=theme.variables.panel_spacing, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[ + TabbedPanel( + selected_index=0, + tabs=[metrics_tab, actions_tab, scenarios_tab], + ) + ], + ), + ft.Column( + expand=3, + spacing=theme.variables.panel_spacing, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[ + Panel(graph_panel), + Panel( + content=ft.Column( + expand=True, + alignment=ft.MainAxisAlignment.START, + controls=[ + pathways_panel, + ], + ), + padding=theme.variables.panel_padding, + ), + ], + ), + ], + ), + bgcolor=theme.colors.primary_lighter, + border_radius=ft.border_radius.only(bottom_left=8, bottom_right=8), + ) + ) logging.basicConfig(level=logging.CRITICAL) diff --git a/source/package/pathways_app/pathways_app/controls/actions_panel.py b/source/package/pathways_app/pathways_app/controls/actions_panel.py index 7abce25..141dec9 100644 --- a/source/package/pathways_app/pathways_app/controls/actions_panel.py +++ b/source/package/pathways_app/pathways_app/controls/actions_panel.py @@ -2,6 +2,14 @@ import random import flet as ft +import theme +from controls.action_icon import ActionIcon +from controls.editable_cell import EditableTextCell +from controls.metric_effect import MetricEffectCell +from controls.metric_value import MetricValueCell +from controls.sortable_header import SortableHeader, SortMode +from controls.styled_button import StyledButton +from controls.styled_table import StyledTable, TableCell, TableColumn from adaptation_pathways.app.model.action import Action from adaptation_pathways.app.model.pathways_project import PathwaysProject @@ -115,36 +123,20 @@ def on_new_action(self, _): self.update() def update_table(self): - sort_mode = SortableHeader.get_sort_mode(self.project.action_sorting) - sort_key = self.project.action_sorting.sort_key - - sortable_headers = [ - SortableHeader( - sort_key="name", - name="Name", - sort_mode=SortMode.NONE if sort_key != "name" else sort_mode, - on_sort=self.on_sort_actions, + columns = [ + TableColumn( + label="Icon", + ), + TableColumn( + label="Name", ), *( - SortableHeader( - sort_key=metric.id, - name=metric.name, - sort_mode=SortMode.NONE if sort_key is not metric.id else sort_mode, - on_sort=self.on_sort_actions, + TableColumn( + label=metric.name, key=metric.id, on_sort=self.on_sort_actions ) for metric in self.project.all_metrics() ), ] - - columns = [ - ft.DataColumn( - label=ft.Text("Icon", expand=True), - ), - *( - ft.DataColumn(label=header, numeric=header.sort_key != "name") - for header in sortable_headers - ), - ] self.action_table.set_columns(columns) self.update_rows() @@ -235,15 +227,11 @@ def update_rows(self): ) rows.append( - ft.DataRow( - [ - ft.DataCell(self.create_icon_editor(action)), - EditableTextCell(action, "name", self.on_name_edited), - *metric_cells, - ], - selected=action.id in self.project.selected_action_ids, - on_select_changed=lambda _, a=action: self.on_action_selected(a), - ) + [ + TableCell(self.create_icon_editor(action)), + EditableTextCell(action, "name", self.on_name_edited), + *metric_cells, + ] ) self.action_table.set_rows(rows) diff --git a/source/package/pathways_app/pathways_app/controls/editable_cell.py b/source/package/pathways_app/pathways_app/controls/editable_cell.py index 21f7175..70eeb28 100644 --- a/source/package/pathways_app/pathways_app/controls/editable_cell.py +++ b/source/package/pathways_app/pathways_app/controls/editable_cell.py @@ -2,12 +2,14 @@ from typing import Callable import flet as ft +import theme +from pathways_app.controls.styled_table import TableCell from pyparsing import abstractmethod from .. import theme -class EditableCell(ft.DataCell, ABC): +class EditableCell(TableCell, ABC): def __init__( self, display_control: ft.Control, @@ -33,7 +35,7 @@ def __init__( self.update_bg() - super().__init__(content=self.cell_content) + super().__init__(control=self.cell_content) def toggle_editing(self, _): self.is_editing = not self.is_editing @@ -47,7 +49,7 @@ def toggle_editing(self, _): self.input_content.visible = self.is_editing self.update_bg() - self.update() + self.control.update() if self.is_editing: self.input_content.focus() @@ -65,7 +67,7 @@ def update_bg(self): def set_calculated(self, is_calculated): self.is_calculated = is_calculated self.update_bg() - self.update() + self.control.update() @abstractmethod def update_display(self): diff --git a/source/package/pathways_app/pathways_app/controls/metrics_panel.py b/source/package/pathways_app/pathways_app/controls/metrics_panel.py index 95c1195..38e556c 100644 --- a/source/package/pathways_app/pathways_app/controls/metrics_panel.py +++ b/source/package/pathways_app/pathways_app/controls/metrics_panel.py @@ -1,6 +1,11 @@ -from typing import Callable +# from typing import Callable import flet as ft +from controls.editable_cell import EditableTextCell +from controls.header import SmallHeader +from controls.styled_button import StyledButton +from controls.styled_table import StyledTable, TableCell, TableColumn +from controls.unit_cell import MetricUnitCell from adaptation_pathways.app.model.metric import Metric from adaptation_pathways.app.model.pathways_project import PathwaysProject @@ -23,8 +28,8 @@ def __init__(self, project: PathwaysProject): self.conditions_table = StyledTable( columns=[ - ft.DataColumn(label=ft.Text("Name")), - ft.DataColumn(label=ft.Text("Unit")), + TableColumn(label="Name"), + TableColumn(label="Unit"), ], rows=[], show_checkboxes=True, @@ -32,8 +37,8 @@ def __init__(self, project: PathwaysProject): self.criteria_table = StyledTable( columns=[ - ft.DataColumn(label=ft.Text("Name")), - ft.DataColumn(label=ft.Text("Unit")), + TableColumn(label="Name"), + TableColumn(label="Unit"), ], rows=[], show_checkboxes=True, @@ -96,7 +101,6 @@ def redraw(self): self.update() def on_metric_updated(self, _): - print(self) self.project.notify_conditions_changed() def on_condition_selected(self, metric: Metric): @@ -138,17 +142,13 @@ def on_delete_criteria(self, _): def get_metric_row( self, metric: Metric, - selected_ids: set[str], - on_metric_selected: Callable[[Metric], None], - ) -> ft.DataRow: - row = ft.DataRow( - [ - EditableTextCell(metric, "name", self.on_metric_updated), - MetricUnitCell(metric, self.on_metric_updated), - ], - selected=metric.id in selected_ids, - ) - row.on_select_changed = lambda e: on_metric_selected(metric) + # selected_ids: set[str], + # on_metric_selected: Callable[[Metric], None], + ) -> list[TableCell]: + row = [ + EditableTextCell(metric, "name", self.on_metric_updated), + MetricUnitCell(metric, self.on_metric_updated), + ] return row def update_metrics(self): @@ -156,8 +156,8 @@ def update_metrics(self): [ self.get_metric_row( metric, - self.project.selected_condition_ids, - self.on_condition_selected, + # self.project.selected_condition_ids, + # self.on_condition_selected, ) for metric in self.project.sorted_conditions ] @@ -169,8 +169,8 @@ def update_metrics(self): [ self.get_metric_row( metric, - self.project.selected_criteria_ids, - self.on_criteria_selected, + # self.project.selected_criteria_ids, + # self.on_criteria_selected, ) for metric in self.project.sorted_criteria ] diff --git a/source/package/pathways_app/pathways_app/controls/pathways_panel.py b/source/package/pathways_app/pathways_app/controls/pathways_panel.py index e175f2b..5479456 100644 --- a/source/package/pathways_app/pathways_app/controls/pathways_panel.py +++ b/source/package/pathways_app/pathways_app/controls/pathways_panel.py @@ -1,4 +1,11 @@ import flet as ft +import theme +from controls.action_icon import ActionIcon +from controls.header import SectionHeader +from controls.metric_value import MetricValueCell +from controls.sortable_header import SortableHeader, SortMode +from controls.styled_button import StyledButton +from controls.styled_table import StyledTable, TableCell, TableColumn from adaptation_pathways.app.model.pathway import Pathway from adaptation_pathways.app.model.pathways_project import PathwaysProject @@ -13,7 +20,7 @@ from .styled_table import StyledTable -class PathwaysPanel(ft.Column): +class PathwaysPanel(ft.Container): def __init__(self, project: PathwaysProject): self.project = project @@ -27,26 +34,22 @@ def __init__(self, project: PathwaysProject): super().__init__( expand=True, - scroll=ft.ScrollMode.AUTO, - controls=[ - ft.Column( - expand=False, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - ft.Row( - [ - SectionHeader( - ft.icons.ACCOUNT_TREE_OUTLINED, "Pathways" - ), - ft.Container(expand=True), - self.delete_pathways_button, - ], - spacing=15, - ), - self.pathway_table, - ], - ) - ], + content=ft.Column( + expand=True, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[ + ft.Row( + [ + SectionHeader(ft.icons.ACCOUNT_TREE_OUTLINED, "Pathways"), + ft.Container(expand=True), + self.delete_pathways_button, + ], + spacing=15, + expand=False, + ), + self.pathway_table, + ], + ), ) def redraw(self): @@ -73,8 +76,8 @@ def on_sort_table(self, header: SortableHeader): self.project.notify_pathways_changed() def update_table(self): - sorting = self.project.pathway_sorting - sort_mode = SortableHeader.get_sort_mode(sorting) + # sorting = self.project.pathway_sorting + # sort_mode = SortableHeader.get_sort_mode(sorting) self.delete_pathways_button.visible = ( len(self.project.selected_pathway_ids) > 0 @@ -83,20 +86,13 @@ def update_table(self): self.pathway_table.set_columns( [ - ft.DataColumn(ft.Text("Pathway")), + TableColumn(label="Pathway", expand=2), *( - ft.DataColumn( - SortableHeader( - metric.id, - metric.name, - sort_mode=( - SortMode.NONE - if sorting.sort_key is not metric.id - else sort_mode - ), - on_sort=self.on_sort_table, - ), - numeric=True, + TableColumn( + label=metric.name, + key=metric.id, + on_sort=self.on_sort_table, + alignment=ft.alignment.center_right, ) for metric in self.project.all_metrics() ), @@ -112,8 +108,8 @@ def update_table(self): # if len(path) > 1: # path = path[1:] row = self.get_pathway_row(pathway, path) - if pathway.id == self.project.root_pathway_id: - row.on_select_changed = None + # if pathway.id == self.project.root_pathway_id: + # row.on_select_changed = None self.rows_by_pathway[pathway] = row rows.append(row) @@ -182,41 +178,38 @@ def get_pathway_row(self, pathway: Pathway, ancestors: list[Pathway]): ), ) - row = ft.DataRow( - [ - ft.DataCell( - ft.Container( - expand=True, - content=ft.Row( - spacing=0, - controls=row_controls, - ), + row = [ + TableCell( + ft.Container( + expand=True, + content=ft.Row( + spacing=0, + controls=row_controls, ), ), - *( - MetricValueCell( - metric, - pathway.metric_data[metric.id], - on_finished_editing=self.on_metric_value_edited, - ) - for metric in self.project.all_metrics() - ), - ], - selected=pathway.id in self.project.selected_pathway_ids, - ) + ), + *( + MetricValueCell( + metric, + pathway.metric_data[metric.id], + on_finished_editing=self.on_metric_value_edited, + ) + for metric in self.project.all_metrics() + ), + ] - if pathway.parent_id is None: - row.color = "#EEEEEE" + # if pathway.parent_id is None: + # row.color = "#EEEEEE" - def on_select_changed(_): - if pathway.id in self.project.selected_pathway_ids: - self.project.selected_pathway_ids.remove(pathway.id) - else: - self.project.selected_pathway_ids.add(pathway.id) + # def on_select_changed(_): + # if pathway.id in self.project.selected_pathway_ids: + # self.project.selected_pathway_ids.remove(pathway.id) + # else: + # self.project.selected_pathway_ids.add(pathway.id) - self.project.notify_pathways_changed() + # self.project.notify_pathways_changed() - row.on_select_changed = on_select_changed + # row.on_select_changed = on_select_changed return row def extend_pathway(self, pathway: Pathway, action_id: str): diff --git a/source/package/pathways_app/pathways_app/controls/scenarios_panel.py b/source/package/pathways_app/pathways_app/controls/scenarios_panel.py index a935bd0..0aa756b 100644 --- a/source/package/pathways_app/pathways_app/controls/scenarios_panel.py +++ b/source/package/pathways_app/pathways_app/controls/scenarios_panel.py @@ -1,4 +1,7 @@ import flet as ft +from controls.styled_button import StyledButton +from controls.styled_dropdown import StyledDropdown +from controls.styled_table import StyledTable, TableCell, TableColumn from adaptation_pathways.app.model.pathways_project import PathwaysProject @@ -39,27 +42,25 @@ def __init__(self, project: PathwaysProject): ), StyledTable( columns=[ - ft.DataColumn(label=ft.Text("Year")), + TableColumn(label="Year"), *( - ft.DataColumn(label=ft.Text(metric.name)) + TableColumn(label=metric.name) for metric in self.project.sorted_conditions ), ], rows=[ - ft.DataRow( - [ - ft.DataCell(ft.Text(year)), - *( - ft.DataCell( - ft.Text( - current_scenario.get_data(year, metric) - or "None" - ) - ) + [ + TableCell(ft.Text(year)), + *( + TableCell( + ft.Text(data or "None"), is_calculated=data is None + ) + for data in [ + current_scenario.get_data(year, metric) for metric in self.project.sorted_conditions - ), - ] - ) + ] + ), + ] for year in year_range ], ), diff --git a/source/package/pathways_app/pathways_app/controls/sortable_header.py b/source/package/pathways_app/pathways_app/controls/sortable_header.py index e6e5dc7..486425f 100644 --- a/source/package/pathways_app/pathways_app/controls/sortable_header.py +++ b/source/package/pathways_app/pathways_app/controls/sortable_header.py @@ -9,8 +9,9 @@ class SortMode(Enum): NONE = 0 - ASCENDING = 1 - DESCENDING = 2 + UNSORTED = 1 + ASCENDING = 2 + DESCENDING = 3 def get_icon(self): match self: @@ -18,8 +19,10 @@ def get_icon(self): return ft.icons.KEYBOARD_ARROW_UP case SortMode.DESCENDING: return ft.icons.KEYBOARD_ARROW_DOWN - case _: + case SortMode.UNSORTED: return ft.icons.UNFOLD_MORE + case _: + return None class SortableHeader(ft.Container): @@ -29,6 +32,10 @@ def __init__( name: str, sort_mode: SortMode = SortMode.NONE, on_sort=None, + expand: bool | int | None = True, + col: int | None = None, + bgcolor: str | None = None, + height: int | None = None, ): self.sort_key = sort_key self.name = name @@ -37,20 +44,31 @@ def __init__( self.update_icon() super().__init__( - expand=True, + expand=expand, + col=col, content=ft.Row( - expand=True, controls=[ft.Text(name, expand=True), self.icon] + expand=True, + controls=[ + ft.Text(name, expand=True, style=theme.text.table_header), + self.icon, + ], ), + padding=theme.variables.table_cell_padding, + bgcolor=bgcolor, + height=height, ) def on_click(_): + if self.sort_mode == SortMode.NONE: + pass + new_sort_mode = ( SortMode.ASCENDING - if self.sort_mode == SortMode.NONE + if self.sort_mode == SortMode.UNSORTED else ( SortMode.DESCENDING if self.sort_mode == SortMode.ASCENDING - else SortMode.NONE + else SortMode.UNSORTED ) ) self.set_sort_mode(new_sort_mode) @@ -75,6 +93,7 @@ def update_icon(self): self.icon.name = self.sort_mode.get_icon() self.icon.color = ( theme.colors.primary_lighter - if self.sort_mode is SortMode.NONE + if self.sort_mode is SortMode.UNSORTED else theme.colors.primary_dark ) + self.icon.visible = self.sort_mode != SortMode.NONE diff --git a/source/package/pathways_app/pathways_app/controls/styled_table.py b/source/package/pathways_app/pathways_app/controls/styled_table.py index 26e4fad..82b9267 100644 --- a/source/package/pathways_app/pathways_app/controls/styled_table.py +++ b/source/package/pathways_app/pathways_app/controls/styled_table.py @@ -1,66 +1,236 @@ # pylint: disable=too-many-arguments +from typing import Callable + import flet as ft +import theme +from pathways_app.controls.sortable_header import SortableHeader, SortMode + + +class TableColumn: + def __init__( + self, + label: str, + key: str | None = None, + on_sort: Callable[[None], None] | None = None, + expand: bool | int | None = True, + alignment: ft.Alignment | None = None, + ): + + self.label = label + self.on_sort = on_sort + self.expand = expand + self.key = label if key is None else key + self.alignment = alignment + -from .. import theme +class TableCell: + def __init__(self, control: ft.Control, is_calculated=False): + self.control = control + self.is_calculated = is_calculated -class StyledTable(ft.DataTable): +class StyledTable(ft.Container): def __init__( self, - columns: list[ft.DataColumn], - rows: list[ft.DataRow], + columns: list[TableColumn], + rows: list[list[TableCell]], row_height=36, sort_column_index: int | None = None, sort_ascending: bool | None = None, + on_sort: Callable[[int], None] | None = None, show_checkboxes=False, ): super().__init__( expand=True, - horizontal_margin=0, - show_checkbox_column=show_checkboxes, - checkbox_horizontal_margin=theme.variables.table_cell_padding.left, - columns=columns, - rows=rows, - bgcolor=theme.colors.true_white, - sort_column_index=sort_column_index, - sort_ascending=sort_ascending, - column_spacing=20, - data_row_min_height=0, - data_row_max_height=row_height, - data_row_color={ - ft.ControlState.HOVERED: theme.colors.off_white, - ft.ControlState.FOCUSED: theme.colors.off_white, - ft.ControlState.SELECTED: theme.colors.primary_white, - ft.ControlState.PRESSED: theme.colors.off_white, - ft.ControlState.DRAGGED: theme.colors.off_white, - ft.ControlState.SCROLLED_UNDER: theme.colors.off_white, - }, - divider_thickness=0, - horizontal_lines=ft.BorderSide(1, theme.colors.primary_lightest), - heading_row_height=30, - heading_row_color=theme.colors.primary_lightest, - heading_text_style=theme.text.table_header, - border=ft.border.all(1, theme.colors.primary_light), + # horizontal_margin=0, + # show_checkbox_column=show_checkboxes, + # checkbox_horizontal_margin=theme.variables.table_cell_padding.left, + # columns=columns, + # rows=rows, + # bgcolor=theme.colors.true_white, + # sort_column_index=sort_column_index, + # sort_ascending=sort_ascending, + # column_spacing=20, + # data_row_min_height=0, + # data_row_max_height=row_height, + # data_row_color={ + # ft.ControlState.HOVERED: theme.colors.off_white, + # ft.ControlState.FOCUSED: theme.colors.off_white, + # ft.ControlState.SELECTED: theme.colors.primary_white, + # ft.ControlState.PRESSED: theme.colors.off_white, + # ft.ControlState.DRAGGED: theme.colors.off_white, + # ft.ControlState.SCROLLED_UNDER: theme.colors.off_white, + # }, + # divider_thickness=0, + # horizontal_lines=ft.BorderSide(1, theme.colors.primary_lightest), + # heading_row_height=30, + # heading_row_color=theme.colors.primary_lightest, + # heading_text_style=theme.text.table_header, + # border=ft.border.all(1, theme.colors.primary_light), ) + self.row_height = row_height + self.sort_column_index = sort_column_index + self.show_checkboxes = show_checkboxes + self.on_sort = on_sort + self.sort_ascending = sort_ascending + self.selected_row_indices: set[int] = set() + self.header_row = ft.Container( + content=ft.Row([], expand=False, spacing=0), + bgcolor=theme.colors.primary_lightest, + border=ft.border.only( + # left=ft.BorderSide(1, theme.colors.primary_medium), + # right=ft.BorderSide(1, theme.colors.primary_medium), + bottom=ft.BorderSide(1, theme.colors.primary_medium), + ), + border_radius=ft.border_radius.only( + top_left=theme.variables.small_radius, + top_right=theme.variables.small_radius, + ), + ) + self.rows = ft.Container( + content=ft.Column([], expand=True, scroll=ft.ScrollMode.ALWAYS, spacing=0), + # bgcolor=theme.colors.true_white, + # border=ft.border.only( + # left=ft.BorderSide(1, theme.colors.primary_medium), + # right=ft.BorderSide(1, theme.colors.primary_medium), + # bottom=ft.BorderSide(1, theme.colors.primary_medium), + # ), + expand=True, + border_radius=ft.border_radius.only( + bottom_left=theme.variables.small_radius, + bottom_right=theme.variables.small_radius, + ), + ) + self.content = ft.Column([self.header_row, self.rows], spacing=0, expand=True) + self.row_data = rows + self.column_data = columns self.set_columns(columns) self.set_rows(rows) - def set_columns(self, columns: list[ft.DataColumn]): - for column in columns: - column.label = ft.Container( - content=column.label, - expand=True, + def on_header_sorted(self, column_index: int, header: SortableHeader): + self.sort_ascending = header.sort_mode == SortMode.ASCENDING + self.sort_column_index = ( + None if header.sort_mode == SortMode.NONE else column_index + ) + if self.on_sort is not None: + self.on_sort(column_index) + + def on_row_selected(self, index: int): + print(index) + if index in self.selected_row_indices: + self.selected_row_indices.remove(index) + else: + self.selected_row_indices.add(index) + + self.update_selected_rows() + self.update() + + def on_all_rows_selected(self): + if self.select_all_checkbox.value: + for index in range(len(self.row_data)): + self.selected_row_indices.add(index) + else: + self.selected_row_indices.clear() + + self.update_selected_rows() + self.update() + + def set_columns(self, columns: list[TableColumn]): + self.column_data = columns + self.select_all_checkbox = ft.Checkbox( + on_change=lambda evt: self.on_all_rows_selected(), + ) + self.header_row.content.controls = [ + ft.Container( + content=self.select_all_checkbox, + width=40, + height=self.row_height, + bgcolor=theme.colors.primary_lightest, padding=theme.variables.table_cell_padding, - ) - self.columns = columns - - def set_rows(self, rows: list[ft.DataRow]): - for row in rows: - for cell in row.cells: - cell.content = ft.Container( - content=cell.content, - padding=theme.variables.table_cell_padding, + ), + *( + ( + SortableHeader( + sort_key=column.key, + name=column.label, + sort_mode=( + SortMode.NONE + if column.on_sort is None + else SortMode.UNSORTED + ), + on_sort=lambda header, i=index: self.on_header_sorted( + i, header + ), + expand=column.expand, + bgcolor=theme.colors.primary_lightest, + height=self.row_height, + ) + ) + for index, column in enumerate(columns) + ), + ] + + def update_selected_rows(self): + self.select_all_checkbox.value = len(self.selected_row_indices) >= len( + self.row_data + ) + for index, row in enumerate(self.rows.content.controls): + row.bgcolor = ( + theme.colors.primary_white + if index in self.selected_row_indices + else ( + theme.colors.true_white + if index % 2 == 0 + else theme.colors.off_white ) + ) + checkbox = row.content.controls[0].content + checkbox.value = index in self.selected_row_indices - self.rows = rows + def set_rows(self, rows: list[list[TableCell]]): + self.row_data = rows + self.rows.content.controls = [ + ft.Container( + ft.Row( + [ + ft.Container( + content=ft.Checkbox( + value=index in self.selected_row_indices, + on_change=lambda evt, i=index: self.on_row_selected(i), + ), + padding=theme.variables.table_cell_padding, + width=40, + height=self.row_height, + ), + *( + self._create_row(index, cell) + for index, cell in enumerate(row) + ), + ], + spacing=0, + ), + expand=True, + ) + for index, row in enumerate(rows) + ] + self.update_selected_rows() + + def _create_row(self, index: int, cell: TableCell) -> ft.Container: + column = self.column_data[index] + return ft.Container( + content=ft.Column( + [ + ft.Container(cell.control, expand=True, padding=10), + ], + expand=True, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + # alignment=ft.MainAxisAlignment.END, + # theme.variables.table_cell_padding, + # alignment=self.column_data[index], + ), + bgcolor=(theme.colors.calculated_bg if cell.is_calculated else None), + expand=column.expand, + height=self.row_height, + # padding=theme.variables.table_cell_padding, + ) diff --git a/source/package/pathways_app/pathways_app/controls/unit_cell.py b/source/package/pathways_app/pathways_app/controls/unit_cell.py index 31b7ee1..7e409f5 100644 --- a/source/package/pathways_app/pathways_app/controls/unit_cell.py +++ b/source/package/pathways_app/pathways_app/controls/unit_cell.py @@ -1,13 +1,15 @@ from typing import Callable import flet as ft +import theme +from pathways_app.controls.styled_table import TableCell from adaptation_pathways.app.model.metric import Metric, MetricUnit, default_units from .. import theme -class MetricUnitCell(ft.DataCell): +class MetricUnitCell(TableCell): def __init__( self, metric: Metric, diff --git a/source/package/pathways_app/pathways_app/theme.py b/source/package/pathways_app/pathways_app/theme.py index ebde983..8d7c593 100644 --- a/source/package/pathways_app/pathways_app/theme.py +++ b/source/package/pathways_app/pathways_app/theme.py @@ -13,7 +13,7 @@ class DefaultThemeColors: primary_darker = "#160E59" secondary_light = "#91E0EC" secondary_medium = "#48BDCF" - calculated_bg = "#E5E5E5" + calculated_bg = "#10000000" colors = DefaultThemeColors() From ce48b52b8d673dc1ec563f5c015ebcf9544cd9e1 Mon Sep 17 00:00:00 2001 From: ajkolenc Date: Fri, 10 Jan 2025 17:33:37 +0100 Subject: [PATCH 02/12] Start refactoring calculation --- .../adaptation_pathways/app/model/metric.py | 13 +++- .../app/model/pathways_project.py | 44 +++++++----- .../app/service/pathway_service.py | 18 ----- .../app/service/scenario_service.py | 2 +- source/package/pathways_app/main.py | 1 - .../pathways_app/controls/actions_panel.py | 4 +- .../pathways_app/controls/editable_cell.py | 68 +++++++++++++------ .../pathways_app/controls/metric_value.py | 28 +++++--- .../pathways_app/controls/scenarios_panel.py | 2 +- .../pathways_app/controls/sortable_header.py | 2 + .../pathways_app/controls/styled_table.py | 29 ++++---- .../pathways_app/pathways_app/example.py | 3 - .../pathways_app/pathways_app/theme.py | 4 +- 13 files changed, 124 insertions(+), 94 deletions(-) diff --git a/source/package/adaptation_pathways/app/model/metric.py b/source/package/adaptation_pathways/app/model/metric.py index 692c0f0..1b939e2 100644 --- a/source/package/adaptation_pathways/app/model/metric.py +++ b/source/package/adaptation_pathways/app/model/metric.py @@ -39,7 +39,6 @@ def format(self, value: float): class Metric: id: str name: str - current_value: float unit_or_default: MetricUnit | str @property @@ -57,10 +56,20 @@ def __hash__(self) -> int: return self.id.__hash__() +class MetricValueState(Enum): + BASE = 0 + ESTIMATE = (1,) + OVERRIDE = 2 + + @dataclasses.dataclass class MetricValue: value: float - is_estimate: bool = False + state: MetricValueState = MetricValueState.BASE + + @property + def is_estimate(self): + return self.state == MetricValueState.ESTIMATE class MetricOperation(Enum): diff --git a/source/package/adaptation_pathways/app/model/pathways_project.py b/source/package/adaptation_pathways/app/model/pathways_project.py index ad0510c..99db66a 100644 --- a/source/package/adaptation_pathways/app/model/pathways_project.py +++ b/source/package/adaptation_pathways/app/model/pathways_project.py @@ -7,7 +7,7 @@ from adaptation_pathways.app.model.sorting import SortingInfo, SortTarget from .action import Action -from .metric import Metric, MetricEffect, MetricOperation, MetricValue +from .metric import Metric, MetricEffect, MetricOperation, MetricValue, MetricValueState from .pathway import Pathway from .scenario import Scenario @@ -274,6 +274,8 @@ def sort_by_attr(action_id: str): self.action_sorting.sort_by_id() def get_pathway(self, pathway_id: str) -> Pathway | None: + if pathway_id is None: + return None return self.pathways_by_id.get(pathway_id, None) def create_pathway( @@ -300,30 +302,36 @@ def _update_pathway_value( self, pathway: Pathway, metric: Metric, updated_pathway_ids: set[str] ): pathway_action = self.get_action(pathway.action_id) - current_value = pathway.metric_data.get(metric.id, MetricValue(0, True)) - pathway.metric_data[metric.id] = current_value - - if current_value is not None and not current_value.is_estimate: - updated_pathway_ids.add(pathway.id) - return - - if pathway.parent_id is None: - pathway.metric_data[metric.id] = MetricValue(metric.current_value) - updated_pathway_ids.add(pathway.id) - return - parent = self.get_pathway(pathway.parent_id) - if parent is None: - pathway.metric_data[metric.id] = MetricValue(metric.current_value) + current_value = pathway.metric_data.get(metric.id, None) + + # Initialize the value if there was none + if current_value is None: + current_value = MetricValue( + 0, + ( + MetricValueState.ESTIMATE + if parent is not None + else MetricValueState.BASE + ), + ) + pathway.metric_data[metric.id] = current_value + + # If we have a non-estimate value, we don't need to update anything + if current_value.state != MetricValueState.ESTIMATE: updated_pathway_ids.add(pathway.id) return - parent_value = parent.metric_data[metric.id] - if parent.id not in updated_pathway_ids and parent_value.is_estimate: self._update_pathway_value(parent, metric, updated_pathway_ids) - current_value.value = pathway_action.apply_effect(metric.id, parent_value.value) + base_value = 0 + if parent is not None: + parent_value = parent.metric_data.get(metric.id, None) + if parent_value is not None: + base_value = parent_value.value + + current_value.value = pathway_action.apply_effect(metric.id, base_value) updated_pathway_ids.add(pathway.id) def delete_pathway(self, pathway_id: str) -> Pathway | None: diff --git a/source/package/adaptation_pathways/app/service/pathway_service.py b/source/package/adaptation_pathways/app/service/pathway_service.py index 4dbfccb..f63caf8 100644 --- a/source/package/adaptation_pathways/app/service/pathway_service.py +++ b/source/package/adaptation_pathways/app/service/pathway_service.py @@ -29,21 +29,3 @@ def generate_pathways( # Generate all pathways that don't violate the provided constraints # Estimate a value for each metric based on its Metric.estimate method return [] - - @staticmethod - def estimate_metric( - pathway: Pathway, - metric: Metric, - all_actions: list[Action], - all_pathways: list[Pathway], - ) -> float: - # For manual estimate, just use the existing data (if there is any) - if metric.estimate is MetricEstimate.MANUAL: - current_data = pathway.metric_data.get(metric) - if current_data is None or current_data.is_estimate: - return 0 - - return current_data.value - - # Do the proper estimate respecting the Metric.estimate method - return 0 diff --git a/source/package/adaptation_pathways/app/service/scenario_service.py b/source/package/adaptation_pathways/app/service/scenario_service.py index 2ab7163..29e4f5b 100644 --- a/source/package/adaptation_pathways/app/service/scenario_service.py +++ b/source/package/adaptation_pathways/app/service/scenario_service.py @@ -9,4 +9,4 @@ def estimate_metric_at_time( metric: Metric, time: float, scenario: Scenario ) -> float: # Replace with a linear interpolation/extrapolation using the nearest points - return metric.current_value + return 0 diff --git a/source/package/pathways_app/main.py b/source/package/pathways_app/main.py index 782474e..6863e41 100644 --- a/source/package/pathways_app/main.py +++ b/source/package/pathways_app/main.py @@ -23,7 +23,6 @@ def main(page: ft.Page): # bitdojo_window could make a custom title bar # page.window.frameless = True - page.window.width = 1200 page.window.height = 800 page.window.resizable = True diff --git a/source/package/pathways_app/pathways_app/controls/actions_panel.py b/source/package/pathways_app/pathways_app/controls/actions_panel.py index 141dec9..c760ae7 100644 --- a/source/package/pathways_app/pathways_app/controls/actions_panel.py +++ b/source/package/pathways_app/pathways_app/controls/actions_panel.py @@ -124,9 +124,7 @@ def on_new_action(self, _): def update_table(self): columns = [ - TableColumn( - label="Icon", - ), + TableColumn(label="Icon", width=45, expand=False), TableColumn( label="Name", ), diff --git a/source/package/pathways_app/pathways_app/controls/editable_cell.py b/source/package/pathways_app/pathways_app/controls/editable_cell.py index 70eeb28..0abc49d 100644 --- a/source/package/pathways_app/pathways_app/controls/editable_cell.py +++ b/source/package/pathways_app/pathways_app/controls/editable_cell.py @@ -14,27 +14,59 @@ def __init__( self, display_control: ft.Control, edit_control: ft.Control, - is_calculated=False, is_editing=False, + is_calculated=False, + can_reset=False, + alignment: ft.Alignment | None = ft.alignment.center_left, + padding: int | ft.Padding | None = theme.variables.table_cell_padding, on_finished_editing: Callable[["EditableCell"], None] | None = None, ): self.display_content = display_control self.input_content = edit_control - self.is_calculated = is_calculated + self.calculated_icon = ft.Container( + ft.Icon( + ft.icons.CALCULATE, + size=theme.variables.calculated_icon_size, + color=theme.colors.calculated_icon, + ), + expand=True, + alignment=ft.alignment.top_left, + ) + self.reset_button = ft.Container( + ft.Icon( + ft.icons.CALCULATE, + size=theme.variables.calculated_icon_size, + color=theme.colors.calculated_icon, + ), + on_click=self.on_reset_to_calculated, + ) + self.is_editing = is_editing + self.is_calculated = is_calculated + self.can_reset = can_reset self.on_finished_editing = on_finished_editing - self.display_content.visible = not self.is_editing - self.input_content.visible = self.is_editing + self.update_visibility() self.cell_content = ft.Container( expand=True, - content=ft.Stack([self.display_content, self.input_content]), + content=ft.Stack( + [ + self.display_content, + ft.Row( + [self.reset_button, self.input_content], + expand=True, + ), + self.calculated_icon, + ], + expand=True, + alignment=alignment, + ), + padding=padding, + bgcolor=theme.colors.calculated_bg if self.is_calculated else None, on_click=self.toggle_editing, ) - self.update_bg() - super().__init__(control=self.cell_content) def toggle_editing(self, _): @@ -45,10 +77,7 @@ def toggle_editing(self, _): else: self.update_display() - self.display_content.visible = not self.is_editing - self.input_content.visible = self.is_editing - - self.update_bg() + self.update_visibility() self.control.update() if self.is_editing: @@ -57,17 +86,14 @@ def toggle_editing(self, _): if self.on_finished_editing is not None: self.on_finished_editing(self) - def update_bg(self): - self.cell_content.bgcolor = ( - theme.colors.calculated_bg - if self.is_calculated and not self.is_editing - else None - ) + def update_visibility(self): + self.display_content.visible = not self.is_editing + self.input_content.visible = self.is_editing + self.calculated_icon.visible = self.is_calculated and not self.is_editing + self.reset_button.visible = self.can_reset and self.is_editing - def set_calculated(self, is_calculated): - self.is_calculated = is_calculated - self.update_bg() - self.control.update() + def on_reset_to_calculated(self): + pass @abstractmethod def update_display(self): diff --git a/source/package/pathways_app/pathways_app/controls/metric_value.py b/source/package/pathways_app/pathways_app/controls/metric_value.py index 6e88822..fe4e821 100644 --- a/source/package/pathways_app/pathways_app/controls/metric_value.py +++ b/source/package/pathways_app/pathways_app/controls/metric_value.py @@ -1,6 +1,6 @@ import flet as ft -from adaptation_pathways.app.model.metric import Metric, MetricValue +from adaptation_pathways.app.model.metric import Metric, MetricValue, MetricValueState from .. import theme from .editable_cell import EditableCell @@ -17,17 +17,11 @@ class MetricValueCell(EditableCell): def __init__(self, metric: Metric, value: MetricValue, on_finished_editing=None): self.metric = metric self.value = value + self.finished_editing_callback = on_finished_editing self.display_content = ft.Text("") self.update_display() - def on_edited(_): - self.value.value = float(self.input_content.value) - self.value.is_estimate = False - self.set_calculated(False) - if on_finished_editing is not None: - on_finished_editing(self) - self.input_content = ft.TextField( dense=True, enable_suggestions=False, @@ -52,9 +46,25 @@ def on_edited(_): self.display_content, self.input_content, is_calculated=value.is_estimate, - on_finished_editing=on_edited, + can_reset=value.state == MetricValueState.OVERRIDE, + on_finished_editing=self.on_edited, + alignment=ft.alignment.center_right, ) + def on_edited(self): + new_value = float(self.input_content.value) + print(new_value) + if new_value != self.value.value and self.value.is_estimate: + self.value.state = MetricValueState.OVERRIDE + self.value.value = new_value + + if self.finished_editing_callback is not None: + self.finished_editing_callback(self) + + def on_reset_to_calculated(self): + self.value.state = MetricValueState.ESTIMATE + self.on_edited() + def update_display(self): self.display_content.value = self.metric.unit.format(self.value.value) diff --git a/source/package/pathways_app/pathways_app/controls/scenarios_panel.py b/source/package/pathways_app/pathways_app/controls/scenarios_panel.py index 0aa756b..773e42c 100644 --- a/source/package/pathways_app/pathways_app/controls/scenarios_panel.py +++ b/source/package/pathways_app/pathways_app/controls/scenarios_panel.py @@ -53,7 +53,7 @@ def __init__(self, project: PathwaysProject): TableCell(ft.Text(year)), *( TableCell( - ft.Text(data or "None"), is_calculated=data is None + ft.Text(data or "None") ) for data in [ current_scenario.get_data(year, metric) diff --git a/source/package/pathways_app/pathways_app/controls/sortable_header.py b/source/package/pathways_app/pathways_app/controls/sortable_header.py index 486425f..6a0044b 100644 --- a/source/package/pathways_app/pathways_app/controls/sortable_header.py +++ b/source/package/pathways_app/pathways_app/controls/sortable_header.py @@ -35,6 +35,7 @@ def __init__( expand: bool | int | None = True, col: int | None = None, bgcolor: str | None = None, + width: int | None = None, height: int | None = None, ): self.sort_key = sort_key @@ -55,6 +56,7 @@ def __init__( ), padding=theme.variables.table_cell_padding, bgcolor=bgcolor, + width=width, height=height, ) diff --git a/source/package/pathways_app/pathways_app/controls/styled_table.py b/source/package/pathways_app/pathways_app/controls/styled_table.py index 82b9267..bbb0473 100644 --- a/source/package/pathways_app/pathways_app/controls/styled_table.py +++ b/source/package/pathways_app/pathways_app/controls/styled_table.py @@ -13,7 +13,8 @@ def __init__( key: str | None = None, on_sort: Callable[[None], None] | None = None, expand: bool | int | None = True, - alignment: ft.Alignment | None = None, + width: int | None = None, + alignment: ft.Alignment | None = ft.alignment.center_left, ): self.label = label @@ -21,12 +22,17 @@ def __init__( self.expand = expand self.key = label if key is None else key self.alignment = alignment + self.width = width class TableCell: - def __init__(self, control: ft.Control, is_calculated=False): + def __init__( + self, + control: ft.Control, + sort_value: int | float | None = None, + ): self.control = control - self.is_calculated = is_calculated + self.sort_value = sort_value class StyledTable(ft.Container): @@ -117,7 +123,6 @@ def on_header_sorted(self, column_index: int, header: SortableHeader): self.on_sort(column_index) def on_row_selected(self, index: int): - print(index) if index in self.selected_row_indices: self.selected_row_indices.remove(index) else: @@ -164,6 +169,7 @@ def set_columns(self, columns: list[TableColumn]): ), expand=column.expand, bgcolor=theme.colors.primary_lightest, + width=column.width, height=self.row_height, ) ) @@ -219,18 +225,9 @@ def set_rows(self, rows: list[list[TableCell]]): def _create_row(self, index: int, cell: TableCell) -> ft.Container: column = self.column_data[index] return ft.Container( - content=ft.Column( - [ - ft.Container(cell.control, expand=True, padding=10), - ], - expand=True, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - # alignment=ft.MainAxisAlignment.END, - # theme.variables.table_cell_padding, - # alignment=self.column_data[index], - ), - bgcolor=(theme.colors.calculated_bg if cell.is_calculated else None), + content=cell.control, expand=column.expand, height=self.row_height, - # padding=theme.variables.table_cell_padding, + width=column.width, + padding=None, ) diff --git a/source/package/pathways_app/pathways_app/example.py b/source/package/pathways_app/pathways_app/example.py index 02d362a..e3e725d 100644 --- a/source/package/pathways_app/pathways_app/example.py +++ b/source/package/pathways_app/pathways_app/example.py @@ -11,21 +11,18 @@ "sea-level-rise", name="Sea Level Rise", unit_or_default="cm", - current_value=10, ) metric_cost = Metric( "cost", name="Cost", unit_or_default="€", - current_value=0, ) metric_habitat_health = Metric( "habitat", name="Habitat Health", unit_or_default="Impact", - current_value=0, ) action_root = Action( diff --git a/source/package/pathways_app/pathways_app/theme.py b/source/package/pathways_app/pathways_app/theme.py index 8d7c593..4aeb32b 100644 --- a/source/package/pathways_app/pathways_app/theme.py +++ b/source/package/pathways_app/pathways_app/theme.py @@ -13,7 +13,8 @@ class DefaultThemeColors: primary_darker = "#160E59" secondary_light = "#91E0EC" secondary_medium = "#48BDCF" - calculated_bg = "#10000000" + calculated_bg = "#208888AA" + calculated_icon = "#8888AA" colors = DefaultThemeColors() @@ -72,6 +73,7 @@ class DefaultThemeVariables: panel_spacing = 6 panel_padding = 10 table_cell_padding = ft.padding.symmetric(4, 8) + calculated_icon_size = 16 variables = DefaultThemeVariables() From d36c6c00818532041d6c1525d01a86bae7504f7b Mon Sep 17 00:00:00 2001 From: ajkolenc Date: Fri, 10 Jan 2025 17:34:53 +0100 Subject: [PATCH 03/12] Remove incorrect imports --- environment/configuration/requirements.txt | 2 ++ .../package/pathways_app/pathways_app/controls/editable_cell.py | 2 +- .../package/pathways_app/pathways_app/controls/styled_table.py | 2 +- source/package/pathways_app/pathways_app/controls/unit_cell.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/environment/configuration/requirements.txt b/environment/configuration/requirements.txt index 8286872..9e272db 100644 --- a/environment/configuration/requirements.txt +++ b/environment/configuration/requirements.txt @@ -1,3 +1,5 @@ docopt matplotlib networkx[default] +pyside6>=6.6 +flet==0.24.* diff --git a/source/package/pathways_app/pathways_app/controls/editable_cell.py b/source/package/pathways_app/pathways_app/controls/editable_cell.py index 0abc49d..5f2709a 100644 --- a/source/package/pathways_app/pathways_app/controls/editable_cell.py +++ b/source/package/pathways_app/pathways_app/controls/editable_cell.py @@ -3,7 +3,7 @@ import flet as ft import theme -from pathways_app.controls.styled_table import TableCell +from .styled_table import TableCell from pyparsing import abstractmethod from .. import theme diff --git a/source/package/pathways_app/pathways_app/controls/styled_table.py b/source/package/pathways_app/pathways_app/controls/styled_table.py index bbb0473..79d6d8f 100644 --- a/source/package/pathways_app/pathways_app/controls/styled_table.py +++ b/source/package/pathways_app/pathways_app/controls/styled_table.py @@ -3,7 +3,7 @@ import flet as ft import theme -from pathways_app.controls.sortable_header import SortableHeader, SortMode +from .sortable_header import SortableHeader, SortMode class TableColumn: diff --git a/source/package/pathways_app/pathways_app/controls/unit_cell.py b/source/package/pathways_app/pathways_app/controls/unit_cell.py index 7e409f5..dc5cdff 100644 --- a/source/package/pathways_app/pathways_app/controls/unit_cell.py +++ b/source/package/pathways_app/pathways_app/controls/unit_cell.py @@ -2,7 +2,7 @@ import flet as ft import theme -from pathways_app.controls.styled_table import TableCell +from .styled_table import TableCell from adaptation_pathways.app.model.metric import Metric, MetricUnit, default_units From f7ae3746f8712836e06ff5c00f7aaf574e53f45b Mon Sep 17 00:00:00 2001 From: ajkolenc Date: Mon, 13 Jan 2025 22:33:44 +0100 Subject: [PATCH 04/12] Start fixing calculation reset --- .../pathways_app/controls/editable_cell.py | 14 ++++++++++---- .../pathways_app/controls/metric_value.py | 9 +++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/source/package/pathways_app/pathways_app/controls/editable_cell.py b/source/package/pathways_app/pathways_app/controls/editable_cell.py index 5f2709a..13d39e2 100644 --- a/source/package/pathways_app/pathways_app/controls/editable_cell.py +++ b/source/package/pathways_app/pathways_app/controls/editable_cell.py @@ -64,14 +64,20 @@ def __init__( ), padding=padding, bgcolor=theme.colors.calculated_bg if self.is_calculated else None, - on_click=self.toggle_editing, + on_click=self.set_editing, ) super().__init__(control=self.cell_content) - def toggle_editing(self, _): - self.is_editing = not self.is_editing + def set_editing(self, _): + self.is_editing = True + self.update_controls() + def set_not_editing(self, _): + self.is_editing = False + self.update_controls() + + def update_controls(self): if self.is_editing: self.update_input() else: @@ -124,7 +130,7 @@ def __init__(self, source: object, value_attribute: str, on_finished_editing=Non suffix_style=theme.text.textfield_symbol, expand=True, content_padding=ft.padding.symmetric(4, 6), - on_blur=self.toggle_editing, + on_blur=self.set_not_editing, ) def on_finished_editing_internal(_): diff --git a/source/package/pathways_app/pathways_app/controls/metric_value.py b/source/package/pathways_app/pathways_app/controls/metric_value.py index fe4e821..812989d 100644 --- a/source/package/pathways_app/pathways_app/controls/metric_value.py +++ b/source/package/pathways_app/pathways_app/controls/metric_value.py @@ -38,7 +38,7 @@ def __init__(self, metric: Metric, value: MetricValue, on_finished_editing=None) text_align=ft.TextAlign.RIGHT, expand=True, content_padding=ft.padding.symmetric(4, 6), - on_blur=self.toggle_editing, + on_blur=self.set_not_editing, ) self.update_input() @@ -51,9 +51,9 @@ def __init__(self, metric: Metric, value: MetricValue, on_finished_editing=None) alignment=ft.alignment.center_right, ) - def on_edited(self): + def on_edited(self, _): new_value = float(self.input_content.value) - print(new_value) + if new_value != self.value.value and self.value.is_estimate: self.value.state = MetricValueState.OVERRIDE self.value.value = new_value @@ -63,7 +63,8 @@ def on_edited(self): def on_reset_to_calculated(self): self.value.state = MetricValueState.ESTIMATE - self.on_edited() + self.set_not_editing() + print(self.value) def update_display(self): self.display_content.value = self.metric.unit.format(self.value.value) From 13fefb5fc2cecc52e840431dd013c6d5bf426a9d Mon Sep 17 00:00:00 2001 From: AJ Kolenc Date: Sun, 26 Jan 2025 15:39:58 +0100 Subject: [PATCH 05/12] Refactored and functionalized scenarios --- environment/configuration/requirements.txt | 2 +- .../adaptation_pathways/app/model/__init__.py | 2 +- .../app/model/pathways_project.py | 252 +++++------ .../adaptation_pathways/app/model/scenario.py | 221 ++++++++- .../adaptation_pathways/app/model/sorting.py | 16 - .../app/service/plotting_service.py | 28 +- source/package/pathways_app/app.py | 26 ++ source/package/pathways_app/config.py | 4 + source/package/pathways_app/main.py | 115 +---- .../pathways_app/controls/action_icon.py | 2 +- .../pathways_app/controls/actions_editor.py | 205 +++++++++ .../pathways_app/controls/actions_panel.py | 88 ++-- .../pathways_app/controls/editable_cell.py | 101 ++++- .../pathways_app/controls/editor_page.py | 166 +++++++ .../pathways_app/controls/graph_editor.py | 253 +++++++++++ .../pathways_app/controls/graph_panel.py | 96 ---- .../pathways_app/controls/header.py | 33 +- .../pathways_app/controls/input_filters.py | 13 + .../pathways_app/controls/menu_bar.py | 74 ++- .../pathways_app/controls/metric_effect.py | 3 + .../pathways_app/controls/metric_value.py | 22 +- .../pathways_app/controls/metrics_editor.py | 159 +++++++ .../pathways_app/controls/metrics_panel.py | 94 ++-- .../pathways_app/controls/panel_header.py | 42 ++ .../pathways_app/controls/pathways_editor.py | 174 ++++++++ .../pathways_app/controls/pathways_panel.py | 126 ++---- .../pathways_app/controls/scenarios_editor.py | 219 +++++++++ .../pathways_app/controls/scenarios_panel.py | 70 --- .../pathways_app/controls/sortable_header.py | 21 +- .../pathways_app/controls/styled_dropdown.py | 75 ++-- .../pathways_app/controls/styled_table.py | 420 ++++++++++++------ .../pathways_app/controls/tabbed_panel.py | 115 +++-- .../pathways_app/controls/unit_cell.py | 302 ++++++------- .../pathways_app/pathways_app/example.py | 34 +- .../pathways_app/pathways_app/theme.py | 121 +++-- .../pathways_app/pathways_app/utils.py | 7 +- 36 files changed, 2543 insertions(+), 1158 deletions(-) create mode 100644 source/package/pathways_app/app.py create mode 100644 source/package/pathways_app/config.py create mode 100644 source/package/pathways_app/pathways_app/controls/actions_editor.py create mode 100644 source/package/pathways_app/pathways_app/controls/editor_page.py create mode 100644 source/package/pathways_app/pathways_app/controls/graph_editor.py delete mode 100644 source/package/pathways_app/pathways_app/controls/graph_panel.py create mode 100644 source/package/pathways_app/pathways_app/controls/input_filters.py create mode 100644 source/package/pathways_app/pathways_app/controls/metrics_editor.py create mode 100644 source/package/pathways_app/pathways_app/controls/panel_header.py create mode 100644 source/package/pathways_app/pathways_app/controls/pathways_editor.py create mode 100644 source/package/pathways_app/pathways_app/controls/scenarios_editor.py delete mode 100644 source/package/pathways_app/pathways_app/controls/scenarios_panel.py diff --git a/environment/configuration/requirements.txt b/environment/configuration/requirements.txt index 9e272db..c20283a 100644 --- a/environment/configuration/requirements.txt +++ b/environment/configuration/requirements.txt @@ -2,4 +2,4 @@ docopt matplotlib networkx[default] pyside6>=6.6 -flet==0.24.* +flet==0.25.* diff --git a/source/package/adaptation_pathways/app/model/__init__.py b/source/package/adaptation_pathways/app/model/__init__.py index 533863c..e30d535 100644 --- a/source/package/adaptation_pathways/app/model/__init__.py +++ b/source/package/adaptation_pathways/app/model/__init__.py @@ -6,4 +6,4 @@ from .metric import Metric from .pathway import Pathway from .pathways_project import PathwaysProject -from .scenario import Scenario, TimeSeriesPoint +from .scenario import Scenario diff --git a/source/package/adaptation_pathways/app/model/pathways_project.py b/source/package/adaptation_pathways/app/model/pathways_project.py index 99db66a..1dff851 100644 --- a/source/package/adaptation_pathways/app/model/pathways_project.py +++ b/source/package/adaptation_pathways/app/model/pathways_project.py @@ -4,7 +4,7 @@ """ from typing import Callable, Iterable -from adaptation_pathways.app.model.sorting import SortingInfo, SortTarget +from adaptation_pathways.app.model.sorting import SortingInfo from .action import Action from .metric import Metric, MetricEffect, MetricOperation, MetricValue, MetricValueState @@ -35,12 +35,13 @@ def __init__( self.end_year = end_year self._current_id = 0 - self.metrics_by_id: dict[str, Metric] = {} + self.conditions_by_id: dict[str, Metric] = {} for metric in conditions: - self.metrics_by_id[metric.id] = metric + self.conditions_by_id[metric.id] = metric + self.criteria_by_id: dict[str, Metric] = {} for metric in criteria: - self.metrics_by_id[metric.id] = metric + self.criteria_by_id[metric.id] = metric self.scenarios_by_id: dict[str, Scenario] = {} for scenario in scenarios: @@ -55,19 +56,30 @@ def __init__( for pathway in pathways: self.pathways_by_id[pathway.id] = pathway - self.condition_sorting = SortingInfo([metric.id for metric in conditions]) - self.criteria_sorting = SortingInfo([metric.id for metric in criteria]) - self.scenario_sorting = SortingInfo([scenario.id for scenario in scenarios]) - self.action_sorting = SortingInfo([action.id for action in actions]) - self.pathway_sorting = SortingInfo([pathway.id for pathway in pathways]) + self.condition_sorting = SortingInfo() + self.criteria_sorting = SortingInfo() + self.scenario_sorting = SortingInfo() + self.action_sorting = SortingInfo() + self.pathway_sorting = SortingInfo() self.root_pathway_id = root_pathway_id self.selected_condition_ids: set[str] = set() self.selected_criteria_ids: set[str] = set() self.selected_action_ids: set[str] = set() self.selected_pathway_ids: set[str] = set() - self.selected_scenario_id: str = "" if len(scenarios) == 0 else scenarios[0].id - self.graph_metric_id: str = conditions[0].id + self.selected_scenario_ids: set[str] = set() + + self.values_scenario_id: str | None = ( + None if len(scenarios) == 0 else scenarios[0].id + ) + + self.graph_metric_id: str | None = ( + conditions[0].id if len(conditions) > 0 else None + ) + self.graph_is_time: bool = self.graph_metric_id is not None + self.graph_scenario_id: str | None = ( + scenarios[0].id if len(scenarios) > 0 else None + ) self.on_conditions_changed: list[Callable[[], None]] = [] self.on_criteria_changed: list[Callable[[], None]] = [] @@ -76,6 +88,9 @@ def __init__( self.on_action_color_changed: list[Callable[[], None]] = [] self.on_pathways_changed: list[Callable[[], None]] = [] + for metric in self.all_metrics(): + self.update_pathway_values(metric.id) + def __hash__(self): return self.id.__hash__() @@ -104,104 +119,124 @@ def notify_pathways_changed(self): listener() @property - def sorted_actions(self): - return ( - self.get_action(action_id) for action_id in self.action_sorting.sorted_ids - ) + def all_actions(self): + return self.actions_by_id.values() @property - def sorted_conditions(self): - return ( - self.get_metric(metric_id) - for metric_id in self.condition_sorting.sorted_ids - ) + def all_conditions(self): + return self.conditions_by_id.values() @property - def sorted_criteria(self): - return ( - self.get_metric(metric_id) for metric_id in self.criteria_sorting.sorted_ids - ) + def all_criteria(self): + return self.criteria_by_id.values() @property - def sorted_scenarios(self): - return ( - self.get_scenario(scenario_id) - for scenario_id in self.scenario_sorting.sorted_ids - ) + def all_scenarios(self): + return self.scenarios_by_id.values() @property - def sorted_pathways(self): - return ( - self.get_pathway(pathway_id) - for pathway_id in self.pathway_sorting.sorted_ids - ) + def all_pathways(self): + return self.pathways_by_id.values() @property def root_pathway(self): return self.get_pathway(self.root_pathway_id) + @property + def values_scenario(self): + return self.get_scenario(self.values_scenario_id) + @property def graph_metric(self): return self.get_metric(self.graph_metric_id) + @property + def graph_scenario(self): + return self.get_scenario(self.graph_scenario_id) + def _create_id(self) -> str: self._current_id += 1 return str(self._current_id) def get_metric(self, metric_id: str) -> Metric | None: - return self.metrics_by_id.get(metric_id, None) + metric = self.conditions_by_id.get(metric_id, None) + if metric is None: + metric = self.criteria_by_id.get(metric_id, None) + return metric def all_metrics(self): - for metric_id in self.condition_sorting.sorted_ids: - yield self.get_metric(metric_id) - for metric_id in self.criteria_sorting.sorted_ids: - yield self.get_metric(metric_id) + yield from self.all_conditions + yield from self.all_criteria - def _create_metric(self, name: str) -> Metric: + def _create_metric(self, name: str, metrics_by_id: dict[str, Metric]) -> Metric: metric_id = self._create_id() - metric = Metric(metric_id, name, 0, "") - self.metrics_by_id[metric.id] = metric - for action in self.sorted_actions: + metric = Metric(metric_id, name, "") + metrics_by_id[metric_id] = metric + + for action in self.all_actions: action.metric_data[metric_id] = MetricEffect(0, MetricOperation.ADD) + + self.update_pathway_values(metric.id) return metric def create_condition(self) -> Metric: - metric = self._create_metric("New Condition") - self.condition_sorting.sorted_ids.append(metric.id) + metric = self._create_metric("New Condition", self.conditions_by_id) return metric def create_criteria(self) -> Metric: - metric = self._create_metric("New Criteria") - self.criteria_sorting.sorted_ids.append(metric.id) + metric = self._create_metric("New Criteria", self.criteria_by_id) return metric def delete_condition(self, metric_id: str) -> Metric | None: - metric = self.metrics_by_id.pop(metric_id) - self.condition_sorting.sorted_ids.remove(metric_id) + metric = self.conditions_by_id.pop(metric_id) self.selected_condition_ids.remove(metric_id) return metric def delete_criteria(self, metric_id: str) -> Metric | None: - metric = self.metrics_by_id.pop(metric_id) - self.criteria_sorting.sorted_ids.remove(metric_id) + metric = self.criteria_by_id.pop(metric_id) self.selected_criteria_ids.remove(metric_id) return metric def get_scenario(self, scenario_id: str) -> Scenario | None: return self.scenarios_by_id.get(scenario_id, None) - def create_scenario(self) -> Scenario: + def create_scenario(self, name: str) -> Scenario: scenario_id = self._create_id() - scenario = Scenario(scenario_id, "New Scenario", {}) + scenario = Scenario(scenario_id, name) self.scenarios_by_id[scenario.id] = scenario - self.scenario_sorting.sorted_ids.append(scenario.id) + if self.graph_scenario_id is None: + self.graph_scenario_id = scenario_id return scenario + def copy_scenario(self, scenario_id: str, suffix=" (Copy)") -> Scenario | None: + to_copy = self.get_scenario(scenario_id) + if to_copy is None: + return None + + new_scenario = self.create_scenario(f"{to_copy.name}{suffix}") + for year_data in to_copy.yearly_data: + new_data = new_scenario.get_or_add_year(year_data.year) + for metric_id, metric_data in year_data.metric_data.items(): + new_data.metric_data[metric_id] = MetricValue( + metric_data.value, metric_data.state + ) + + return new_scenario + def delete_scenario(self, scenario_id: str) -> Scenario | None: scenario = self.scenarios_by_id.pop(scenario_id) - self.scenario_sorting.sorted_ids.remove(scenario_id) + if self.graph_scenario_id is scenario_id: + self.graph_scenario_id = next(self.all_scenarios, None) return scenario + def delete_scenarios(self, scenario_ids: Iterable[str]): + for scenario_id in scenario_ids: + self.delete_scenario(scenario_id) + + def update_scenario_values(self, metric_id: str): + for scenario in self.all_scenarios: + scenario.recalculate_values(metric_id) + def get_action(self, action_id: str) -> Action: return self.actions_by_id[action_id] @@ -219,60 +254,23 @@ def create_action(self, color, icon) -> Action: ) self.actions_by_id[action.id] = action - self.action_sorting.sorted_ids.append(action.id) return action def delete_action(self, action_id: str) -> Action | None: action = self.actions_by_id.pop(action_id) - self.action_sorting.sorted_ids.remove(action_id) return action - def delete_selected_actions(self): + def delete_actions(self, action_ids: Iterable[str]): pathway_ids_to_delete: list[str] = [] - for action_id in self.selected_action_ids: + for action_id in action_ids: self.delete_action(action_id) - for pathway in self.sorted_pathways: + for pathway in self.all_pathways: if pathway.action_id == action_id: pathway_ids_to_delete.append(pathway.id) - self.selected_action_ids.clear() self.delete_pathways(pathway_ids_to_delete) - def sort_actions(self): - if self.action_sorting.target is SortTarget.METRIC: - sorting_metric = self.get_metric(self.action_sorting.sort_key) - - if sorting_metric is not None: - - def sort_by_metric(action_id: str): - action = self.get_action(action_id) - if action is None: - return 0 - data = action.metric_data.get(sorting_metric.id, None) - return data.value if data is not None else 0 - - self.action_sorting.sorted_ids.sort( - key=sort_by_metric, - reverse=not self.action_sorting.ascending, - ) - - elif self.action_sorting.target is SortTarget.ATTRIBUTE: - - def sort_by_attr(action_id: str): - if self.action_sorting.sort_key is None: - return "" - action = self.get_action(action_id) - return getattr(action, self.action_sorting.sort_key, "") - - self.action_sorting.sorted_ids.sort( - key=sort_by_attr, reverse=not self.action_sorting.ascending - ) - - else: - self.action_sorting.ascending = True - self.action_sorting.sort_by_id() - def get_pathway(self, pathway_id: str) -> Pathway | None: if pathway_id is None: return None @@ -283,7 +281,6 @@ def create_pathway( ) -> Pathway: pathway = Pathway(action_id, parent_pathway_id) self.pathways_by_id[pathway.id] = pathway - self.pathway_sorting.sorted_ids.append(pathway.id) for metric in self.all_metrics(): self.update_pathway_values(metric.id) @@ -295,14 +292,16 @@ def update_pathway_values(self, metric_id: str): return updated_pathways: set[str] = set() - for pathway in self.sorted_pathways: + for pathway in self.all_pathways: self._update_pathway_value(pathway, metric, updated_pathways) def _update_pathway_value( self, pathway: Pathway, metric: Metric, updated_pathway_ids: set[str] ): pathway_action = self.get_action(pathway.action_id) - parent = self.get_pathway(pathway.parent_id) + parent = ( + None if pathway.parent_id is None else self.get_pathway(pathway.parent_id) + ) current_value = pathway.metric_data.get(metric.id, None) # Initialize the value if there was none @@ -322,12 +321,17 @@ def _update_pathway_value( updated_pathway_ids.add(pathway.id) return - if parent.id not in updated_pathway_ids and parent_value.is_estimate: - self._update_pathway_value(parent, metric, updated_pathway_ids) - - base_value = 0 + base_value: float = 0 if parent is not None: parent_value = parent.metric_data.get(metric.id, None) + + if ( + parent.id not in updated_pathway_ids + and parent_value is not None + and parent_value.is_estimate + ): + self._update_pathway_value(parent, metric, updated_pathway_ids) + if parent_value is not None: base_value = parent_value.value @@ -336,15 +340,15 @@ def _update_pathway_value( def delete_pathway(self, pathway_id: str) -> Pathway | None: pathway = self.pathways_by_id.pop(pathway_id, None) - self.pathway_sorting.sorted_ids.remove(pathway_id) return pathway def delete_pathways(self, pathway_ids: Iterable[str]): ids_to_delete: set[str] = set() ids_to_delete.update(pathway_ids) + print(ids_to_delete) # Delete any orphaned children - for pathway in self.sorted_pathways: + for pathway in self.all_pathways: if pathway.id in ids_to_delete: continue @@ -353,52 +357,16 @@ def delete_pathways(self, pathway_ids: Iterable[str]): ids_to_delete.add(pathway.id) for pathway_id in ids_to_delete: + print(pathway_id) self.delete_pathway(pathway_id) def delete_selected_pathways(self): self.delete_pathways(self.selected_pathway_ids) self.selected_pathway_ids.clear() - def sort_pathways(self): - if self.pathway_sorting.target is SortTarget.METRIC: - sorting_metric = self.get_metric(self.pathway_sorting.sort_key) - - if sorting_metric is not None: - - def sort_by_metric(pathway_id: str): - pathway = self.get_pathway(pathway_id) - if pathway is None: - return 0 - - data = pathway.metric_data.get(sorting_metric.id, None) - return data.value if data is not None else 0 - - self.pathway_sorting.sorted_ids.sort( - key=sort_by_metric, - reverse=not self.pathway_sorting.ascending, - ) - - elif self.pathway_sorting.target is SortTarget.ATTRIBUTE: - - def sort_by_attr(pathway_id: str): - if self.pathway_sorting.sort_key is None: - return "" - pathway = self.get_pathway(pathway_id) - return getattr(pathway, self.pathway_sorting.sort_key, "") - - self.pathway_sorting.sorted_ids.sort( - key=sort_by_attr, reverse=not self.pathway_sorting.ascending - ) - - else: - self.pathway_sorting.ascending = True - self.pathway_sorting.sort_by_id() - def get_children(self, pathway_id: str): return ( - pathway - for pathway in self.sorted_pathways - if pathway.parent_id == pathway_id + pathway for pathway in self.all_pathways if pathway.parent_id == pathway_id ) def get_ancestors(self, pathway: Pathway): diff --git a/source/package/adaptation_pathways/app/model/scenario.py b/source/package/adaptation_pathways/app/model/scenario.py index 5f3f502..6de1b79 100644 --- a/source/package/adaptation_pathways/app/model/scenario.py +++ b/source/package/adaptation_pathways/app/model/scenario.py @@ -1,26 +1,213 @@ -import dataclasses +# pylint: disable=too-many-return-statements,too-many-branches +from .metric import MetricValue, MetricValueState -from .metric import MetricValue +class YearDataPoint: + def __init__(self, year: int): + self.year = year + self.metric_data: dict[str, MetricValue] = {} -@dataclasses.dataclass -class TimeSeriesPoint: - time: float - data: MetricValue | None + def get_or_add_data(self, metric_id: str) -> MetricValue: + data = self.metric_data.get(metric_id, None) + if data is None: + data = MetricValue(0, MetricValueState.ESTIMATE) + self.metric_data[metric_id] = data + return data -@dataclasses.dataclass class Scenario: - id: str - name: str - metric_data_over_time: dict[int, dict[str, MetricValue]] + def __init__(self, scenario_id: str, name: str): + self.id = scenario_id + self.name = name + self.yearly_data: list[YearDataPoint] = [] - def get_data(self, year: int, metric_id: str) -> MetricValue | None: - if year not in self.metric_data_over_time: - return None + def get_or_add_year(self, year: int) -> YearDataPoint: + data = self.get_data(year) + if data is None: + data = YearDataPoint(year) + self.yearly_data.append(data) + self.sort_yearly_data() - year_data = self.metric_data_over_time[year] - if metric_id not in year_data: - return None + return data - return year_data[metric_id] + def get_data(self, year: int) -> YearDataPoint | None: + for data_point in self.yearly_data: + if data_point.year == year: + return data_point + return None + + def set_data(self, year: int, metric_id: str, value: MetricValue): + data = self.get_or_add_year(year) + data.metric_data[metric_id] = value + + def sort_yearly_data(self): + self.yearly_data.sort(key=lambda point: point.year) + + def recalculate_values(self, metric_id: str): + for index, data in enumerate(self.yearly_data): + metric_value = data.get_or_add_data(metric_id) + + # We only recalculate estimated values + if not metric_value.is_estimate: + continue + + previous_point = self._get_previous_value(index, metric_id) + next_point = self._get_next_value(index, metric_id) + + # If we don't have any data to interpolate, we can't recalculate + if previous_point is None and next_point is None: + continue + + # If we have both a previous and next point, we can interpolate + if previous_point is not None and next_point is not None: + metric_value.value = self._estimate_value( + data.year, + previous_point[0], + previous_point[1].value, + next_point[0], + next_point[1].value, + ) + continue + + # If we don't have a previous point, try extrapolating with the next two points + if next_point is not None: + # Check if we have a second data point + second_next_point = self._get_next_value(next_point[2], metric_id) + + # If we don't have a second next data point, we have to use the next one as is + if second_next_point is None: + metric_value.value = next_point[1].value + continue + + # Otherwise we can extrapolate + metric_value.value = self._estimate_value( + data.year, + next_point[0], + next_point[1].value, + second_next_point[0], + second_next_point[1].value, + ) + continue + + # If we don't have a next point, try extrapolating with the previous two points + if previous_point is not None: + # Check if we have a second data point + second_previous_point = self._get_previous_value( + previous_point[2], metric_id + ) + + # If we don't have a second next data point, we have to use the next one as is + if second_previous_point is None: + metric_value.value = previous_point[1].value + continue + # Otherwise we can extrapolate + metric_value.value = self._estimate_value( + data.year, + second_previous_point[0], + second_previous_point[1].value, + previous_point[0], + previous_point[1].value, + ) + + def _get_previous_value( + self, year_index: int, metric_id: str + ) -> tuple[int, MetricValue, int] | None: + for index in range(year_index - 1, -1, -1): + data = self.yearly_data[index] + metric_value = data.metric_data[metric_id] + if not metric_value.is_estimate: + return (data.year, metric_value, index) + return None + + def _get_next_value( + self, year_index: int, metric_id: str + ) -> tuple[int, MetricValue, int] | None: + for index in range(year_index + 1, len(self.yearly_data)): + data = self.yearly_data[index] + metric_value = data.metric_data[metric_id] + if not metric_value.is_estimate: + return (data.year, metric_value, index) + + return None + + def _estimate_value( + self, x: float, x_1: float, y_1: float, x_2: float, y_2: float + ) -> float: + slope = (y_2 - y_1) / (x_2 - x_1) + return slope * (x - x_1) + y_1 + + def estimate_tipping_point(self, metric_id: str, metric_value: float) -> float: + if len(self.yearly_data) == 0: + return 0 + + if len(self.yearly_data) == 1: + return self.yearly_data[0].year + + # Find the global min and max to establish the bounds + global_min: tuple[float, float] = (0, 0) + has_global_min = False + global_max: tuple[float, float] = (0, 0) + has_global_max = False + + for year_data in self.yearly_data: + year_value = year_data.metric_data.get(metric_id, None) + if year_value is None: + continue + + if not has_global_min or year_value.value < global_min[1]: + has_global_min = True + global_min = (year_data.year, year_value.value) + + if not has_global_max or year_value.value > global_max[1]: + has_global_max = True + global_max = (year_data.year, year_value.value) + + # That means we don't have any valid data points for this metric + if not has_global_min or not has_global_max: + return 0 + + if metric_value <= global_min[1]: + return global_min[0] + + if metric_value >= global_max[1]: + return global_max[0] + + for index, year_data in enumerate(self.yearly_data): + if index + 1 >= len(self.yearly_data): + continue + + year_value = year_data.metric_data.get(metric_id, None) + if year_value is None: + continue + + next_year_data = self.yearly_data[index + 1] + next_year_value = next_year_data.metric_data.get(metric_id, None) + + if next_year_value is None: + continue + + min_year, min_value, max_year, max_value = ( + ( + year_data.year, + year_value.value, + next_year_data.year, + next_year_value.value, + ) + if year_value.value <= next_year_value.value + else ( + next_year_data.year, + next_year_value.value, + year_data.year, + year_value.value, + ) + ) + + if metric_value < min_value or metric_value > max_value: + continue + + return self._estimate_value( + metric_value, min_value, float(min_year), max_value, float(max_year) + ) + + # We should never get here, but just in case + return 0 diff --git a/source/package/adaptation_pathways/app/model/sorting.py b/source/package/adaptation_pathways/app/model/sorting.py index 0bd26bc..a3467f3 100644 --- a/source/package/adaptation_pathways/app/model/sorting.py +++ b/source/package/adaptation_pathways/app/model/sorting.py @@ -1,24 +1,8 @@ -from enum import Enum - - -class SortTarget(Enum): - NONE = 0 - ATTRIBUTE = 1 - METRIC = 2 - - class SortingInfo: def __init__( self, - ids: list[str], - target=SortTarget.NONE, sort_key: str | None = None, ascending=True, ): - self.sorted_ids = ids - self.target = target self.sort_key = sort_key self.ascending = ascending - - def sort_by_id(self): - self.sorted_ids.sort(reverse=not self.ascending) diff --git a/source/package/adaptation_pathways/app/service/plotting_service.py b/source/package/adaptation_pathways/app/service/plotting_service.py index 19a69c1..f32ef9a 100644 --- a/source/package/adaptation_pathways/app/service/plotting_service.py +++ b/source/package/adaptation_pathways/app/service/plotting_service.py @@ -11,7 +11,9 @@ from adaptation_pathways.action import Action from adaptation_pathways.alias import Sequence, TippingPointByAction +from adaptation_pathways.app.model.pathway import Pathway from adaptation_pathways.app.model.pathways_project import PathwaysProject +from adaptation_pathways.app.model.scenario import Scenario from adaptation_pathways.graph.convert import sequence_graph_to_pathway_map from adaptation_pathways.graph.node.action import Action as ActionNode from adaptation_pathways.graph.sequence_graph import SequenceGraph @@ -31,25 +33,39 @@ def draw_metro_map( action_nodes: dict[str, ActionNode] = {} tipping_points: TippingPointByAction = {} action_colors: dict[str, str] = {} + root_pathway = project.root_pathway metric = project.graph_metric + scenario: Scenario | None = ( + None if not project.graph_is_time else project.graph_scenario + ) + + def get_tipping_point(pathway: Pathway) -> float: + metric_value = pathway.metric_data.get(metric.id, None) + if metric_value is None: + metric_value = root_pathway.metric_data.get(metric.id, None) + + if metric_value is None: + return 0 + + if scenario is not None: + return scenario.estimate_tipping_point(metric.id, metric_value.value) + + return metric_value.value # Create action node for each pathway - for pathway in project.sorted_pathways: + for pathway in project.all_pathways: pathway_action = project.get_action(pathway.action_id) action = Action(pathway_action.name) action_node = ActionNode(action) action_nodes[pathway.id] = action_node action_colors[action.name] = pathway_action.color - - metric_value = pathway.metric_data.get(metric.id, None) - value = metric.current_value if metric_value is None else metric_value.value - tipping_points[action] = value + tipping_points[action] = get_tipping_point(pathway) # Populate sequences sequence_graph = SequenceGraph() sequences: list[Sequence] = [] - for pathway in project.sorted_pathways: + for pathway in project.all_pathways: if pathway.parent_id is None: continue diff --git a/source/package/pathways_app/app.py b/source/package/pathways_app/app.py new file mode 100644 index 0000000..eba4081 --- /dev/null +++ b/source/package/pathways_app/app.py @@ -0,0 +1,26 @@ +import flet as ft +from pathways_app import example +from pathways_app.config import Config + + +class App: + def __init__(self, page: ft.Page): + self.page = page + self.project = example.project + self.file_picker = ft.FilePicker(on_result=self.on_file_opened) + + def open_link(self, url: str): + self.page.launch_url(url) + + def open_project(self): + print("Open") + self.file_picker.pick_files( + "Choose a Project File", + file_type=ft.FilePickerFileType.ANY, + allowed_extensions=[Config.project_extension], + allow_multiple=False, + ) + + def on_file_opened(self, event: ft.FilePickerResultEvent): + for file in event.files: + print(file) diff --git a/source/package/pathways_app/config.py b/source/package/pathways_app/config.py new file mode 100644 index 0000000..86310de --- /dev/null +++ b/source/package/pathways_app/config.py @@ -0,0 +1,4 @@ +class Config: + project_extension = "pwproj" + about_url = "https://pathways.deltares.nl/" + github_url = "https://github.com/Deltares-research/PathwaysGenerator/" diff --git a/source/package/pathways_app/main.py b/source/package/pathways_app/main.py index 6863e41..4500eb4 100644 --- a/source/package/pathways_app/main.py +++ b/source/package/pathways_app/main.py @@ -3,29 +3,25 @@ import flet as ft from pathways_app.cli.app import main import theme -from controls.actions_panel import ActionsPanel -from controls.graph_panel import GraphPanel -from controls.header import SectionHeader +from controls.editor_page import EditorPage from controls.menu_bar import MenuBar -from controls.metrics_panel import MetricsPanel -from controls.panel import Panel -from controls.pathways_panel import PathwaysPanel -from controls.scenarios_panel import ScenariosPanel -from controls.tabbed_panel import TabbedPanel +from pathways_app.app import App locale.setlocale(locale.LC_ALL, "") def main(page: ft.Page): + # bitdojo_window could make a custom title bar + # page.window.frameless = True + # page.window.width = 1200 + # page.window.height = 800 page.theme = theme.theme page.theme_mode = ft.ThemeMode.LIGHT - # bitdojo_window could make a custom title bar - # page.window.frameless = True - page.window.width = 1200 - page.window.height = 800 page.window.resizable = True + page.window.alignment = ft.alignment.center + page.window.maximized = True page.title = "Pathways Generator" page.fonts = theme.fonts @@ -33,100 +29,15 @@ def main(page: ft.Page): page.padding = 1 page.spacing = 0 - project = example.project - - page.appbar = MenuBar(project) - metrics_panel = MetricsPanel(project) - metrics_tab = (SectionHeader(ft.icons.TUNE, "Metrics"), metrics_panel) - - actions_panel = ActionsPanel(project) - actions_tab = ( - SectionHeader(ft.icons.CONSTRUCTION_OUTLINED, "Actions"), - actions_panel, - ) - - scenarios_panel = ScenariosPanel(project) - scenarios_tab = ( - SectionHeader(ft.icons.PUBLIC, "Scenarios"), - scenarios_panel, - ) - - graph_panel = GraphPanel(project) - pathways_panel = PathwaysPanel(project) - - def on_metrics_changed(): - metrics_panel.redraw() - scenarios_panel.redraw() - actions_panel.redraw() - pathways_panel.redraw() - graph_panel.redraw() - - def on_scenarios_changed(): - scenarios_panel.redraw() - graph_panel.redraw() - - def on_actions_changed(): - actions_panel.redraw() - pathways_panel.redraw() - graph_panel.redraw() - - def on_pathways_changed(): - pathways_panel.redraw() - graph_panel.redraw() - - def on_action_color_changed(): - pathways_panel.redraw() - graph_panel.redraw() - - # def on_graph_changed(): - # graph_panel.redraw() - - project.on_conditions_changed.append(on_metrics_changed) - project.on_criteria_changed.append(on_metrics_changed) - project.on_scenarios_changed.append(on_scenarios_changed) - project.on_actions_changed.append(on_actions_changed) - project.on_action_color_changed.append(on_action_color_changed) - project.on_pathways_changed.append(on_pathways_changed) + app = App(page) + page.appbar = MenuBar(app) + page.overlay.append(app.file_picker) page.add( ft.Container( expand=True, padding=theme.variables.panel_spacing, - content=ft.Row( - expand=True, - spacing=theme.variables.panel_spacing, - controls=[ - ft.Column( - expand=2, - spacing=theme.variables.panel_spacing, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - TabbedPanel( - selected_index=0, - tabs=[metrics_tab, actions_tab, scenarios_tab], - ) - ], - ), - ft.Column( - expand=3, - spacing=theme.variables.panel_spacing, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - Panel(graph_panel), - Panel( - content=ft.Column( - expand=True, - alignment=ft.MainAxisAlignment.START, - controls=[ - pathways_panel, - ], - ), - padding=theme.variables.panel_padding, - ), - ], - ), - ], - ), + content=EditorPage(app.project), bgcolor=theme.colors.primary_lighter, border_radius=ft.border_radius.only(bottom_left=8, bottom_right=8), ) @@ -134,6 +45,6 @@ def on_action_color_changed(): logging.basicConfig(level=logging.CRITICAL) -ft.app(target=main, assets_dir="assets") +ft.app(target=main, assets_dir="assets", view=ft.AppView.WEB_BROWSER) print("Pathways App Started") diff --git a/source/package/pathways_app/pathways_app/controls/action_icon.py b/source/package/pathways_app/pathways_app/controls/action_icon.py index 2de0fe9..0bff294 100644 --- a/source/package/pathways_app/pathways_app/controls/action_icon.py +++ b/source/package/pathways_app/pathways_app/controls/action_icon.py @@ -9,7 +9,7 @@ class ActionIcon(ft.Container): def __init__(self, action: Action, display_tooltip=True, size=36): self.icon = ft.Icon(action.icon, size=(size * 0.5), color=action.color) self.ring = ft.Icon( - ft.icons.CIRCLE_OUTLINED, + ft.Icons.CIRCLE_OUTLINED, size=size, color=action.color, tooltip=( diff --git a/source/package/pathways_app/pathways_app/controls/actions_editor.py b/source/package/pathways_app/pathways_app/controls/actions_editor.py new file mode 100644 index 0000000..03fcee1 --- /dev/null +++ b/source/package/pathways_app/pathways_app/controls/actions_editor.py @@ -0,0 +1,205 @@ +# pylint: disable=too-many-arguments,too-many-instance-attributes +import random + +import flet as ft +import theme +from controls.action_icon import ActionIcon +from controls.editable_cell import EditableTextCell +from controls.metric_effect import MetricEffectCell +from controls.metric_value import MetricValueCell +from controls.styled_table import StyledTable, TableCell, TableColumn, TableRow +from pathways_app.controls.panel_header import PanelHeader + +from adaptation_pathways.app.model.action import Action +from adaptation_pathways.app.model.pathways_project import PathwaysProject + +from .. import theme +from .action_icon import ActionIcon +from .editable_cell import EditableTextCell +from .metric_effect import MetricEffectCell +from .metric_value import MetricValueCell +from .sortable_header import SortableHeader, SortMode +from .styled_button import StyledButton +from .styled_table import StyledTable + + +class ActionsEditor(ft.Column): + def __init__(self, project: PathwaysProject): + super().__init__( + expand=True, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + spacing=40, + ) + + self.project = project + + self.header = PanelHeader(title="Actions", icon=theme.icons.actions) + + self.action_table = StyledTable( + columns=[], + rows=[], + row_height=42, + on_add=self.on_new_action, + on_delete=self.on_delete_actions, + ) + self.update_table() + + self.controls = [ + ft.Column( + expand=True, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[ + self.header, + self.action_table, + ], + ), + ] + + def redraw(self): + self.update_table() + # self.update() + + def on_name_edited(self, _): + self.project.notify_actions_changed() + self.update() + + def on_cell_edited(self, cell: MetricValueCell): + self.project.update_pathway_values(cell.metric.id) + self.project.notify_actions_changed() + self.update() + + def on_action_selected(self, action: Action): + if action.id in self.project.selected_action_ids: + self.project.selected_action_ids.remove(action.id) + else: + self.project.selected_action_ids.add(action.id) + self.redraw() + self.update() + + def on_delete_actions(self, rows: list[TableRow]): + self.project.delete_actions(row.row_id for row in rows) + self.project.notify_actions_changed() + self.update() + + def on_new_action(self): + self.project.create_action( + random.choice(theme.action_colors), + random.choice(theme.action_icons), + ) + self.project.notify_actions_changed() + self.update() + + def update_table(self): + columns = [ + TableColumn(label="Icon", width=45, expand=False, sortable=False), + TableColumn( + label="Name", + ), + *( + TableColumn(label=metric.name, key=metric.id) + for metric in self.project.all_metrics() + ), + ] + self.action_table.set_columns(columns) + + self.update_rows() + + def create_icon_editor(self, action: Action): + def on_color_picked(color: str): + action.color = color + action_icon.update_action(action) + self.project.notify_action_color_changed() + + def on_icon_picked(icon: str): + action.icon = icon + action_icon.update_action(action) + self.project.notify_action_color_changed() + + def on_editor_closed(_): + self.project.notify_actions_changed() + + def update_items(): + action_button.items = [ + ft.PopupMenuItem( + content=ft.GridView( + spacing=4, + width=200, + runs_count=4, + padding=ft.padding.symmetric(4, 6), + child_aspect_ratio=1.0, + controls=[ + ft.Container( + bgcolor=color, + border=ft.border.all( + 2, + ( + theme.colors.primary_medium + if action.color == color + else theme.colors.primary_lightest + ), + ), + on_click=lambda e, c=color: on_color_picked(c), + width=10, + height=10, + ) + for color in theme.action_colors + ], + ) + ), + ft.PopupMenuItem( + content=ft.GridView( + expand=True, + runs_count=5, + padding=ft.padding.symmetric(12, 6), + controls=[ + ft.Container( + ft.Icon(icon), + on_click=lambda e, i=icon: on_icon_picked(i), + ) + for icon in theme.action_icons + ], + spacing=4, + ) + ), + ] + + action_icon = ActionIcon(action) + + action_button = ft.PopupMenuButton( + action_icon, + items=[], + bgcolor=theme.colors.off_white, + menu_position=ft.PopupMenuPosition.UNDER, + on_cancel=on_editor_closed, + ) + update_items() + return action_button + + def update_rows(self): + rows = [] + + for action in self.project.all_actions: + if action.id == self.project.root_pathway.action_id: + continue + + metric_cells = [] + for metric in self.project.all_metrics(): + effect = action.metric_data[metric.id] + metric_cells.append( + MetricEffectCell( + metric, effect, on_finished_editing=self.on_cell_edited + ) + ) + + rows.append( + TableRow( + row_id=action.id, + cells=[ + TableCell(self.create_icon_editor(action)), + EditableTextCell(action, "name", self.on_name_edited), + *metric_cells, + ], + ) + ) + + self.action_table.set_rows(rows) diff --git a/source/package/pathways_app/pathways_app/controls/actions_panel.py b/source/package/pathways_app/pathways_app/controls/actions_panel.py index c760ae7..03fcee1 100644 --- a/source/package/pathways_app/pathways_app/controls/actions_panel.py +++ b/source/package/pathways_app/pathways_app/controls/actions_panel.py @@ -7,13 +7,11 @@ from controls.editable_cell import EditableTextCell from controls.metric_effect import MetricEffectCell from controls.metric_value import MetricValueCell -from controls.sortable_header import SortableHeader, SortMode -from controls.styled_button import StyledButton -from controls.styled_table import StyledTable, TableCell, TableColumn +from controls.styled_table import StyledTable, TableCell, TableColumn, TableRow +from pathways_app.controls.panel_header import PanelHeader from adaptation_pathways.app.model.action import Action from adaptation_pathways.app.model.pathways_project import PathwaysProject -from adaptation_pathways.app.model.sorting import SortTarget from .. import theme from .action_icon import ActionIcon @@ -25,46 +23,33 @@ from .styled_table import StyledTable -class ActionsPanel(ft.Column): +class ActionsEditor(ft.Column): def __init__(self, project: PathwaysProject): super().__init__( expand=True, horizontal_alignment=ft.CrossAxisAlignment.STRETCH, spacing=40, - scroll=ft.ScrollMode.AUTO, ) self.project = project - self.action_table = StyledTable( - columns=[], rows=[], row_height=42, show_checkboxes=True - ) + self.header = PanelHeader(title="Actions", icon=theme.icons.actions) - self.delete_action_button = StyledButton( - "Delete", - icon=ft.icons.DELETE, - on_click=self.on_delete_actions, + self.action_table = StyledTable( + columns=[], + rows=[], + row_height=42, + on_add=self.on_new_action, + on_delete=self.on_delete_actions, ) - self.update_table() self.controls = [ ft.Column( - expand=False, + expand=True, horizontal_alignment=ft.CrossAxisAlignment.STRETCH, controls=[ - ft.Row( - expand=True, - controls=[ - ft.Container(expand=True), - self.delete_action_button, - StyledButton( - "New", - icon=ft.icons.ADD_CIRCLE_OUTLINE, - on_click=self.on_new_action, - ), - ], - ), + self.header, self.action_table, ], ), @@ -91,30 +76,12 @@ def on_action_selected(self, action: Action): self.redraw() self.update() - def on_sort_actions(self, header: SortableHeader): - if header.sort_mode == SortMode.NONE: - self.project.action_sorting.target = SortTarget.NONE - self.project.action_sorting.sort_key = None - self.project.action_sorting.ascending = True - else: - self.project.action_sorting.target = ( - SortTarget.ATTRIBUTE if header.sort_key == "name" else SortTarget.METRIC - ) - self.project.action_sorting.sort_key = header.sort_key - self.project.action_sorting.ascending = ( - header.sort_mode == SortMode.ASCENDING - ) - - self.project.sort_actions() - self.project.notify_actions_changed() - self.update() - - def on_delete_actions(self, _): - self.project.delete_selected_actions() + def on_delete_actions(self, rows: list[TableRow]): + self.project.delete_actions(row.row_id for row in rows) self.project.notify_actions_changed() self.update() - def on_new_action(self, _): + def on_new_action(self): self.project.create_action( random.choice(theme.action_colors), random.choice(theme.action_icons), @@ -124,21 +91,18 @@ def on_new_action(self, _): def update_table(self): columns = [ - TableColumn(label="Icon", width=45, expand=False), + TableColumn(label="Icon", width=45, expand=False, sortable=False), TableColumn( label="Name", ), *( - TableColumn( - label=metric.name, key=metric.id, on_sort=self.on_sort_actions - ) + TableColumn(label=metric.name, key=metric.id) for metric in self.project.all_metrics() ), ] self.action_table.set_columns(columns) self.update_rows() - self.delete_action_button.visible = len(self.project.selected_action_ids) > 0 def create_icon_editor(self, action: Action): def on_color_picked(color: str): @@ -214,7 +178,10 @@ def update_items(): def update_rows(self): rows = [] - for action in self.project.sorted_actions: + for action in self.project.all_actions: + if action.id == self.project.root_pathway.action_id: + continue + metric_cells = [] for metric in self.project.all_metrics(): effect = action.metric_data[metric.id] @@ -225,11 +192,14 @@ def update_rows(self): ) rows.append( - [ - TableCell(self.create_icon_editor(action)), - EditableTextCell(action, "name", self.on_name_edited), - *metric_cells, - ] + TableRow( + row_id=action.id, + cells=[ + TableCell(self.create_icon_editor(action)), + EditableTextCell(action, "name", self.on_name_edited), + *metric_cells, + ], + ) ) self.action_table.set_rows(rows) diff --git a/source/package/pathways_app/pathways_app/controls/editable_cell.py b/source/package/pathways_app/pathways_app/controls/editable_cell.py index 13d39e2..0cb929c 100644 --- a/source/package/pathways_app/pathways_app/controls/editable_cell.py +++ b/source/package/pathways_app/pathways_app/controls/editable_cell.py @@ -3,10 +3,10 @@ import flet as ft import theme -from .styled_table import TableCell +from pathways_app.controls.input_filters import IntInputFilter from pyparsing import abstractmethod -from .. import theme +from .styled_table import TableCell class EditableCell(TableCell, ABC): @@ -19,13 +19,14 @@ def __init__( can_reset=False, alignment: ft.Alignment | None = ft.alignment.center_left, padding: int | ft.Padding | None = theme.variables.table_cell_padding, + sort_value: str | int | float | None = None, on_finished_editing: Callable[["EditableCell"], None] | None = None, ): self.display_content = display_control self.input_content = edit_control self.calculated_icon = ft.Container( ft.Icon( - ft.icons.CALCULATE, + ft.Icons.CALCULATE, size=theme.variables.calculated_icon_size, color=theme.colors.calculated_icon, ), @@ -33,12 +34,23 @@ def __init__( alignment=ft.alignment.top_left, ) self.reset_button = ft.Container( - ft.Icon( - ft.icons.CALCULATE, - size=theme.variables.calculated_icon_size, - color=theme.colors.calculated_icon, + ft.Container( + ft.Icon( + ft.Icons.RESTART_ALT, + size=theme.variables.calculated_icon_size, + color=theme.colors.calculated_icon, + tooltip=ft.Tooltip( + "Recalculate", + bgcolor=theme.colors.calculated_icon, + wait_duration=0, + vertical_offset=15, + text_style=theme.text.action_tooltip, + ), + ), + on_click=self.on_reset_to_calculated, ), - on_click=self.on_reset_to_calculated, + expand=True, + alignment=ft.alignment.top_left, ) self.is_editing = is_editing @@ -53,11 +65,14 @@ def __init__( content=ft.Stack( [ self.display_content, - ft.Row( - [self.reset_button, self.input_content], + ft.Stack( + [ + self.input_content, + ], expand=True, ), self.calculated_icon, + self.reset_button, ], expand=True, alignment=alignment, @@ -67,7 +82,7 @@ def __init__( on_click=self.set_editing, ) - super().__init__(control=self.cell_content) + super().__init__(control=self.cell_content, sort_value=sort_value) def set_editing(self, _): self.is_editing = True @@ -96,9 +111,9 @@ def update_visibility(self): self.display_content.visible = not self.is_editing self.input_content.visible = self.is_editing self.calculated_icon.visible = self.is_calculated and not self.is_editing - self.reset_button.visible = self.can_reset and self.is_editing + self.reset_button.visible = self.can_reset and not self.is_editing - def on_reset_to_calculated(self): + def on_reset_to_calculated(self, _): pass @abstractmethod @@ -135,6 +150,7 @@ def __init__(self, source: object, value_attribute: str, on_finished_editing=Non def on_finished_editing_internal(_): self.value = self.input_content.value + self.sort_value = self.value if on_finished_editing is not None: on_finished_editing(self) @@ -143,6 +159,7 @@ def on_finished_editing_internal(_): self.input_content, on_finished_editing=on_finished_editing_internal, ) + self.sort_value = self.value @property def value(self) -> str: @@ -157,3 +174,61 @@ def update_input(self): def update_display(self): self.display_content.value = self.input_content.value + + def on_reset_to_calculated(self, _): + pass + + +class EditableIntCell(EditableCell): + def __init__(self, source: object, value_attribute: str, on_finished_editing=None): + self.source = source + self.value_attribute = value_attribute + + self.display_content = ft.Text(self.value, expand=True) + self.input_content = ft.TextField( + dense=True, + enable_suggestions=False, + value=self.value, + keyboard_type=ft.KeyboardType.NUMBER, + input_filter=IntInputFilter(), + bgcolor=theme.colors.true_white, + border_color=theme.colors.primary_medium, + focused_border_color=theme.colors.primary_light, + cursor_color=theme.colors.primary_medium, + text_style=theme.text.textfield, + prefix_style=theme.text.textfield_symbol, + suffix_style=theme.text.textfield_symbol, + expand=True, + content_padding=ft.padding.symmetric(4, 6), + on_blur=self.set_not_editing, + ) + + def on_finished_editing_internal(_): + self.value = int(self.input_content.value) + self.sort_value = self.value + if on_finished_editing is not None: + on_finished_editing(self) + + super().__init__( + self.display_content, + self.input_content, + on_finished_editing=on_finished_editing_internal, + ) + self.sort_value = self.value + + @property + def value(self) -> int: + return getattr(self.source, self.value_attribute) + + @value.setter + def value(self, value: int): + setattr(self.source, self.value_attribute, value) + + def update_input(self): + self.input_content.value = self.display_content.value + + def update_display(self): + self.display_content.value = self.input_content.value + + def on_reset_to_calculated(self, _): + pass diff --git a/source/package/pathways_app/pathways_app/controls/editor_page.py b/source/package/pathways_app/pathways_app/controls/editor_page.py new file mode 100644 index 0000000..20edadb --- /dev/null +++ b/source/package/pathways_app/pathways_app/controls/editor_page.py @@ -0,0 +1,166 @@ +import flet as ft +import theme +from controls.actions_editor import ActionsEditor +from controls.graph_editor import GraphEditor +from controls.header import SectionHeader +from controls.metrics_editor import MetricsEditor +from controls.panel import Panel +from controls.pathways_editor import PathwaysPanel +from controls.scenarios_editor import ScenariosEditor +from controls.tabbed_panel import TabbedPanel + +from adaptation_pathways.app.model.pathways_project import PathwaysProject + + +class EditorPage(ft.Row): + def __init__(self, project: PathwaysProject): + self.project = project + self.expanded_editor: ft.Control | None = None + + self.metrics_editor = MetricsEditor(project) + self.metrics_tab = ( + SectionHeader(theme.icons.metrics, size=20), + self.metrics_editor, + ) + + self.actions_editor = ActionsEditor(project) + self.actions_tab = ( + SectionHeader(theme.icons.actions, size=20), + self.actions_editor, + ) + + self.scenarios_editor = ScenariosEditor(project) + self.scenarios_tab = ( + SectionHeader(theme.icons.scenarios, size=20), + self.scenarios_editor, + ) + + self.tabbed_panel = TabbedPanel( + selected_index=0, + tabs=[ + self.metrics_tab, + self.actions_tab, + self.scenarios_tab, + ], + ) + + self.graph_editor = GraphEditor(project) + self.graph_panel = Panel(self.graph_editor) + + self.pathways_editor = PathwaysPanel(project) + self.pathways_panel = Panel( + content=ft.Column( + expand=True, + alignment=ft.MainAxisAlignment.START, + controls=[ + self.pathways_editor, + ], + ), + padding=theme.variables.panel_padding, + ) + + self.metrics_editor.header.on_expand = lambda: self.on_editor_expanded( + self.tabbed_panel + ) + self.actions_editor.header.on_expand = lambda: self.on_editor_expanded( + self.tabbed_panel + ) + self.scenarios_editor.header.on_expand = lambda: self.on_editor_expanded( + self.tabbed_panel + ) + self.graph_editor.header.on_expand = lambda: self.on_editor_expanded( + self.graph_panel + ) + self.pathways_editor.header.on_expand = lambda: self.on_editor_expanded( + self.pathways_panel + ) + + self.left_column = ft.Column( + expand=2, + spacing=theme.variables.panel_spacing, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[self.tabbed_panel], + ) + + self.right_column = ft.Column( + expand=3, + spacing=theme.variables.panel_spacing, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[self.graph_panel, self.pathways_panel], + ) + + super().__init__( + expand=True, + spacing=theme.variables.panel_spacing, + controls=[self.left_column, self.right_column], + ) + + self.update_layout() + + project.on_conditions_changed.append(self.on_metrics_changed) + project.on_criteria_changed.append(self.on_metrics_changed) + project.on_scenarios_changed.append(self.on_scenarios_changed) + project.on_actions_changed.append(self.on_actions_changed) + project.on_action_color_changed.append(self.on_action_color_changed) + project.on_pathways_changed.append(self.on_pathways_changed) + + def update_layout(self): + if self.expanded_editor is None: + self.left_column.visible = True + self.right_column.visible = True + self.graph_panel.visible = True + self.pathways_panel.visible = True + else: + self.left_column.visible = self.expanded_editor == self.tabbed_panel + self.pathways_panel.visible = self.expanded_editor == self.pathways_panel + self.graph_panel.visible = self.expanded_editor == self.graph_panel + self.right_column.visible = ( + self.pathways_panel.visible or self.graph_panel.visible + ) + + self.metrics_editor.header.set_expanded( + self.expanded_editor == self.tabbed_panel + ) + self.actions_editor.header.set_expanded( + self.expanded_editor == self.tabbed_panel + ) + self.scenarios_editor.header.set_expanded( + self.expanded_editor == self.tabbed_panel + ) + self.pathways_editor.header.set_expanded( + self.expanded_editor == self.pathways_panel + ) + self.graph_editor.header.set_expanded(self.expanded_editor == self.graph_panel) + + def on_editor_expanded(self, editor): + if self.expanded_editor == editor: + self.expanded_editor = None + else: + self.expanded_editor = editor + + self.update_layout() + self.update() + + def on_metrics_changed(self): + self.metrics_editor.redraw() + self.scenarios_editor.redraw() + self.actions_editor.redraw() + self.pathways_editor.redraw() + self.graph_editor.redraw() + + def on_scenarios_changed(self): + self.scenarios_editor.redraw() + self.graph_editor.redraw() + + def on_actions_changed(self): + self.actions_editor.redraw() + self.pathways_editor.redraw() + self.graph_editor.redraw() + + def on_pathways_changed(self): + self.pathways_editor.redraw() + self.graph_editor.redraw() + + def on_action_color_changed(self): + self.pathways_editor.redraw() + self.graph_editor.redraw() diff --git a/source/package/pathways_app/pathways_app/controls/graph_editor.py b/source/package/pathways_app/pathways_app/controls/graph_editor.py new file mode 100644 index 0000000..0e4975a --- /dev/null +++ b/source/package/pathways_app/pathways_app/controls/graph_editor.py @@ -0,0 +1,253 @@ +from typing import Callable + +import flet as ft +import matplotlib.pyplot +import theme +from controls.styled_button import StyledButton +from controls.styled_dropdown import StyledDropdown +from flet.matplotlib_chart import MatplotlibChart +from pathways_app.controls.header import SmallHeader + +from adaptation_pathways.app.model.pathways_project import PathwaysProject +from adaptation_pathways.app.service.plotting_service import PlottingService + + +class GraphHeader(ft.Row): + def __init__( + self, + on_expand: Callable[[], None] | None = None, + on_sidebar: Callable[[], None] | None = None, + ): + self.on_expand = on_expand + self.on_sidebar = on_sidebar + + self.expand_icon = ft.Icon( + ft.Icons.OPEN_IN_FULL, + color=theme.colors.primary_dark, + size=theme.variables.icon_button_size, + ) + self.sidebar_icon = ft.Icon( + ft.Icons.VIEW_SIDEBAR, + color=theme.colors.primary_dark, + size=theme.variables.icon_button_size, + ) + + super().__init__( + expand=False, + controls=[ + ft.Container(self.sidebar_icon, on_click=self.on_sidebar_clicked), + ft.Container(expand=True), + ft.Container(self.expand_icon, on_click=self.on_expand_clicked), + ], + ) + self.set_sidebar_open(True) + self.set_expanded(False) + + def set_expanded(self, expanded: bool): + self.expand_icon.name = ( + theme.icons.minimize if expanded else theme.icons.maximize + ) + + def on_expand_clicked(self, _): + if self.on_expand is None: + return + self.on_expand() + + def set_sidebar_open(self, is_open: bool): + self.sidebar_icon.name = ( + theme.icons.sidebar_open if is_open else theme.icons.sidebar_closed + ) + + def on_sidebar_clicked(self, _): + if self.on_sidebar is None: + return + self.on_sidebar() + + +class GraphEditor(ft.Row): + def __init__(self, project: PathwaysProject): + super().__init__(expand=False, spacing=0) + + self.project = project + + self.header = GraphHeader(on_sidebar=self.on_sidebar_toggle) + + self.graph_container = ft.Container( + expand=True, bgcolor=theme.colors.true_white + ) + + self.metric_dropdown = StyledDropdown( + value="", options=[], width=200, on_change=self.on_graph_metric_changed + ) + + self.metric_dropdown.value = ( + "time" if self.project.graph_is_time else self.project.graph_metric_id + ) + + self.graph_options = ft.Column([], expand=False, spacing=3) + + self.update_parameters() + + self.sidebar = ft.Container( + expand=False, + padding=theme.variables.panel_padding, + content=ft.Column( + expand=False, + width=200, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[ + StyledDropdown( + "Metro Map", + options=[ + ft.dropdown.Option("Metro Map"), + ft.dropdown.Option("Bar Chart"), + ], + option_icons=[ft.Icons.ROUTE_OUTLINED, ft.Icons.BAR_CHART], + height=36, + text_style=theme.text.dropdown_large, + ), + self.graph_options, + ft.Container(expand=True), + ft.Row( + [ + StyledButton("Export", icon=ft.Icons.SAVE_SHARP), + ], + alignment=ft.MainAxisAlignment.END, + ), + ], + ), + ) + + self.controls = [ + self.sidebar, + ft.Container( + expand=True, + bgcolor=theme.colors.true_white, + border_radius=ft.border_radius.only( + top_right=theme.variables.small_radius, + bottom_right=theme.variables.small_radius, + ), + padding=ft.padding.only(bottom=10), + content=ft.Stack( + expand=True, + controls=[ + ft.Column( + [ + ft.Container(height=10), + self.graph_container, + self.metric_dropdown, + ], + expand=True, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + ), + ft.Container( + content=self.header, + padding=theme.variables.panel_padding, + alignment=ft.alignment.top_center, + height=36, + ), + ], + alignment=ft.alignment.top_center, + ), + ), + ] + + self.update_graph() + + def on_sidebar_toggle(self): + self.sidebar.visible = not self.sidebar.visible + self.header.set_sidebar_open(self.sidebar.visible) + self.update() + + def on_graph_metric_changed(self, _): + self.project.graph_is_time = self.metric_dropdown.value == "time" + if not self.project.graph_is_time: + self.project.graph_metric_id = self.metric_dropdown.value + print(self.project.graph_is_time) + print(self.project.graph_metric_id) + self.redraw() + + def on_time_metric_changed(self, _): + self.project.graph_metric_id = self.time_metric_option.value + self.redraw() + + def on_graph_scenario_changed(self, _): + self.project.graph_scenario_id = self.graph_scenario_option.value + self.redraw() + + def redraw(self): + self.update_parameters() + self.update_graph() + self.update() + + def update_parameters(self): + if self.project.graph_is_time: + self.time_metric_option = StyledDropdown( + ( + self.project.graph_metric_id + if self.project.graph_metric_id is not None + else "none" + ), + options=[ + ft.dropdown.Option( + key=None, text="- Select a Condition -", disabled=True + ), + *( + ft.dropdown.Option( + key=metric.id, + text=f"{metric.name} ({metric.unit.symbol})", + ) + for metric in self.project.all_conditions + ), + ], + on_change=self.on_time_metric_changed, + ) + + self.graph_scenario_option = StyledDropdown( + ( + self.project.graph_scenario_id + if self.project.graph_scenario_id is not None + else "none" + ), + options=[ + ft.dropdown.Option( + key="none", text="- Select a Scenario -", disabled=True + ), + *( + ft.dropdown.Option( + key=scenario.id, + text=f"{scenario.name}", + ) + for scenario in self.project.all_scenarios + ), + ], + on_change=self.on_graph_scenario_changed, + ) + + self.graph_options.controls = [ + ft.Container(height=0), + SmallHeader("Condition"), + self.time_metric_option, + ft.Container(height=15), + SmallHeader("Scenario"), + self.graph_scenario_option, + ] + else: + self.graph_options.controls = [] + + self.metric_dropdown.options = [ + ft.dropdown.Option( + key="time", text="Time", disabled=len(self.project.scenarios_by_id) == 0 + ), + *( + ft.dropdown.Option( + key=metric.id, text=f"{metric.name} ({metric.unit.symbol})" + ) + for metric in self.project.all_conditions + ), + ] + + def update_graph(self): + figure, _ = PlottingService.draw_metro_map(self.project) + self.graph_container.content = MatplotlibChart(figure) + matplotlib.pyplot.close(figure) diff --git a/source/package/pathways_app/pathways_app/controls/graph_panel.py b/source/package/pathways_app/pathways_app/controls/graph_panel.py deleted file mode 100644 index a24507b..0000000 --- a/source/package/pathways_app/pathways_app/controls/graph_panel.py +++ /dev/null @@ -1,96 +0,0 @@ -import flet as ft -import matplotlib.pyplot -from flet.matplotlib_chart import MatplotlibChart - -from adaptation_pathways.app.model.pathways_project import PathwaysProject -from adaptation_pathways.app.service.plotting_service import PlottingService - -from .. import theme -from .styled_button import StyledButton -from .styled_dropdown import StyledDropdown - - -class GraphPanel(ft.Row): - def __init__(self, project: PathwaysProject): - super().__init__(expand=False, spacing=0) - - self.project = project - - self.graph_container = ft.Container( - expand=True, bgcolor=theme.colors.true_white - ) - - self.metric_dropdown = StyledDropdown( - value="", - options=[], - width=200, - ) - - self.update_parameters() - - self.controls = [ - ft.Container( - expand=False, - padding=theme.variables.panel_padding, - content=ft.Column( - expand=False, - width=200, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - StyledDropdown( - "Metro Map", - options=[ - ft.dropdown.Option("Metro Map"), - ft.dropdown.Option("Bar Chart"), - ], - option_icons=[ft.icons.ROUTE_OUTLINED, ft.icons.BAR_CHART], - height=36, - text_style=theme.text.dropdown_large, - ), - ft.Container(expand=True), - ft.Row( - [ - StyledButton("Export", icon=ft.icons.SAVE_SHARP), - ], - alignment=ft.MainAxisAlignment.END, - ), - ], - ), - ), - ft.Container( - expand=True, - bgcolor=theme.colors.true_white, - border_radius=ft.border_radius.only( - top_right=theme.variables.small_radius, - bottom_right=theme.variables.small_radius, - ), - padding=ft.padding.only(bottom=10), - content=ft.Column( - [self.graph_container, self.metric_dropdown], - horizontal_alignment=ft.CrossAxisAlignment.CENTER, - ), - ), - ] - - self.update_graph() - - def redraw(self): - self.update_parameters() - self.update_graph() - self.update() - - def update_parameters(self): - self.metric_dropdown.options = [ - *( - ft.dropdown.Option( - key=metric.id, text=f"{metric.name} ({metric.unit.symbol})" - ) - for metric in self.project.sorted_conditions - ), - ] - self.metric_dropdown.value = self.project.graph_metric_id - - def update_graph(self): - figure, _ = PlottingService.draw_metro_map(self.project) - self.graph_container.content = MatplotlibChart(figure) - matplotlib.pyplot.close(figure) diff --git a/source/package/pathways_app/pathways_app/controls/header.py b/source/package/pathways_app/pathways_app/controls/header.py index 87746bd..3175713 100644 --- a/source/package/pathways_app/pathways_app/controls/header.py +++ b/source/package/pathways_app/pathways_app/controls/header.py @@ -7,8 +7,8 @@ class SectionHeader(ft.Container): def __init__( self, - icon=None, - text="", + icon: str | None = None, + text: str | None = None, size=16, expand=False, color=theme.colors.primary_dark, @@ -35,20 +35,21 @@ def __init__( weight=ft.FontWeight.W_600, ) - if icon is not None: - self.content = ft.Row( - expand=expand, - controls=[ - ft.Icon(icon, size=size, color=color), - ft.Text(text, style=header_text_style), - ], - spacing=8, - vertical_alignment=ft.CrossAxisAlignment.CENTER, - ) - else: - self.content = ft.Row( - expand=True, controls=[ft.Text(text, style=header_text_style)] - ) + self.icon = ft.Icon(icon, size=size, color=color) + self.icon.visible = icon is not None + + self.text = ft.Text(text, style=header_text_style) + self.text.visible = text is not None + + self.content = ft.Row( + expand=expand, + controls=[ + self.icon, + self.text, + ], + spacing=8, + vertical_alignment=ft.CrossAxisAlignment.CENTER, + ) class SmallHeader(ft.Text): diff --git a/source/package/pathways_app/pathways_app/controls/input_filters.py b/source/package/pathways_app/pathways_app/controls/input_filters.py new file mode 100644 index 0000000..57f2e7d --- /dev/null +++ b/source/package/pathways_app/pathways_app/controls/input_filters.py @@ -0,0 +1,13 @@ +import flet as ft + + +class FloatInputFilter(ft.InputFilter): + def __init__(self): + super().__init__( + regex_string=r"^$|^[-+]?\d*(\.\d*)?$", allow=True, replacement_string="" + ) + + +class IntInputFilter(ft.InputFilter): + def __init__(self): + super().__init__(regex_string=r"^-?\d*$", allow=True, replacement_string="") diff --git a/source/package/pathways_app/pathways_app/controls/menu_bar.py b/source/package/pathways_app/pathways_app/controls/menu_bar.py index 46003a3..74948ed 100644 --- a/source/package/pathways_app/pathways_app/controls/menu_bar.py +++ b/source/package/pathways_app/pathways_app/controls/menu_bar.py @@ -1,20 +1,67 @@ import flet as ft - -from adaptation_pathways.app.model import PathwaysProject - -from .. import theme +import theme +from pathways_app.app import App +from pathways_app.config import Config class MenuBar(ft.Container): - def __init__(self, project: PathwaysProject): + def __init__(self, app: App): + self.app = app + super().__init__( content=ft.Stack( [ ft.Row( [ - ft.Image(theme.icon), + ft.Image(theme.icon, height=36, width=36), ft.Text("PATHWAYS\nGENERATOR", style=theme.text.logo), - ] + ft.Container(width=15), + ft.SubmenuButton( + ft.Text("Project", style=theme.text.menu_button), + controls=[ + ft.MenuItemButton( + content=ft.Text("New"), + on_click=self.on_new_project, + ), + ft.MenuItemButton( + content=ft.Text("Open..."), + on_click=self.on_open_project, + ), + ft.MenuItemButton( + content=ft.Text("Save As..."), + on_click=self.on_save_project, + ), + ], + # style=theme.buttons.menu_bar_button, + ), + ft.SubmenuButton( + ft.Text("Help", style=theme.text.menu_button), + controls=[ + ft.MenuItemButton( + content=ft.Text("About Pathways"), + on_click=lambda e: app.open_link( + Config.about_url + ), + ), + ft.MenuItemButton( + content=ft.Text("GitHub Repository"), + on_click=lambda e: app.open_link( + Config.github_url + ), + ), + ft.Container( + content=ft.Text( + "version 1.0.0", + color=theme.colors.primary_light, + ), + padding=ft.padding.symmetric(5, 10), + ), + ], + # style=theme.buttons.menu_bar_button, + menu_style=theme.buttons.submenu, + ), + ], + expand=True, ), ft.Row( [ @@ -29,11 +76,11 @@ def __init__(self, project: PathwaysProject): ft.Column( controls=[ ft.Text( - project.name, + app.project.name, color=theme.colors.true_white, ), ft.Text( - project.organization, + app.project.organization, text_align=ft.TextAlign.CENTER, color=theme.colors.true_white, ), @@ -60,3 +107,12 @@ def __init__(self, project: PathwaysProject): bottom=ft.border.BorderSide(1, theme.colors.primary_darker) ), ) + + def on_new_project(self): + pass + + def on_open_project(self, _): + self.app.open_project() + + def on_save_project(self): + pass diff --git a/source/package/pathways_app/pathways_app/controls/metric_effect.py b/source/package/pathways_app/pathways_app/controls/metric_effect.py index 33ff121..84c5820 100644 --- a/source/package/pathways_app/pathways_app/controls/metric_effect.py +++ b/source/package/pathways_app/pathways_app/controls/metric_effect.py @@ -115,3 +115,6 @@ def update_input(self): self.input_content.suffix_text = None self.value_input.value = self.effect.value + + def on_reset_to_calculated(self): + pass diff --git a/source/package/pathways_app/pathways_app/controls/metric_value.py b/source/package/pathways_app/pathways_app/controls/metric_value.py index 812989d..8f8bd77 100644 --- a/source/package/pathways_app/pathways_app/controls/metric_value.py +++ b/source/package/pathways_app/pathways_app/controls/metric_value.py @@ -1,22 +1,16 @@ import flet as ft +import theme +from controls.editable_cell import EditableCell +from pathways_app.controls.input_filters import FloatInputFilter from adaptation_pathways.app.model.metric import Metric, MetricValue, MetricValueState -from .. import theme -from .editable_cell import EditableCell - - -class FloatInputFilter(ft.InputFilter): - def __init__(self): - super().__init__( - regex_string=r"^$|^[-+]?\d*(\.\d*)?$", allow=True, replacement_string="" - ) - class MetricValueCell(EditableCell): def __init__(self, metric: Metric, value: MetricValue, on_finished_editing=None): self.metric = metric self.value = value + self.sort_value = value.value self.finished_editing_callback = on_finished_editing self.display_content = ft.Text("") @@ -49,6 +43,7 @@ def __init__(self, metric: Metric, value: MetricValue, on_finished_editing=None) can_reset=value.state == MetricValueState.OVERRIDE, on_finished_editing=self.on_edited, alignment=ft.alignment.center_right, + sort_value=self.value.value, ) def on_edited(self, _): @@ -56,15 +51,16 @@ def on_edited(self, _): if new_value != self.value.value and self.value.is_estimate: self.value.state = MetricValueState.OVERRIDE + self.value.value = new_value + self.sort_value = self.value.value if self.finished_editing_callback is not None: self.finished_editing_callback(self) - def on_reset_to_calculated(self): + def on_reset_to_calculated(self, _): self.value.state = MetricValueState.ESTIMATE - self.set_not_editing() - print(self.value) + self.update_controls() def update_display(self): self.display_content.value = self.metric.unit.format(self.value.value) diff --git a/source/package/pathways_app/pathways_app/controls/metrics_editor.py b/source/package/pathways_app/pathways_app/controls/metrics_editor.py new file mode 100644 index 0000000..c114e10 --- /dev/null +++ b/source/package/pathways_app/pathways_app/controls/metrics_editor.py @@ -0,0 +1,159 @@ +# from typing import Callable + +import flet as ft +from controls.editable_cell import EditableTextCell +from controls.header import SmallHeader +from controls.styled_table import StyledTable, TableColumn, TableRow +from controls.unit_cell import MetricUnitCell +from pathways_app import theme +from pathways_app.controls.panel_header import PanelHeader + +from adaptation_pathways.app.model.metric import Metric +from adaptation_pathways.app.model.pathways_project import PathwaysProject + +from .editable_cell import EditableTextCell +from .header import SmallHeader +from .styled_button import StyledButton +from .styled_table import StyledTable +from .unit_cell import MetricUnitCell + + +class MetricsEditor(ft.Column): + def __init__(self, project: PathwaysProject): + super().__init__() + + self.project = project + self.header = PanelHeader("Metrics", theme.icons.metrics) + self.expand = True + self.horizontal_alignment = ft.CrossAxisAlignment.STRETCH + self.spacing = 40 + + self.conditions_table = StyledTable( + columns=[ + TableColumn(label="Name"), + TableColumn(label="Unit"), + ], + rows=[], + show_checkboxes=True, + on_add=self.on_new_condition, + on_delete=self.on_delete_conditions, + pre_operation_content=ft.Row( + [ + SmallHeader("Conditions"), + ft.Container(expand=True), + ], + expand=True, + ), + ) + + self.criteria_table = StyledTable( + columns=[ + TableColumn(label="Name"), + TableColumn(label="Unit"), + ], + rows=[], + show_checkboxes=True, + on_add=self.on_new_criteria, + on_delete=self.on_delete_criteria, + pre_operation_content=ft.Row( + [ + SmallHeader("Criteria"), + ft.Container(expand=True), + ], + expand=True, + ), + ) + + self.update_metrics() + + self.controls = [ + ft.Column( + expand=True, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[ + self.header, + self.conditions_table, + ], + ), + ft.Column( + expand=True, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[ + self.criteria_table, + ], + ), + ] + + def redraw(self): + self.update_metrics() + self.update() + + def on_metric_updated(self, _): + self.project.notify_conditions_changed() + + def on_condition_selected(self, metric: Metric): + if metric.id in self.project.selected_condition_ids: + self.project.selected_condition_ids.remove(metric.id) + else: + self.project.selected_condition_ids.add(metric.id) + + self.redraw() + + def on_criteria_selected(self, metric: Metric): + if metric.id in self.project.selected_criteria_ids: + self.project.selected_criteria_ids.remove(metric.id) + else: + self.project.selected_criteria_ids.add(metric.id) + + self.redraw() + + def on_new_condition(self, _): + metric = self.project.create_condition() + self.project.update_pathway_values(metric.id) + self.project.notify_conditions_changed() + + def on_delete_conditions(self, _): + for metric_id in self.project.selected_condition_ids: + self.project.delete_condition(metric_id) + self.project.notify_conditions_changed() + + def on_new_criteria(self, _): + metric = self.project.create_criteria() + self.project.update_pathway_values(metric.id) + self.project.notify_criteria_changed() + + def on_delete_criteria(self, _): + for metric_id in self.project.selected_criteria_ids: + self.project.delete_criteria(metric_id) + self.project.notify_criteria_changed() + + def get_metric_row( + self, + metric: Metric, + # selected_ids: set[str], + # on_metric_selected: Callable[[Metric], None], + ) -> TableRow: + row = TableRow( + row_id=metric.id, + cells=[ + EditableTextCell(metric, "name", self.on_metric_updated), + MetricUnitCell(metric, self.on_metric_updated), + ], + ) + return row + + def update_metrics(self): + self.conditions_table.set_rows( + self.get_metric_row(metric) for metric in self.project.all_conditions + ) + + self.criteria_table.set_rows( + [ + self.get_metric_row( + metric, + # self.project.selected_criteria_ids, + # self.on_criteria_selected, + ) + for metric in self.project.all_criteria + ] + ) diff --git a/source/package/pathways_app/pathways_app/controls/metrics_panel.py b/source/package/pathways_app/pathways_app/controls/metrics_panel.py index 38e556c..c114e10 100644 --- a/source/package/pathways_app/pathways_app/controls/metrics_panel.py +++ b/source/package/pathways_app/pathways_app/controls/metrics_panel.py @@ -3,9 +3,10 @@ import flet as ft from controls.editable_cell import EditableTextCell from controls.header import SmallHeader -from controls.styled_button import StyledButton -from controls.styled_table import StyledTable, TableCell, TableColumn +from controls.styled_table import StyledTable, TableColumn, TableRow from controls.unit_cell import MetricUnitCell +from pathways_app import theme +from pathways_app.controls.panel_header import PanelHeader from adaptation_pathways.app.model.metric import Metric from adaptation_pathways.app.model.pathways_project import PathwaysProject @@ -17,12 +18,13 @@ from .unit_cell import MetricUnitCell -class MetricsPanel(ft.Column): +class MetricsEditor(ft.Column): def __init__(self, project: PathwaysProject): super().__init__() self.project = project - self.expand = False + self.header = PanelHeader("Metrics", theme.icons.metrics) + self.expand = True self.horizontal_alignment = ft.CrossAxisAlignment.STRETCH self.spacing = 40 @@ -33,6 +35,15 @@ def __init__(self, project: PathwaysProject): ], rows=[], show_checkboxes=True, + on_add=self.on_new_condition, + on_delete=self.on_delete_conditions, + pre_operation_content=ft.Row( + [ + SmallHeader("Conditions"), + ft.Container(expand=True), + ], + expand=True, + ), ) self.criteria_table = StyledTable( @@ -42,55 +53,32 @@ def __init__(self, project: PathwaysProject): ], rows=[], show_checkboxes=True, - ) - - self.delete_condition_button = StyledButton( - "Delete", ft.icons.DELETE, self.on_delete_conditions - ) - self.delete_criteria_button = StyledButton( - "Delete", ft.icons.DELETE, self.on_delete_criteria + on_add=self.on_new_criteria, + on_delete=self.on_delete_criteria, + pre_operation_content=ft.Row( + [ + SmallHeader("Criteria"), + ft.Container(expand=True), + ], + expand=True, + ), ) self.update_metrics() self.controls = [ ft.Column( - expand=False, + expand=True, horizontal_alignment=ft.CrossAxisAlignment.STRETCH, controls=[ - ft.Row( - expand=False, - controls=[ - SmallHeader("Conditions"), - ft.Container(expand=True), - self.delete_condition_button, - StyledButton( - "New", - icon=ft.icons.ADD_CIRCLE_OUTLINE, - on_click=self.on_new_condition, - ), - ], - ), + self.header, self.conditions_table, ], ), ft.Column( - expand=False, + expand=True, horizontal_alignment=ft.CrossAxisAlignment.STRETCH, controls=[ - ft.Row( - expand=False, - controls=[ - SmallHeader("Criteria"), - ft.Container(expand=True), - self.delete_criteria_button, - StyledButton( - text="New", - icon=ft.icons.ADD_CIRCLE_OUTLINE, - on_click=self.on_new_criteria, - ), - ], - ), self.criteria_table, ], ), @@ -144,26 +132,20 @@ def get_metric_row( metric: Metric, # selected_ids: set[str], # on_metric_selected: Callable[[Metric], None], - ) -> list[TableCell]: - row = [ - EditableTextCell(metric, "name", self.on_metric_updated), - MetricUnitCell(metric, self.on_metric_updated), - ] + ) -> TableRow: + row = TableRow( + row_id=metric.id, + cells=[ + EditableTextCell(metric, "name", self.on_metric_updated), + MetricUnitCell(metric, self.on_metric_updated), + ], + ) return row def update_metrics(self): self.conditions_table.set_rows( - [ - self.get_metric_row( - metric, - # self.project.selected_condition_ids, - # self.on_condition_selected, - ) - for metric in self.project.sorted_conditions - ] + self.get_metric_row(metric) for metric in self.project.all_conditions ) - has_selected_conditions = len(self.project.selected_condition_ids) > 0 - self.delete_condition_button.visible = has_selected_conditions self.criteria_table.set_rows( [ @@ -172,8 +154,6 @@ def update_metrics(self): # self.project.selected_criteria_ids, # self.on_criteria_selected, ) - for metric in self.project.sorted_criteria + for metric in self.project.all_criteria ] ) - has_selected_criteria = len(self.project.selected_criteria_ids) > 0 - self.delete_criteria_button.visible = has_selected_criteria diff --git a/source/package/pathways_app/pathways_app/controls/panel_header.py b/source/package/pathways_app/pathways_app/controls/panel_header.py new file mode 100644 index 0000000..a115085 --- /dev/null +++ b/source/package/pathways_app/pathways_app/controls/panel_header.py @@ -0,0 +1,42 @@ +from typing import Callable + +import flet as ft +from pathways_app import theme +from pathways_app.controls.header import SectionHeader + + +class PanelHeader(ft.Container): + def __init__( + self, + title: str, + icon: str | None = None, + on_expand: Callable[[], None] | None = None, + ): + self.on_expand = on_expand + self.icon = ft.Icon(ft.Icons.EXPAND, color=theme.colors.primary_dark, size=16) + self.set_expanded(False) + + super().__init__( + content=ft.Row( + [ + SectionHeader(icon, title), + ft.Container(expand=True), + ft.Container( + self.icon, + padding=0, + on_click=self.on_expand_clicked, + ), + ] + ), + padding=ft.padding.only(bottom=8), + border=ft.border.only(bottom=ft.BorderSide(1, theme.colors.primary_dark)), + ) + + def set_expanded(self, expanded: bool): + self.icon.name = theme.icons.minimize if expanded else theme.icons.maximize + + def on_expand_clicked(self, _): + if self.on_expand is None: + return + + self.on_expand() diff --git a/source/package/pathways_app/pathways_app/controls/pathways_editor.py b/source/package/pathways_app/pathways_app/controls/pathways_editor.py new file mode 100644 index 0000000..2b8b8ce --- /dev/null +++ b/source/package/pathways_app/pathways_app/controls/pathways_editor.py @@ -0,0 +1,174 @@ +import flet as ft +import theme +from controls.action_icon import ActionIcon +from controls.metric_value import MetricValueCell +from controls.styled_table import StyledTable, TableCell, TableColumn, TableRow +from pathways_app.controls.panel_header import PanelHeader + +from adaptation_pathways.app.model.pathway import Pathway +from adaptation_pathways.app.model.pathways_project import PathwaysProject + +from .. import theme +from .action_icon import ActionIcon +from .header import SectionHeader +from .metric_value import MetricValueCell +from .sortable_header import SortableHeader, SortMode +from .styled_button import StyledButton +from .styled_table import StyledTable + + +class PathwaysPanel(ft.Container): + def __init__(self, project: PathwaysProject): + self.project = project + + self.header = PanelHeader("Pathways", ft.Icons.ACCOUNT_TREE_OUTLINED) + + self.rows_by_pathway: dict[Pathway, ft.DataRow] = {} + + self.pathway_table = StyledTable( + columns=[], rows=[], show_checkboxes=True, on_delete=self.on_delete_pathways + ) + self.update_table() + + super().__init__( + expand=True, + content=ft.Column( + expand=True, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[ + self.header, + self.pathway_table, + ], + ), + ) + + def redraw(self): + self.update_table() + self.update() + + def on_delete_pathways(self, rows: list[TableRow]): + self.project.delete_pathways(row.row_id for row in rows) + self.project.notify_pathways_changed() + + def update_table(self): + self.pathway_table.set_columns( + [ + TableColumn( + label="Pathway", + expand=2, + ), + *( + TableColumn( + label=metric.name, + key=metric.id, + alignment=ft.alignment.center_right, + ) + for metric in self.project.all_metrics() + ), + ] + ) + + rows = [] + + self.rows_by_pathway = {} + for pathway in self.project.all_pathways: + ancestors = self.project.get_ancestors_and_self(pathway) + path = [*reversed([*ancestors])] + row = self.get_pathway_row(pathway, path) + self.rows_by_pathway[pathway] = row + rows.append(row) + + self.pathway_table.set_rows(rows) + return rows + + def on_metric_value_edited(self, cell: MetricValueCell): + self.project.update_pathway_values(cell.metric.id) + self.project.notify_pathways_changed() + + def get_pathway_row(self, pathway: Pathway, ancestors: list[Pathway]): + children = [*self.project.get_children(pathway.id)] + pathway_action = self.project.get_action(pathway.action_id) + + unused_action_ids = [ + action.id + for action in self.project.all_actions + if not any(ancestor.action_id == action.id for ancestor in ancestors) + and not any(child.action_id == action.id for child in children) + ] + + row_controls = [ + ActionIcon(self.project.get_action(ancestor.action_id), size=26) + for ancestor in ancestors + ] + + if pathway.parent_id is None: + row_controls.append(ft.Text(" Current ", color=pathway_action.color)) + + if len(unused_action_ids) > 0: + row_controls.append( + ft.PopupMenuButton( + ft.Icon( + ft.Icons.ADD_CIRCLE_OUTLINE, + size=20, + color=theme.colors.primary_lightest, + ), + items=[ + ft.PopupMenuItem( + content=ft.Row( + [ + ActionIcon( + action, + display_tooltip=False, + ), + ft.Text( + action.name, + style=theme.text.normal, + ), + ] + ), + on_click=lambda e, action_id=action_id: self.extend_pathway( + pathway, action_id + ), + ) + for action_id in unused_action_ids + for action in [self.project.get_action(action_id)] + ], + tooltip=ft.Tooltip( + "Add", + bgcolor=theme.colors.primary_white, + ), + bgcolor=theme.colors.off_white, + menu_position=ft.PopupMenuPosition.UNDER, + ), + ) + + row = TableRow( + row_id=pathway.id, + cells=[ + TableCell( + ft.Container( + expand=True, + content=ft.Row( + spacing=0, + controls=row_controls, + ), + ), + sort_value=pathway.id, + ), + *( + MetricValueCell( + metric, + pathway.metric_data[metric.id], + on_finished_editing=self.on_metric_value_edited, + ) + for metric in self.project.all_metrics() + ), + ], + can_be_deleted=pathway.id != self.project.root_pathway_id, + ) + + return row + + def extend_pathway(self, pathway: Pathway, action_id: str): + self.project.create_pathway(action_id, pathway.id) + self.project.notify_pathways_changed() diff --git a/source/package/pathways_app/pathways_app/controls/pathways_panel.py b/source/package/pathways_app/pathways_app/controls/pathways_panel.py index 5479456..2b8b8ce 100644 --- a/source/package/pathways_app/pathways_app/controls/pathways_panel.py +++ b/source/package/pathways_app/pathways_app/controls/pathways_panel.py @@ -1,15 +1,12 @@ import flet as ft import theme from controls.action_icon import ActionIcon -from controls.header import SectionHeader from controls.metric_value import MetricValueCell -from controls.sortable_header import SortableHeader, SortMode -from controls.styled_button import StyledButton -from controls.styled_table import StyledTable, TableCell, TableColumn +from controls.styled_table import StyledTable, TableCell, TableColumn, TableRow +from pathways_app.controls.panel_header import PanelHeader from adaptation_pathways.app.model.pathway import Pathway from adaptation_pathways.app.model.pathways_project import PathwaysProject -from adaptation_pathways.app.model.sorting import SortTarget from .. import theme from .action_icon import ActionIcon @@ -24,11 +21,12 @@ class PathwaysPanel(ft.Container): def __init__(self, project: PathwaysProject): self.project = project + self.header = PanelHeader("Pathways", ft.Icons.ACCOUNT_TREE_OUTLINED) + self.rows_by_pathway: dict[Pathway, ft.DataRow] = {} - self.pathway_table = StyledTable(columns=[], rows=[], show_checkboxes=True) - self.delete_pathways_button = StyledButton( - "Delete", ft.icons.DELETE, self.on_delete_pathways + self.pathway_table = StyledTable( + columns=[], rows=[], show_checkboxes=True, on_delete=self.on_delete_pathways ) self.update_table() @@ -38,15 +36,7 @@ def __init__(self, project: PathwaysProject): expand=True, horizontal_alignment=ft.CrossAxisAlignment.STRETCH, controls=[ - ft.Row( - [ - SectionHeader(ft.icons.ACCOUNT_TREE_OUTLINED, "Pathways"), - ft.Container(expand=True), - self.delete_pathways_button, - ], - spacing=15, - expand=False, - ), + self.header, self.pathway_table, ], ), @@ -56,42 +46,21 @@ def redraw(self): self.update_table() self.update() - def on_delete_pathways(self, _): - self.project.delete_selected_pathways() - self.project.notify_pathways_changed() - - def on_sort_table(self, header: SortableHeader): - if header.sort_mode is SortMode.NONE: - self.project.pathway_sorting.target = SortTarget.NONE - self.project.pathway_sorting.sort_key = None - self.project.pathway_sorting.ascending = True - else: - self.project.pathway_sorting.target = SortTarget.METRIC - self.project.pathway_sorting.sort_key = header.sort_key - self.project.pathway_sorting.ascending = ( - header.sort_mode == SortMode.ASCENDING - ) - - self.project.sort_pathways() + def on_delete_pathways(self, rows: list[TableRow]): + self.project.delete_pathways(row.row_id for row in rows) self.project.notify_pathways_changed() def update_table(self): - # sorting = self.project.pathway_sorting - # sort_mode = SortableHeader.get_sort_mode(sorting) - - self.delete_pathways_button.visible = ( - len(self.project.selected_pathway_ids) > 0 - and not self.project.root_pathway_id in self.project.selected_pathway_ids - ) - self.pathway_table.set_columns( [ - TableColumn(label="Pathway", expand=2), + TableColumn( + label="Pathway", + expand=2, + ), *( TableColumn( label=metric.name, key=metric.id, - on_sort=self.on_sort_table, alignment=ft.alignment.center_right, ) for metric in self.project.all_metrics() @@ -102,14 +71,10 @@ def update_table(self): rows = [] self.rows_by_pathway = {} - for pathway in self.project.sorted_pathways: + for pathway in self.project.all_pathways: ancestors = self.project.get_ancestors_and_self(pathway) path = [*reversed([*ancestors])] - # if len(path) > 1: - # path = path[1:] row = self.get_pathway_row(pathway, path) - # if pathway.id == self.project.root_pathway_id: - # row.on_select_changed = None self.rows_by_pathway[pathway] = row rows.append(row) @@ -118,7 +83,6 @@ def update_table(self): def on_metric_value_edited(self, cell: MetricValueCell): self.project.update_pathway_values(cell.metric.id) - self.project.sort_pathways() self.project.notify_pathways_changed() def get_pathway_row(self, pathway: Pathway, ancestors: list[Pathway]): @@ -126,10 +90,10 @@ def get_pathway_row(self, pathway: Pathway, ancestors: list[Pathway]): pathway_action = self.project.get_action(pathway.action_id) unused_action_ids = [ - action_id - for action_id in self.project.action_sorting.sorted_ids - if not any(ancestor.action_id == action_id for ancestor in ancestors) - and not any(child.action_id == action_id for child in children) + action.id + for action in self.project.all_actions + if not any(ancestor.action_id == action.id for ancestor in ancestors) + and not any(child.action_id == action.id for child in children) ] row_controls = [ @@ -144,7 +108,7 @@ def get_pathway_row(self, pathway: Pathway, ancestors: list[Pathway]): row_controls.append( ft.PopupMenuButton( ft.Icon( - ft.icons.ADD_CIRCLE_OUTLINE, + ft.Icons.ADD_CIRCLE_OUTLINE, size=20, color=theme.colors.primary_lightest, ), @@ -178,41 +142,33 @@ def get_pathway_row(self, pathway: Pathway, ancestors: list[Pathway]): ), ) - row = [ - TableCell( - ft.Container( - expand=True, - content=ft.Row( - spacing=0, - controls=row_controls, + row = TableRow( + row_id=pathway.id, + cells=[ + TableCell( + ft.Container( + expand=True, + content=ft.Row( + spacing=0, + controls=row_controls, + ), ), + sort_value=pathway.id, ), - ), - *( - MetricValueCell( - metric, - pathway.metric_data[metric.id], - on_finished_editing=self.on_metric_value_edited, - ) - for metric in self.project.all_metrics() - ), - ] - - # if pathway.parent_id is None: - # row.color = "#EEEEEE" - - # def on_select_changed(_): - # if pathway.id in self.project.selected_pathway_ids: - # self.project.selected_pathway_ids.remove(pathway.id) - # else: - # self.project.selected_pathway_ids.add(pathway.id) - - # self.project.notify_pathways_changed() + *( + MetricValueCell( + metric, + pathway.metric_data[metric.id], + on_finished_editing=self.on_metric_value_edited, + ) + for metric in self.project.all_metrics() + ), + ], + can_be_deleted=pathway.id != self.project.root_pathway_id, + ) - # row.on_select_changed = on_select_changed return row def extend_pathway(self, pathway: Pathway, action_id: str): self.project.create_pathway(action_id, pathway.id) - self.project.sort_pathways() self.project.notify_pathways_changed() diff --git a/source/package/pathways_app/pathways_app/controls/scenarios_editor.py b/source/package/pathways_app/pathways_app/controls/scenarios_editor.py new file mode 100644 index 0000000..28fe15f --- /dev/null +++ b/source/package/pathways_app/pathways_app/controls/scenarios_editor.py @@ -0,0 +1,219 @@ +import datetime +from functools import partial + +import flet as ft +from controls.styled_dropdown import StyledDropdown +from controls.styled_table import StyledTable, TableColumn, TableRow +from pathways_app import theme +from pathways_app.controls.editable_cell import EditableIntCell, EditableTextCell +from pathways_app.controls.header import SmallHeader +from pathways_app.controls.metric_value import MetricValueCell +from pathways_app.controls.panel_header import PanelHeader +from pathways_app.utils import find_index + +from adaptation_pathways.app.model.metric import Metric +from adaptation_pathways.app.model.pathways_project import PathwaysProject +from adaptation_pathways.app.model.scenario import YearDataPoint + + +class ScenariosEditor(ft.Column): + def __init__(self, project: PathwaysProject): + super().__init__( + expand=True, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + ) + + self.project = project + + self.header = PanelHeader("Scenarios", theme.icons.scenarios) + + self.scenario_table = StyledTable( + columns=[TableColumn("Name", key="name")], + rows=[], + expand=1, + on_add=self.on_add_scenario, + on_delete=self.on_delete_scenarios, + on_copy=self.on_copy_scenarios, + ) + self.update_scenario_table() + + self.no_scenario_option = ft.dropdown.Option( + key="none", text="- Choose a Scenario -", disabled=True + ) + self.scenario_dropdown = StyledDropdown( + value=( + "none" + if self.project.values_scenario_id is None + else self.project.values_scenario_id + ), + options=[], + on_change=self.on_scenario_changed, + ) + + self.scenario_values_table = StyledTable( + columns=[], + rows=[], + expand=3, + on_add=self.on_add_year, + add_label="Add Year", + on_delete=self.on_delete_years, + pre_operation_content=ft.Row( + [self.scenario_dropdown, ft.Container(expand=True)], expand=True + ), + ) + + self.controls = [ + self.header, + self.scenario_table, + ft.Container(height=20), + SmallHeader("Scenario Data"), + self.scenario_values_table, + ] + + self.update_dropdown() + self.update_values_table() + + def update_dropdown(self): + self.scenario_dropdown.set_options( + [ + self.no_scenario_option, + *( + ft.dropdown.Option(key=scenario.id, text=scenario.name) + for scenario in self.project.all_scenarios + ), + ] + ) + + def update_scenario_table(self): + self.scenario_table.set_rows( + [ + TableRow( + scenario.id, + [EditableTextCell(scenario, "name", self.on_scenario_name_edited)], + ) + for scenario in self.project.all_scenarios + ] + ) + + def on_add_scenario(self): + self.project.create_scenario("New Scenario") + self.project.notify_scenarios_changed() + + def on_copy_scenarios(self, rows: list[TableRow]): + for row in rows: + self.project.copy_scenario(row.row_id) + + self.project.notify_scenarios_changed() + + def on_scenario_name_edited(self): + self.project.notify_scenarios_changed() + + def on_delete_scenarios(self, rows: list[TableRow]): + self.project.delete_scenarios(row.row_id for row in rows) + self.project.notify_scenarios_changed() + + def update_values_table(self): + self.update_values_headers() + self.update_values_rows() + + def update_values_headers(self): + self.scenario_values_table.set_columns( + [ + TableColumn(label="Year", sortable=False), + *( + TableColumn(label=metric.name, sortable=False) + for metric in self.project.all_conditions + ), + ] + ) + + def update_values_rows(self): + if self.project.values_scenario is None: + self.scenario_values_table.set_rows([]) + return + + self.scenario_values_table.set_rows( + [ + self._get_year_row(point) + for point in self.project.values_scenario.yearly_data + ] + ) + + def _get_year_row(self, point: YearDataPoint): + return TableRow( + row_id=point.year, + cells=[ + EditableIntCell(point, "year", self.on_year_edited), + *( + self._get_metric_cell(metric, point) + for metric in self.project.all_conditions + ), + ], + ) + + def _get_metric_cell(self, metric: Metric, point: YearDataPoint): + return MetricValueCell( + metric, + point.get_or_add_data(metric.id), + on_finished_editing=self.on_metric_value_edited, + ) + + def on_year_edited(self, _): + for metric in self.project.all_conditions: + self.project.values_scenario.recalculate_values(metric.id) + + self.project.values_scenario.sort_yearly_data() + self.project.notify_scenarios_changed() + + def on_metric_value_edited(self, cell: MetricValueCell): + self.project.values_scenario.recalculate_values(cell.metric.id) + self.project.notify_scenarios_changed() + + def on_scenario_changed(self, _): + self.project.values_scenario_id = ( + self.scenario_dropdown.value + if self.scenario_dropdown.value in self.project.scenarios_by_id + else None + ) + self.redraw() + + def on_add_year(self): + if self.project.values_scenario is None: + return + + scenario = self.project.values_scenario + + year = datetime.datetime.now().year + year_count = len(scenario.yearly_data) + + if year_count > 0: + year = scenario.yearly_data[year_count - 1].year + 1 + + scenario.get_or_add_year(year) + for metric in self.project.all_conditions: + scenario.recalculate_values(metric.id) + + self.project.notify_scenarios_changed() + + def on_delete_years(self, rows: list[TableRow]): + def is_year(data: YearDataPoint, year: int): + return data.year == year + + for row in rows: + row_year = int(row.row_id) + + data_index = find_index( + self.project.values_scenario.yearly_data, + partial(is_year, year=row_year), + ) + if data_index is None: + continue + self.project.values_scenario.yearly_data.pop(data_index) + + self.project.notify_scenarios_changed() + + def redraw(self): + self.update_scenario_table() + self.update_dropdown() + self.update_values_table() + self.update() diff --git a/source/package/pathways_app/pathways_app/controls/scenarios_panel.py b/source/package/pathways_app/pathways_app/controls/scenarios_panel.py deleted file mode 100644 index 773e42c..0000000 --- a/source/package/pathways_app/pathways_app/controls/scenarios_panel.py +++ /dev/null @@ -1,70 +0,0 @@ -import flet as ft -from controls.styled_button import StyledButton -from controls.styled_dropdown import StyledDropdown -from controls.styled_table import StyledTable, TableCell, TableColumn - -from adaptation_pathways.app.model.pathways_project import PathwaysProject - -from .styled_button import StyledButton -from .styled_dropdown import StyledDropdown -from .styled_table import StyledTable - - -class ScenariosPanel(ft.Column): - def __init__(self, project: PathwaysProject): - super().__init__( - expand=True, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - scroll=ft.ScrollMode.AUTO, - ) - - self.project = project - year_range = range(self.project.start_year, self.project.end_year) - current_scenario = self.project.get_scenario(self.project.selected_scenario_id) - if current_scenario is None: - return - - self.controls = [ - ft.Text("!! UNDER CONSTRUCTION !!", color="#FF0000"), - ft.Row( - expand=False, - controls=[ - StyledDropdown( - value=current_scenario.name, - options=[ - ft.dropdown.Option(key=scenario.id, text=scenario.name) - for scenario in self.project.sorted_scenarios - ], - ), - ft.Container(expand=True), - StyledButton("New", icon=ft.icons.ADD_CIRCLE_OUTLINE), - ], - ), - StyledTable( - columns=[ - TableColumn(label="Year"), - *( - TableColumn(label=metric.name) - for metric in self.project.sorted_conditions - ), - ], - rows=[ - [ - TableCell(ft.Text(year)), - *( - TableCell( - ft.Text(data or "None") - ) - for data in [ - current_scenario.get_data(year, metric) - for metric in self.project.sorted_conditions - ] - ), - ] - for year in year_range - ], - ), - ] - - def redraw(self): - pass diff --git a/source/package/pathways_app/pathways_app/controls/sortable_header.py b/source/package/pathways_app/pathways_app/controls/sortable_header.py index 6a0044b..cee2027 100644 --- a/source/package/pathways_app/pathways_app/controls/sortable_header.py +++ b/source/package/pathways_app/pathways_app/controls/sortable_header.py @@ -1,8 +1,9 @@ from enum import Enum +from typing import Callable import flet as ft -from adaptation_pathways.app.model.sorting import SortingInfo, SortTarget +from adaptation_pathways.app.model.sorting import SortingInfo from .. import theme @@ -16,11 +17,11 @@ class SortMode(Enum): def get_icon(self): match self: case SortMode.ASCENDING: - return ft.icons.KEYBOARD_ARROW_UP + return ft.Icons.KEYBOARD_ARROW_UP case SortMode.DESCENDING: - return ft.icons.KEYBOARD_ARROW_DOWN + return ft.Icons.KEYBOARD_ARROW_DOWN case SortMode.UNSORTED: - return ft.icons.UNFOLD_MORE + return ft.Icons.UNFOLD_MORE case _: return None @@ -31,7 +32,7 @@ def __init__( sort_key: str, name: str, sort_mode: SortMode = SortMode.NONE, - on_sort=None, + on_sort: Callable[["SortableHeader"], None] | None = None, expand: bool | int | None = True, col: int | None = None, bgcolor: str | None = None, @@ -61,9 +62,6 @@ def __init__( ) def on_click(_): - if self.sort_mode == SortMode.NONE: - pass - new_sort_mode = ( SortMode.ASCENDING if self.sort_mode == SortMode.UNSORTED @@ -78,18 +76,17 @@ def on_click(_): if on_sort is not None: on_sort(self) - self.on_click = on_click + if self.sort_mode != SortMode.NONE: + self.on_click = on_click @classmethod def get_sort_mode(cls, sort_info: SortingInfo) -> SortMode: - if sort_info.target is SortTarget.NONE: - return SortMode.NONE return SortMode.ASCENDING if sort_info.ascending else SortMode.DESCENDING def set_sort_mode(self, sort_mode: SortMode): self.sort_mode = sort_mode self.update_icon() - # self.icon.update() + self.icon.update() def update_icon(self): self.icon.name = self.sort_mode.get_icon() diff --git a/source/package/pathways_app/pathways_app/controls/styled_dropdown.py b/source/package/pathways_app/pathways_app/controls/styled_dropdown.py index 75215ff..eb99f76 100644 --- a/source/package/pathways_app/pathways_app/controls/styled_dropdown.py +++ b/source/package/pathways_app/pathways_app/controls/styled_dropdown.py @@ -1,8 +1,7 @@ # pylint: disable=too-many-arguments import flet as ft - -from .. import theme -from ..utils import index_of_first +import theme +from utils import find_index class StyledDropdown(ft.Dropdown): @@ -21,7 +20,7 @@ def __init__( value=value, text_style=text_style, expand=False, - options=options, + options=[], width=width, bgcolor=theme.colors.true_white, content_padding=ft.padding.symmetric(4, 8), @@ -32,31 +31,43 @@ def __init__( on_blur=on_blur, ) - def update_icon(): - if option_icons is None: - return - - option_index = index_of_first(options, lambda el: el.key == self.value) - if option_index is not None: - self.prefix = ft.Row( - spacing=5, - controls=[ - ft.Icon( - option_icons[option_index], - color=theme.colors.primary_dark, - expand=False, - ), - ft.Text(self.value, style=text_style), - ], - ) - - def on_value_changed(e): - update_icon() - self.update() - - if on_change is not None: - on_change(e) - - self.prefix = None - update_icon() - self.on_change = on_value_changed + self.prefix = ft.Row( + spacing=5, + controls=[], + ) + self.text_style = text_style + self.set_options(options, option_icons) + self.change_callback = on_change + self.on_change = self.on_value_changed + + def update_icon(self): + if self.option_icons is None: + return + + option_index = find_index(self.options, lambda el: el.key == self.value) + if option_index is not None: + self.prefix.visible = True + self.prefix.controls = [ + ft.Icon( + self.option_icons[option_index], + color=theme.colors.primary_dark, + expand=False, + ), + ft.Text(self.value, style=self.text_style), + ] + else: + self.prefix.visible = False + + def set_options( + self, options: list[ft.dropdown.Option], icons: list[str] | None = None + ): + self.options = options + self.option_icons = icons + self.update_icon() + + def on_value_changed(self, e): + self.update_icon() + self.update() + + if self.change_callback is not None: + self.change_callback(e) diff --git a/source/package/pathways_app/pathways_app/controls/styled_table.py b/source/package/pathways_app/pathways_app/controls/styled_table.py index 79d6d8f..bd5c54c 100644 --- a/source/package/pathways_app/pathways_app/controls/styled_table.py +++ b/source/package/pathways_app/pathways_app/controls/styled_table.py @@ -3,6 +3,8 @@ import flet as ft import theme +from pathways_app.controls.styled_button import StyledButton + from .sortable_header import SortableHeader, SortMode @@ -11,140 +13,204 @@ def __init__( self, label: str, key: str | None = None, - on_sort: Callable[[None], None] | None = None, expand: bool | int | None = True, + sortable: bool = True, width: int | None = None, alignment: ft.Alignment | None = ft.alignment.center_left, ): - self.label = label - self.on_sort = on_sort self.expand = expand self.key = label if key is None else key self.alignment = alignment self.width = width + self.sortable = sortable class TableCell: def __init__( self, control: ft.Control, - sort_value: int | float | None = None, + sort_value: int | float | str | None = None, ): self.control = control self.sort_value = sort_value + self.on_sort_value_changed: Callable[[], None] | None = None + + def set_sort_value(self, value: int | float | str | None): + self.sort_value = value + if self.on_sort_value_changed is not None: + self.on_sort_value_changed() + + +class TableRow: + def __init__(self, row_id: str, cells: list[TableCell], can_be_deleted=True): + self.row_id = row_id + self.cells = cells + self.can_be_deleted = can_be_deleted + + +class ColumnData: + def __init__( + self, + column: TableColumn, + height: int, + on_sort: Callable[[SortableHeader], None] | None = None, + ): + self.column = column + self.header = SortableHeader( + name=column.label, + sort_key=column.key, + sort_mode=(SortMode.UNSORTED if column.sortable else SortMode.NONE), + on_sort=on_sort, + expand=column.expand, + bgcolor=theme.colors.primary_lightest, + width=column.width, + height=height, + ) + + +class RowData: + def __init__( + self, + row: TableRow, + initial_index: int, + columns: list[ColumnData], + height: int, + on_selected: Callable[["RowData"], None] | None = None, + ): + self.row = row + self.initial_index = initial_index + self.on_selected = on_selected + self.checkbox = ft.Checkbox( + value=False, + on_change=self.on_checkbox_clicked, + ) + self.height = height + self.control = ft.Container( + ft.Row( + [ + ft.Container( + content=self.checkbox, + padding=theme.variables.table_cell_padding, + width=40, + height=height, + ), + *( + self._create_cell_control(cell, index, columns) + for index, cell in enumerate(self.row.cells) + ), + ], + spacing=0, + ), + expand=True, + ) + + def on_checkbox_clicked(self, _): + if self.on_selected is None: + return + self.on_selected(self) + + def _create_cell_control( + self, cell: TableCell, index: int, columns: list[ColumnData] + ): + column = columns[index].column + return ft.Container( + content=cell.control, + expand=column.expand, + height=self.height, + width=column.width, + padding=None, + ) class StyledTable(ft.Container): def __init__( self, columns: list[TableColumn], - rows: list[list[TableCell]], + rows: list[TableRow], row_height=36, sort_column_index: int | None = None, - sort_ascending: bool | None = None, - on_sort: Callable[[int], None] | None = None, + sort_ascending: bool = True, + on_sorted: Callable[[], None] | None = None, + on_add: Callable[[], TableRow] | None = None, + add_label="Add", + on_delete: Callable[[], list[TableRow]] | None = None, + delete_label="Delete", + on_copy: Callable[[list[TableRow]], list[TableRow]] | None = None, + copy_label="Duplicate", + pre_operation_content: ft.Control | None = None, show_checkboxes=False, + expand: bool | int | None = True, ): super().__init__( - expand=True, - # horizontal_margin=0, - # show_checkbox_column=show_checkboxes, - # checkbox_horizontal_margin=theme.variables.table_cell_padding.left, - # columns=columns, - # rows=rows, - # bgcolor=theme.colors.true_white, - # sort_column_index=sort_column_index, - # sort_ascending=sort_ascending, - # column_spacing=20, - # data_row_min_height=0, - # data_row_max_height=row_height, - # data_row_color={ - # ft.ControlState.HOVERED: theme.colors.off_white, - # ft.ControlState.FOCUSED: theme.colors.off_white, - # ft.ControlState.SELECTED: theme.colors.primary_white, - # ft.ControlState.PRESSED: theme.colors.off_white, - # ft.ControlState.DRAGGED: theme.colors.off_white, - # ft.ControlState.SCROLLED_UNDER: theme.colors.off_white, - # }, - # divider_thickness=0, - # horizontal_lines=ft.BorderSide(1, theme.colors.primary_lightest), - # heading_row_height=30, - # heading_row_color=theme.colors.primary_lightest, - # heading_text_style=theme.text.table_header, - # border=ft.border.all(1, theme.colors.primary_light), + expand=expand, ) self.row_height = row_height self.sort_column_index = sort_column_index - self.show_checkboxes = show_checkboxes - self.on_sort = on_sort self.sort_ascending = sort_ascending - self.selected_row_indices: set[int] = set() + self.show_checkboxes = show_checkboxes + self.on_sorted = on_sorted + + self.on_add = on_add + self.on_copy = on_copy + self.on_delete = on_delete + + self.selected_row_ids: set[str] = set() + self.add_button = StyledButton( + add_label, ft.Icons.ADD_CIRCLE_OUTLINE, on_click=self.on_add_clicked + ) + self.copy_button = StyledButton( + copy_label, ft.Icons.ADD_CIRCLE_OUTLINE, on_click=self.on_copy_clicked + ) + self.delete_button = StyledButton( + delete_label, + ft.Icons.REMOVE_CIRCLE_OUTLINE, + on_click=self.on_delete_clicked, + ) + self.operation_row = ft.Row( + [self.delete_button, self.copy_button, self.add_button], + expand=True, + alignment=ft.MainAxisAlignment.END, + ) + if pre_operation_content is not None: + self.operation_row.controls.insert(0, pre_operation_content) + + self.operations = ft.Container(self.operation_row, height=36) + self.column_data: list[ColumnData] = [] + self.header_row = ft.Container( content=ft.Row([], expand=False, spacing=0), bgcolor=theme.colors.primary_lightest, border=ft.border.only( - # left=ft.BorderSide(1, theme.colors.primary_medium), - # right=ft.BorderSide(1, theme.colors.primary_medium), bottom=ft.BorderSide(1, theme.colors.primary_medium), ), - border_radius=ft.border_radius.only( - top_left=theme.variables.small_radius, - top_right=theme.variables.small_radius, - ), ) - self.rows = ft.Container( + + self.row_data: list[RowData] = [] + self.row_container = ft.Container( content=ft.Column([], expand=True, scroll=ft.ScrollMode.ALWAYS, spacing=0), - # bgcolor=theme.colors.true_white, - # border=ft.border.only( - # left=ft.BorderSide(1, theme.colors.primary_medium), - # right=ft.BorderSide(1, theme.colors.primary_medium), - # bottom=ft.BorderSide(1, theme.colors.primary_medium), - # ), expand=True, - border_radius=ft.border_radius.only( - bottom_left=theme.variables.small_radius, - bottom_right=theme.variables.small_radius, - ), ) - self.content = ft.Column([self.header_row, self.rows], spacing=0, expand=True) - self.row_data = rows - self.column_data = columns - self.set_columns(columns) - self.set_rows(rows) - def on_header_sorted(self, column_index: int, header: SortableHeader): - self.sort_ascending = header.sort_mode == SortMode.ASCENDING - self.sort_column_index = ( - None if header.sort_mode == SortMode.NONE else column_index + self.content = ft.Column( + [self.operations, self.header_row, self.row_container], + spacing=0, + expand=True, ) - if self.on_sort is not None: - self.on_sort(column_index) - - def on_row_selected(self, index: int): - if index in self.selected_row_indices: - self.selected_row_indices.remove(index) - else: - self.selected_row_indices.add(index) - - self.update_selected_rows() - self.update() - - def on_all_rows_selected(self): - if self.select_all_checkbox.value: - for index in range(len(self.row_data)): - self.selected_row_indices.add(index) - else: - self.selected_row_indices.clear() - self.update_selected_rows() - self.update() + self.set_columns(columns) + self.set_rows(rows) def set_columns(self, columns: list[TableColumn]): - self.column_data = columns + self.column_data = [ + ColumnData( + column=column, height=self.row_height, on_sort=self.on_header_sorted + ) + for column in columns + ] self.select_all_checkbox = ft.Checkbox( - on_change=lambda evt: self.on_all_rows_selected(), + on_change=self.on_all_rows_selected, ) self.header_row.content.controls = [ ft.Container( @@ -154,80 +220,140 @@ def set_columns(self, columns: list[TableColumn]): bgcolor=theme.colors.primary_lightest, padding=theme.variables.table_cell_padding, ), - *( - ( - SortableHeader( - sort_key=column.key, - name=column.label, - sort_mode=( - SortMode.NONE - if column.on_sort is None - else SortMode.UNSORTED - ), - on_sort=lambda header, i=index: self.on_header_sorted( - i, header - ), - expand=column.expand, - bgcolor=theme.colors.primary_lightest, - width=column.width, - height=self.row_height, - ) - ) - for index, column in enumerate(columns) - ), + *(column.header for column in self.column_data), ] + def set_rows(self, rows: list[TableRow]): + self.row_data = [ + RowData( + row=row, + initial_index=index, + columns=self.column_data, + height=self.row_height, + on_selected=self.on_row_selected, + ) + for index, row in enumerate(rows) + ] + + for row in self.row_data: + for cell in row.row.cells: + cell.on_sort_value_changed = self.sort_rows + + self.sort_rows() + self.update_selected_rows() + + def sort_rows(self): + self.row_data.sort(key=self.get_sort_value, reverse=not self.sort_ascending) + self.row_container.content.controls = [row.control for row in self.row_data] + self.update_selected_rows() + + def get_sort_value(self, row: RowData): + if self.sort_column_index is None: + return row.initial_index + + cell = row.row.cells[self.sort_column_index] + return cell.sort_value + + def on_header_sorted(self, header: SortableHeader): + column_index = None + for index, column in enumerate(self.column_data): + if column.header == header: + column_index = index + + self.sort_ascending = header.sort_mode != SortMode.DESCENDING + self.sort_column_index = ( + None if header.sort_mode == SortMode.UNSORTED else column_index + ) + + for index, column in enumerate(self.column_data): + if index != self.sort_column_index: + column.header.set_sort_mode( + SortMode.NONE + if column.header.sort_mode is SortMode.NONE + else SortMode.UNSORTED + ) + + self.sort_rows() + self.update() + + if self.on_sorted is not None: + self.on_sorted() + + def on_row_selected(self, row: RowData): + if row.row.row_id in self.selected_row_ids: + self.selected_row_ids.remove(row.row.row_id) + else: + self.selected_row_ids.add(row.row.row_id) + self.update_selected_rows() + self.update() + + def on_all_rows_selected(self, _): + if self.select_all_checkbox.value: + for row in self.row_data: + self.selected_row_ids.add(row.row.row_id) + else: + self.selected_row_ids.clear() + + self.update_selected_rows() + self.update() + def update_selected_rows(self): - self.select_all_checkbox.value = len(self.selected_row_indices) >= len( + self.copy_button.visible = ( + len(self.selected_row_ids) > 0 and self.on_copy is not None + ) + self.add_button.visible = ( + not self.copy_button.visible and self.on_add is not None + ) + + deletable_count = sum( + data.row.can_be_deleted and data.row.row_id in self.selected_row_ids + for data in self.row_data + ) + self.delete_button.visible = deletable_count > 0 and self.on_delete is not None + + self.select_all_checkbox.value = len(self.selected_row_ids) >= len( self.row_data ) - for index, row in enumerate(self.rows.content.controls): - row.bgcolor = ( - theme.colors.primary_white - if index in self.selected_row_indices + + for index, row in enumerate(self.row_data): + is_selected = row.row.row_id in self.selected_row_ids + row.control.bgcolor = ( + theme.colors.row_selected + if is_selected else ( theme.colors.true_white if index % 2 == 0 else theme.colors.off_white ) ) - checkbox = row.content.controls[0].content - checkbox.value = index in self.selected_row_indices + row.checkbox.value = is_selected - def set_rows(self, rows: list[list[TableCell]]): - self.row_data = rows - self.rows.content.controls = [ - ft.Container( - ft.Row( - [ - ft.Container( - content=ft.Checkbox( - value=index in self.selected_row_indices, - on_change=lambda evt, i=index: self.on_row_selected(i), - ), - padding=theme.variables.table_cell_padding, - width=40, - height=self.row_height, - ), - *( - self._create_row(index, cell) - for index, cell in enumerate(row) - ), - ], - spacing=0, - ), - expand=True, - ) - for index, row in enumerate(rows) + def on_add_clicked(self, _): + if self.on_add is None: + return + + self.on_add() + + def on_copy_clicked(self, _): + if self.on_copy is None: + return + + rows_to_copy = [ + data.row + for data in self.row_data + if data.row.row_id in self.selected_row_ids ] - self.update_selected_rows() + self.selected_row_ids.clear() + self.on_copy(rows_to_copy) - def _create_row(self, index: int, cell: TableCell) -> ft.Container: - column = self.column_data[index] - return ft.Container( - content=cell.control, - expand=column.expand, - height=self.row_height, - width=column.width, - padding=None, - ) + def on_delete_clicked(self, _): + if self.on_delete is None: + return + + rows_to_delete = [ + data.row + for data in self.row_data + if data.row.can_be_deleted and data.row.row_id in self.selected_row_ids + ] + self.selected_row_ids.clear() + self.on_delete(rows_to_delete) diff --git a/source/package/pathways_app/pathways_app/controls/tabbed_panel.py b/source/package/pathways_app/pathways_app/controls/tabbed_panel.py index 92b6306..740b25b 100644 --- a/source/package/pathways_app/pathways_app/controls/tabbed_panel.py +++ b/source/package/pathways_app/pathways_app/controls/tabbed_panel.py @@ -4,85 +4,76 @@ from .header import SectionHeader -class TabbedPanel(ft.Column): - selected_index: int = 0 - tab_buttons: list[ft.TextButton] - content: ft.Container +class TabbedPanel(ft.Container): def __init__( self, tabs: list[tuple[SectionHeader, ft.Control]], selected_index: int ): - def get_tab_bgcolor(index: int): - if self.selected_index == index: - return theme.colors.primary_white - return theme.colors.primary_white - - def get_opacity(index: int): - return 1 if self.selected_index == index else 0.5 - - def on_click(e): - self.selected_index = self.tab_buttons.index(e.control) - - for [index, tab] in enumerate(self.tab_buttons): - tab.content.bgcolor = get_tab_bgcolor(index) - tab.content.opacity = get_opacity(index) - tab.content.update() - - self.content.content = tabs[self.selected_index][1] - self.content.update() - super().__init__( - expand=True, spacing=0, horizontal_alignment=ft.CrossAxisAlignment.STRETCH + expand=True, + bgcolor=theme.colors.primary_light, + border=ft.border.all(1, theme.colors.primary_light), + border_radius=ft.border_radius.all(theme.variables.small_radius), ) self.selected_index = selected_index + self.tabs = tabs self.tab_buttons = [ - ft.TextButton( - expand=True, - content=ft.Container( - expand=True, - content=tab[0], - padding=10, - bgcolor=get_tab_bgcolor(index), - opacity=get_opacity(index), - border_radius=ft.border_radius.only( - top_left=theme.variables.small_radius, - top_right=theme.variables.small_radius, - ), - border=ft.border.only( - left=ft.BorderSide(1, theme.colors.primary_light), - right=ft.BorderSide(1, theme.colors.primary_light), - top=ft.BorderSide(1, theme.colors.primary_light), - ), - ), - style=ft.ButtonStyle(padding=0), - on_click=on_click, + ft.Container( + content=tab[0], + padding=ft.padding.symmetric(10, 15), + opacity=self.get_opacity(index), + bgcolor=self.get_tab_bgcolor(index), + on_click=self.on_tab_clicked, ) - for [index, tab] in enumerate(tabs) + for index, tab in enumerate(tabs) ] - self.content = ft.Container( + self.tab_content = ft.Container( expand=True, content=tabs[selected_index][1], margin=0, padding=theme.variables.panel_padding, bgcolor=theme.colors.primary_white, - border=ft.border.only( - left=ft.BorderSide(1, theme.colors.primary_light), - right=ft.BorderSide(1, theme.colors.primary_light), - bottom=ft.BorderSide(1, theme.colors.primary_light), - ), - border_radius=ft.border_radius.only( - bottom_left=theme.variables.small_radius, - bottom_right=theme.variables.small_radius, - ), ) - self.controls = [ - ft.Row( - controls=self.tab_buttons, - spacing=3, - ), - self.content, - ] + self.content = ft.Row( + expand=True, + vertical_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[ + ft.Container( + expand=False, + bgcolor=theme.colors.primary_lightest, + content=ft.Column( + expand=True, + controls=self.tab_buttons, + spacing=0, + ), + padding=0, + border=ft.border.only( + right=ft.BorderSide(1, theme.colors.primary_lighter) + ), + ), + self.tab_content, + ], + spacing=0, + ) + + def get_tab_bgcolor(self, index: int): + if self.selected_index == index: + return theme.colors.primary_white + return None + + def get_opacity(self, index: int): + return 1 if self.selected_index == index else 0.5 + + def on_tab_clicked(self, e): + self.selected_index = self.tab_buttons.index(e.control) + + for [index, tab] in enumerate(self.tab_buttons): + tab.bgcolor = self.get_tab_bgcolor(index) + tab.opacity = self.get_opacity(index) + + self.tab_content.content = self.tabs[self.selected_index][1] + self.update() diff --git a/source/package/pathways_app/pathways_app/controls/unit_cell.py b/source/package/pathways_app/pathways_app/controls/unit_cell.py index dc5cdff..fa5ab61 100644 --- a/source/package/pathways_app/pathways_app/controls/unit_cell.py +++ b/source/package/pathways_app/pathways_app/controls/unit_cell.py @@ -2,11 +2,10 @@ import flet as ft import theme -from .styled_table import TableCell from adaptation_pathways.app.model.metric import Metric, MetricUnit, default_units -from .. import theme +from .styled_table import TableCell class MetricUnitCell(TableCell): @@ -19,6 +18,7 @@ def __init__( def on_default_metric_selected(unit: str): self.metric.unit_or_default = unit + self.sort_value = metric.unit.display_name if on_unit_change is not None: on_unit_change(self) @@ -26,170 +26,170 @@ def create_unit_button(unit: MetricUnit): return ft.MenuItemButton( key=unit.symbol, content=ft.Text(unit.name, style=theme.text.normal), - style=theme.buttons.submenu, + style=theme.buttons.submenu_button, on_click=lambda e, u=unit: on_default_metric_selected(u.symbol), ) - def create_unit_submenu(name: str, controls: ft.Control): - return ft.SubmenuButton( - ft.Text(name, style=theme.text.normal), - style=theme.buttons.submenu, - controls=controls, + def create_unit_submenu(name: str, controls: list[ft.Control]): + return ft.PopupMenuItem( + content=ft.SubmenuButton( + ft.Text(name, style=theme.text.normal, expand=True), + style=theme.buttons.submenu_button, + menu_style=theme.buttons.nested_submenu, + controls=controls, + expand=True, + ), ) def create_submenu_header(text: str): return ft.Container( - ft.Text(value=text.upper(), style=theme.text.submenu_header), + content=ft.Text( + value=text.upper(), style=theme.text.submenu_header, expand=True + ), padding=ft.padding.only(left=4, right=4, top=8, bottom=4), bgcolor=theme.colors.true_white, expand=True, ) super().__init__( - ft.MenuBar( - style=theme.buttons.menu_button, - controls=[ - ft.SubmenuButton( - expand=False, - style=theme.buttons.submenu, - content=ft.Text( - metric.unit.display_name, style=theme.text.normal - ), - controls=[ - create_unit_submenu( - "Length", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.length.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.length.imperial - ), - ], - ), - create_unit_submenu( - "Area", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.area.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.area.imperial - ), - ], - ), - create_unit_submenu( - "Volume", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.volume.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.volume.imperial - ), - ], - ), - create_unit_submenu( - "Temperature", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.temperature.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.temperature.imperial - ), - ], - ), - create_unit_submenu( - "Velocity", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.velocity.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.velocity.imperial - ), - ], - ), - create_unit_submenu( - "Acceleration", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.acceleration.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.acceleration.imperial - ), - ], - ), - create_unit_submenu( - "Mass/Weight", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.mass_weight.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.mass_weight.imperial - ), - ], - ), - create_unit_submenu( - "Time", - [ - *( - create_unit_button(unit) - for unit in default_units.time - ), - ], - ), - create_unit_submenu( - "Currency", - [ - *( - create_unit_button(unit) - for unit in default_units.currency - ), - ], - ), - create_unit_submenu( - "Relative", - [ - *( - create_unit_button(unit) - for unit in default_units.relative - ), - ], + ft.PopupMenuButton( + expand=True, + style=theme.buttons.unit_menu, + menu_padding=ft.padding.all(0), + menu_position=ft.PopupMenuPosition.UNDER, + # menu_style=theme.buttons.submenu, + content=ft.Text(metric.unit.display_name, style=theme.text.normal), + items=[ + create_unit_submenu( + "Length", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.length.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.length.imperial + ), + ], + ), + create_unit_submenu( + "Area", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.area.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.area.imperial + ), + ], + ), + create_unit_submenu( + "Volume", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.volume.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.volume.imperial + ), + ], + ), + create_unit_submenu( + "Temperature", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.temperature.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.temperature.imperial + ), + ], + ), + create_unit_submenu( + "Velocity", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.velocity.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.velocity.imperial + ), + ], + ), + create_unit_submenu( + "Acceleration", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.acceleration.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.acceleration.imperial + ), + ], + ), + create_unit_submenu( + "Mass/Weight", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.mass_weight.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.mass_weight.imperial + ), + ], + ), + create_unit_submenu( + "Time", + [ + *(create_unit_button(unit) for unit in default_units.time), + ], + ), + create_unit_submenu( + "Currency", + [ + *( + create_unit_button(unit) + for unit in default_units.currency + ), + ], + ), + create_unit_submenu( + "Relative", + [ + *( + create_unit_button(unit) + for unit in default_units.relative ), ], - ) + ), ], ) ) + self.sort_value = metric.unit.display_name diff --git a/source/package/pathways_app/pathways_app/example.py b/source/package/pathways_app/pathways_app/example.py index e3e725d..6e4e375 100644 --- a/source/package/pathways_app/pathways_app/example.py +++ b/source/package/pathways_app/pathways_app/example.py @@ -1,10 +1,9 @@ import flet as ft from adaptation_pathways.app.model.action import Action, MetricEffect -from adaptation_pathways.app.model.metric import Metric, MetricValue +from adaptation_pathways.app.model.metric import Metric, MetricValue, MetricValueState from adaptation_pathways.app.model.pathway import Pathway from adaptation_pathways.app.model.pathways_project import PathwaysProject -from adaptation_pathways.app.model.scenario import Scenario metric_sea_level_rise = Metric( @@ -29,7 +28,7 @@ "current-situation", name="Current", color="#999999", - icon=ft.icons.HOME, + icon=ft.Icons.HOME, metric_data={ metric_sea_level_rise.id: MetricEffect(0), metric_cost.id: MetricEffect(0), @@ -41,7 +40,7 @@ "sea-wall", name="Sea Wall", color="#5A81DB", - icon=ft.icons.WATER, + icon=ft.Icons.WATER, metric_data={ metric_sea_level_rise.id: MetricEffect(10), metric_cost.id: MetricEffect(100000), @@ -53,7 +52,7 @@ "pump", name="Pump", color="#44C1E1", - icon=ft.icons.WATER_DROP_SHARP, + icon=ft.Icons.WATER_DROP_SHARP, metric_data={ metric_sea_level_rise.id: MetricEffect(5), metric_cost.id: MetricEffect(50000), @@ -65,7 +64,7 @@ id="nature-based", name="Nature-Based", color="#E0C74B", - icon=ft.icons.PARK, + icon=ft.Icons.PARK, metric_data={ metric_sea_level_rise.id: MetricEffect(1), metric_cost.id: MetricEffect(5000), @@ -74,6 +73,9 @@ ) root_pathway = Pathway(action_root.id) +root_pathway.metric_data[metric_sea_level_rise.id] = MetricValue(0) +root_pathway.metric_data[metric_cost.id] = MetricValue(0) +root_pathway.metric_data[metric_habitat_health.id] = MetricValue(0) project = PathwaysProject( project_id="test-id", @@ -83,16 +85,7 @@ end_year=2054, conditions=[metric_sea_level_rise], criteria=[metric_cost], - scenarios=[ - Scenario( - id="scenario-1", - name="Best Case", - metric_data_over_time={ - 2025: {metric_sea_level_rise.id: MetricValue(13)}, - 2026: {metric_sea_level_rise.id: MetricValue(21)}, - }, - ) - ], + scenarios=[], actions=[action_pump, action_sea_wall, action_nature_based], pathways=[root_pathway], root_action=action_root, @@ -102,3 +95,12 @@ project.create_pathway(action_pump.id, root_pathway.id) project.create_pathway(action_sea_wall.id, root_pathway.id) project.create_pathway(action_nature_based.id, root_pathway.id) + +best_case_scenario = project.create_scenario("Best Case") +best_case_scenario.set_data( + 2025, metric_sea_level_rise.id, MetricValue(0, MetricValueState.OVERRIDE) +) +best_case_scenario.set_data( + 2050, metric_sea_level_rise.id, MetricValue(10, MetricValueState.OVERRIDE) +) +project.values_scenario_id = best_case_scenario.id diff --git a/source/package/pathways_app/pathways_app/theme.py b/source/package/pathways_app/pathways_app/theme.py index 4aeb32b..c4401fe 100644 --- a/source/package/pathways_app/pathways_app/theme.py +++ b/source/package/pathways_app/pathways_app/theme.py @@ -13,8 +13,9 @@ class DefaultThemeColors: primary_darker = "#160E59" secondary_light = "#91E0EC" secondary_medium = "#48BDCF" - calculated_bg = "#208888AA" + calculated_bg = "#60CCCCEE" calculated_icon = "#8888AA" + row_selected = "#D5F9FF" colors = DefaultThemeColors() @@ -35,35 +36,35 @@ class DefaultThemeColors: ] action_icons = [ - ft.icons.ACCESS_TIME_FILLED, - ft.icons.AC_UNIT, - ft.icons.ACCOUNT_BALANCE, - ft.icons.AIRPLANEMODE_ACTIVE, - ft.icons.BATTERY_FULL, - ft.icons.BRIGHTNESS_HIGH_SHARP, - ft.icons.BUILD_SHARP, - ft.icons.CALCULATE, - ft.icons.CAMERA_ALT, - ft.icons.CELL_TOWER, - ft.icons.CLOUD_SHARP, - ft.icons.CORONAVIRUS, - ft.icons.DEVICE_THERMOSTAT, - ft.icons.DIRECTIONS_BIKE, - ft.icons.DIRECTIONS_BOAT, - ft.icons.DIRECTIONS_CAR, - ft.icons.DIRECTIONS_BUS, - ft.icons.DIRECTIONS_TRAIN, - ft.icons.DIRECTIONS_WALK, - ft.icons.DISCOUNT_SHARP, - ft.icons.DIVERSITY_3_SHARP, - ft.icons.EMERGENCY_SHARP, - ft.icons.FAMILY_RESTROOM_SHARP, - ft.icons.FASTFOOD_SHARP, - ft.icons.FAVORITE, - ft.icons.FILTER_VINTAGE_OUTLINED, - ft.icons.FLASH_ON, - ft.icons.FLOOD_SHARP, - ft.icons.FOREST_SHARP, + ft.Icons.ACCESS_TIME_FILLED, + ft.Icons.AC_UNIT, + ft.Icons.ACCOUNT_BALANCE, + ft.Icons.AIRPLANEMODE_ACTIVE, + ft.Icons.BATTERY_FULL, + ft.Icons.BRIGHTNESS_HIGH_SHARP, + ft.Icons.BUILD_SHARP, + ft.Icons.CALCULATE, + ft.Icons.CAMERA_ALT, + ft.Icons.CELL_TOWER, + ft.Icons.CLOUD_SHARP, + ft.Icons.CORONAVIRUS, + ft.Icons.DEVICE_THERMOSTAT, + ft.Icons.DIRECTIONS_BIKE, + ft.Icons.DIRECTIONS_BOAT, + ft.Icons.DIRECTIONS_CAR, + ft.Icons.DIRECTIONS_BUS, + ft.Icons.DIRECTIONS_TRAIN, + ft.Icons.DIRECTIONS_WALK, + ft.Icons.DISCOUNT_SHARP, + ft.Icons.DIVERSITY_3_SHARP, + ft.Icons.EMERGENCY_SHARP, + ft.Icons.FAMILY_RESTROOM_SHARP, + ft.Icons.FASTFOOD_SHARP, + ft.Icons.FAVORITE, + ft.Icons.FILTER_VINTAGE_OUTLINED, + ft.Icons.FLASH_ON, + ft.Icons.FLOOD_SHARP, + ft.Icons.FOREST_SHARP, ] @@ -74,6 +75,7 @@ class DefaultThemeVariables: panel_padding = 10 table_cell_padding = ft.padding.symmetric(4, 8) calculated_icon_size = 16 + icon_button_size = 16 variables = DefaultThemeVariables() @@ -111,6 +113,10 @@ class DefaultThemeTextStyles: font_family=font_family, size=12, color=colors.primary_lighter ) + menu_button = ft.TextStyle( + font_family=font_family, size=14, color=colors.true_white + ) + button = ft.TextStyle( font_family=font_family_semibold, size=12, @@ -137,6 +143,13 @@ class DefaultThemeTextStyles: class DefaultThemeIcons: globe = "icons/icon_globe.svg" + actions = ft.Icons.CONSTRUCTION_OUTLINED + metrics = ft.Icons.TUNE + scenarios = ft.Icons.PUBLIC + maximize = ft.Icons.OPEN_IN_FULL + minimize = ft.Icons.CLOSE_FULLSCREEN + sidebar_open = ft.Icons.VIEW_SIDEBAR_OUTLINED + sidebar_closed = ft.Icons.VIEW_SIDEBAR_OUTLINED icon = "images/deltares-logo-white.png" @@ -151,6 +164,14 @@ class DefaultThemeButtons: shape=ft.RoundedRectangleBorder(radius=variables.small_radius), ) + menu_bar_button = ft.ButtonStyle( + bgcolor=colors.primary_dark, + color=colors.true_white, + padding=ft.padding.symmetric(0, 20), + shape=ft.RoundedRectangleBorder(radius=variables.small_radius), + mouse_cursor=ft.MouseCursor.CLICK, + ) + menu_button = ft.MenuStyle( alignment=ft.alignment.center_left, bgcolor=colors.true_white, @@ -162,14 +183,50 @@ class DefaultThemeButtons: mouse_cursor=ft.MouseCursor.CELL, ) - submenu = ft.ButtonStyle( - color=colors.primary_dark, + submenu = ft.MenuStyle( + alignment=ft.alignment.bottom_left, bgcolor=colors.true_white, + shadow_color=colors.true_white, + surface_tint_color=colors.true_white, + padding=ft.padding.symmetric(0, 0), + shape=ft.RoundedRectangleBorder(radius=variables.small_radius), + # side=ft.BorderSide(1, color=colors.primary_medium), + mouse_cursor=ft.MouseCursor.CELL, + # alignment=ft.alignment.center_left, + # bgcolor=colors.true_white, + # shape=ft.RoundedRectangleBorder(radius=variables.small_radius), + # side=ft.BorderSide(1, color=colors.primary_medium), + ) + + submenu_button = ft.ButtonStyle( + color=colors.primary_dark, shape=ft.RoundedRectangleBorder(radius=variables.small_radius), padding=ft.padding.symmetric(4, 6), # side=ft.BorderSide(1, color=colors.primary_medium), ) + nested_submenu = ft.MenuStyle( + alignment=ft.alignment.top_right, + bgcolor=colors.true_white, + shadow_color=colors.true_white, + surface_tint_color=colors.true_white, + padding=ft.padding.symmetric(0, 0), + shape=ft.RoundedRectangleBorder(radius=variables.small_radius), + # side=ft.BorderSide(1, color=colors.primary_medium), + mouse_cursor=ft.MouseCursor.CELL, + # alignment=ft.alignment.center_left, + # bgcolor=colors.true_white, + # shape=ft.RoundedRectangleBorder(radius=variables.small_radius), + # side=ft.BorderSide(1, color=colors.primary_medium), + ) + + unit_menu = ft.ButtonStyle( + color=colors.primary_dark, + shape=ft.RoundedRectangleBorder(radius=0), + padding=ft.padding.symmetric(0, 0), + # side=ft.BorderSide(1, color=colors.primary_medium), + ) + buttons = DefaultThemeButtons() diff --git a/source/package/pathways_app/pathways_app/utils.py b/source/package/pathways_app/pathways_app/utils.py index 64b0cea..3c96862 100644 --- a/source/package/pathways_app/pathways_app/utils.py +++ b/source/package/pathways_app/pathways_app/utils.py @@ -1,7 +1,10 @@ -from typing import Any, Callable +from typing import Callable, TypeVar -def index_of_first(element_list: list[Any], pred: Callable[[Any], bool]) -> int | None: +T = TypeVar("T") + + +def find_index(element_list: list[T], pred: Callable[[T], bool]) -> int | None: for index, value in enumerate(element_list): if pred(value): return index From c956def31837c5dda83b2081c15f8a7752f932c3 Mon Sep 17 00:00:00 2001 From: AJ Kolenc Date: Sun, 26 Jan 2025 16:28:02 +0100 Subject: [PATCH 06/12] Rebase fixes --- source/package/pathways_app/main.py | 40 ---- .../pathways_app/pathways_app/cli/app.py | 117 ++-------- .../pathways_app/{ => pathways_app}/config.py | 0 .../pathways_app/controls/actions_editor.py | 12 +- .../pathways_app/controls/actions_panel.py | 205 ------------------ .../pathways_app/controls/editable_cell.py | 2 +- .../pathways_app/controls/editor_page.py | 19 +- .../pathways_app/controls/graph_editor.py | 9 +- .../pathways_app/controls/menu_bar.py | 9 +- .../pathways_app/controls/metric_value.py | 7 +- .../pathways_app/controls/metrics_editor.py | 27 +-- .../pathways_app/controls/metrics_panel.py | 159 -------------- .../pathways_app/controls/pathways_editor.py | 11 +- .../pathways_app/controls/pathways_panel.py | 174 --------------- .../pathways_app/controls/scenarios_editor.py | 17 +- .../pathways_app/controls/styled_dropdown.py | 5 +- .../pathways_app/controls/styled_table.py | 10 +- .../pathways_app/controls/unit_cell.py | 2 +- .../{app.py => pathways_app/pathways_app.py} | 7 +- 19 files changed, 75 insertions(+), 757 deletions(-) rename source/package/pathways_app/{ => pathways_app}/config.py (100%) delete mode 100644 source/package/pathways_app/pathways_app/controls/actions_panel.py delete mode 100644 source/package/pathways_app/pathways_app/controls/metrics_panel.py delete mode 100644 source/package/pathways_app/pathways_app/controls/pathways_panel.py rename source/package/pathways_app/{app.py => pathways_app/pathways_app.py} (88%) diff --git a/source/package/pathways_app/main.py b/source/package/pathways_app/main.py index 4500eb4..1851c8b 100644 --- a/source/package/pathways_app/main.py +++ b/source/package/pathways_app/main.py @@ -2,46 +2,6 @@ import flet as ft from pathways_app.cli.app import main -import theme -from controls.editor_page import EditorPage -from controls.menu_bar import MenuBar -from pathways_app.app import App - - -locale.setlocale(locale.LC_ALL, "") - - -def main(page: ft.Page): - # bitdojo_window could make a custom title bar - # page.window.frameless = True - # page.window.width = 1200 - # page.window.height = 800 - page.theme = theme.theme - page.theme_mode = ft.ThemeMode.LIGHT - - page.window.resizable = True - page.window.alignment = ft.alignment.center - page.window.maximized = True - - page.title = "Pathways Generator" - page.fonts = theme.fonts - page.bgcolor = theme.colors.primary_darker - page.padding = 1 - page.spacing = 0 - - app = App(page) - page.appbar = MenuBar(app) - page.overlay.append(app.file_picker) - - page.add( - ft.Container( - expand=True, - padding=theme.variables.panel_spacing, - content=EditorPage(app.project), - bgcolor=theme.colors.primary_lighter, - border_radius=ft.border_radius.only(bottom_left=8, bottom_right=8), - ) - ) logging.basicConfig(level=logging.CRITICAL) diff --git a/source/package/pathways_app/pathways_app/cli/app.py b/source/package/pathways_app/pathways_app/cli/app.py index c86ef33..c80189e 100644 --- a/source/package/pathways_app/pathways_app/cli/app.py +++ b/source/package/pathways_app/pathways_app/cli/app.py @@ -2,31 +2,26 @@ import flet as ft -from .. import example, theme -from ..controls.actions_panel import ActionsPanel -from ..controls.graph_panel import GraphPanel -from ..controls.header import SectionHeader +from .. import theme +from ..controls.editor_page import EditorPage from ..controls.menu_bar import MenuBar -from ..controls.metrics_panel import MetricsPanel -from ..controls.panel import Panel -from ..controls.pathways_panel import PathwaysPanel -from ..controls.scenarios_panel import ScenariosPanel -from ..controls.tabbed_panel import TabbedPanel +from ..pathways_app import PathwaysApp locale.setlocale(locale.LC_ALL, "") def main(page: ft.Page): - page.theme = theme.theme - page.theme_mode = ft.ThemeMode.LIGHT - # bitdojo_window could make a custom title bar # page.window.frameless = True + # page.window.width = 1200 + # page.window.height = 800 + page.theme = theme.theme + page.theme_mode = ft.ThemeMode.LIGHT - page.window.width = 1200 - page.window.height = 800 page.window.resizable = True + page.window.alignment = ft.alignment.center + page.window.maximized = True page.title = "Pathways Generator" page.fonts = theme.fonts @@ -34,101 +29,15 @@ def main(page: ft.Page): page.padding = 1 page.spacing = 0 - project = example.project - - page.appbar = MenuBar(project) - metrics_panel = MetricsPanel(project) - metrics_tab = (SectionHeader(ft.icons.TUNE, "Metrics"), metrics_panel) - - actions_panel = ActionsPanel(project) - actions_tab = ( - SectionHeader(ft.icons.CONSTRUCTION_OUTLINED, "Actions"), - actions_panel, - ) - - scenarios_panel = ScenariosPanel(project) - scenarios_tab = ( - SectionHeader(ft.icons.PUBLIC, "Scenarios"), - scenarios_panel, - ) - - graph_panel = GraphPanel(project) - pathways_panel = PathwaysPanel(project) - - def on_metrics_changed(): - metrics_panel.redraw() - scenarios_panel.redraw() - actions_panel.redraw() - pathways_panel.redraw() - graph_panel.redraw() - - def on_scenarios_changed(): - scenarios_panel.redraw() - graph_panel.redraw() - - def on_actions_changed(): - actions_panel.redraw() - pathways_panel.redraw() - graph_panel.redraw() - - def on_pathways_changed(): - pathways_panel.redraw() - graph_panel.redraw() - - def on_action_color_changed(): - pathways_panel.redraw() - graph_panel.redraw() - - # def on_graph_changed(): - # graph_panel.redraw() - - project.on_conditions_changed.append(on_metrics_changed) - project.on_criteria_changed.append(on_metrics_changed) - project.on_scenarios_changed.append(on_scenarios_changed) - project.on_actions_changed.append(on_actions_changed) - project.on_action_color_changed.append(on_action_color_changed) - project.on_pathways_changed.append(on_pathways_changed) + app = PathwaysApp(page) + page.appbar = MenuBar(app) + page.overlay.append(app.file_picker) page.add( ft.Container( expand=True, padding=theme.variables.panel_spacing, - content=ft.Row( - expand=True, - spacing=theme.variables.panel_spacing, - controls=[ - ft.Column( - expand=2, - spacing=theme.variables.panel_spacing, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - TabbedPanel( - selected_index=0, - tabs=[metrics_tab, actions_tab, scenarios_tab], - ) - ], - ), - ft.Column( - expand=3, - spacing=theme.variables.panel_spacing, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - Panel(graph_panel), - Panel( - content=ft.Column( - expand=False, - alignment=ft.MainAxisAlignment.START, - spacing=15, - controls=[ - pathways_panel, - ], - ), - padding=theme.variables.panel_padding, - ), - ], - ), - ], - ), + content=EditorPage(app.project), bgcolor=theme.colors.primary_lighter, border_radius=ft.border_radius.only(bottom_left=8, bottom_right=8), ) diff --git a/source/package/pathways_app/config.py b/source/package/pathways_app/pathways_app/config.py similarity index 100% rename from source/package/pathways_app/config.py rename to source/package/pathways_app/pathways_app/config.py diff --git a/source/package/pathways_app/pathways_app/controls/actions_editor.py b/source/package/pathways_app/pathways_app/controls/actions_editor.py index 03fcee1..9227af5 100644 --- a/source/package/pathways_app/pathways_app/controls/actions_editor.py +++ b/source/package/pathways_app/pathways_app/controls/actions_editor.py @@ -2,13 +2,6 @@ import random import flet as ft -import theme -from controls.action_icon import ActionIcon -from controls.editable_cell import EditableTextCell -from controls.metric_effect import MetricEffectCell -from controls.metric_value import MetricValueCell -from controls.styled_table import StyledTable, TableCell, TableColumn, TableRow -from pathways_app.controls.panel_header import PanelHeader from adaptation_pathways.app.model.action import Action from adaptation_pathways.app.model.pathways_project import PathwaysProject @@ -18,9 +11,8 @@ from .editable_cell import EditableTextCell from .metric_effect import MetricEffectCell from .metric_value import MetricValueCell -from .sortable_header import SortableHeader, SortMode -from .styled_button import StyledButton -from .styled_table import StyledTable +from .panel_header import PanelHeader +from .styled_table import StyledTable, TableCell, TableColumn, TableRow class ActionsEditor(ft.Column): diff --git a/source/package/pathways_app/pathways_app/controls/actions_panel.py b/source/package/pathways_app/pathways_app/controls/actions_panel.py deleted file mode 100644 index 03fcee1..0000000 --- a/source/package/pathways_app/pathways_app/controls/actions_panel.py +++ /dev/null @@ -1,205 +0,0 @@ -# pylint: disable=too-many-arguments,too-many-instance-attributes -import random - -import flet as ft -import theme -from controls.action_icon import ActionIcon -from controls.editable_cell import EditableTextCell -from controls.metric_effect import MetricEffectCell -from controls.metric_value import MetricValueCell -from controls.styled_table import StyledTable, TableCell, TableColumn, TableRow -from pathways_app.controls.panel_header import PanelHeader - -from adaptation_pathways.app.model.action import Action -from adaptation_pathways.app.model.pathways_project import PathwaysProject - -from .. import theme -from .action_icon import ActionIcon -from .editable_cell import EditableTextCell -from .metric_effect import MetricEffectCell -from .metric_value import MetricValueCell -from .sortable_header import SortableHeader, SortMode -from .styled_button import StyledButton -from .styled_table import StyledTable - - -class ActionsEditor(ft.Column): - def __init__(self, project: PathwaysProject): - super().__init__( - expand=True, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - spacing=40, - ) - - self.project = project - - self.header = PanelHeader(title="Actions", icon=theme.icons.actions) - - self.action_table = StyledTable( - columns=[], - rows=[], - row_height=42, - on_add=self.on_new_action, - on_delete=self.on_delete_actions, - ) - self.update_table() - - self.controls = [ - ft.Column( - expand=True, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - self.header, - self.action_table, - ], - ), - ] - - def redraw(self): - self.update_table() - # self.update() - - def on_name_edited(self, _): - self.project.notify_actions_changed() - self.update() - - def on_cell_edited(self, cell: MetricValueCell): - self.project.update_pathway_values(cell.metric.id) - self.project.notify_actions_changed() - self.update() - - def on_action_selected(self, action: Action): - if action.id in self.project.selected_action_ids: - self.project.selected_action_ids.remove(action.id) - else: - self.project.selected_action_ids.add(action.id) - self.redraw() - self.update() - - def on_delete_actions(self, rows: list[TableRow]): - self.project.delete_actions(row.row_id for row in rows) - self.project.notify_actions_changed() - self.update() - - def on_new_action(self): - self.project.create_action( - random.choice(theme.action_colors), - random.choice(theme.action_icons), - ) - self.project.notify_actions_changed() - self.update() - - def update_table(self): - columns = [ - TableColumn(label="Icon", width=45, expand=False, sortable=False), - TableColumn( - label="Name", - ), - *( - TableColumn(label=metric.name, key=metric.id) - for metric in self.project.all_metrics() - ), - ] - self.action_table.set_columns(columns) - - self.update_rows() - - def create_icon_editor(self, action: Action): - def on_color_picked(color: str): - action.color = color - action_icon.update_action(action) - self.project.notify_action_color_changed() - - def on_icon_picked(icon: str): - action.icon = icon - action_icon.update_action(action) - self.project.notify_action_color_changed() - - def on_editor_closed(_): - self.project.notify_actions_changed() - - def update_items(): - action_button.items = [ - ft.PopupMenuItem( - content=ft.GridView( - spacing=4, - width=200, - runs_count=4, - padding=ft.padding.symmetric(4, 6), - child_aspect_ratio=1.0, - controls=[ - ft.Container( - bgcolor=color, - border=ft.border.all( - 2, - ( - theme.colors.primary_medium - if action.color == color - else theme.colors.primary_lightest - ), - ), - on_click=lambda e, c=color: on_color_picked(c), - width=10, - height=10, - ) - for color in theme.action_colors - ], - ) - ), - ft.PopupMenuItem( - content=ft.GridView( - expand=True, - runs_count=5, - padding=ft.padding.symmetric(12, 6), - controls=[ - ft.Container( - ft.Icon(icon), - on_click=lambda e, i=icon: on_icon_picked(i), - ) - for icon in theme.action_icons - ], - spacing=4, - ) - ), - ] - - action_icon = ActionIcon(action) - - action_button = ft.PopupMenuButton( - action_icon, - items=[], - bgcolor=theme.colors.off_white, - menu_position=ft.PopupMenuPosition.UNDER, - on_cancel=on_editor_closed, - ) - update_items() - return action_button - - def update_rows(self): - rows = [] - - for action in self.project.all_actions: - if action.id == self.project.root_pathway.action_id: - continue - - metric_cells = [] - for metric in self.project.all_metrics(): - effect = action.metric_data[metric.id] - metric_cells.append( - MetricEffectCell( - metric, effect, on_finished_editing=self.on_cell_edited - ) - ) - - rows.append( - TableRow( - row_id=action.id, - cells=[ - TableCell(self.create_icon_editor(action)), - EditableTextCell(action, "name", self.on_name_edited), - *metric_cells, - ], - ) - ) - - self.action_table.set_rows(rows) diff --git a/source/package/pathways_app/pathways_app/controls/editable_cell.py b/source/package/pathways_app/pathways_app/controls/editable_cell.py index 0cb929c..e85f270 100644 --- a/source/package/pathways_app/pathways_app/controls/editable_cell.py +++ b/source/package/pathways_app/pathways_app/controls/editable_cell.py @@ -2,10 +2,10 @@ from typing import Callable import flet as ft -import theme from pathways_app.controls.input_filters import IntInputFilter from pyparsing import abstractmethod +from .. import theme from .styled_table import TableCell diff --git a/source/package/pathways_app/pathways_app/controls/editor_page.py b/source/package/pathways_app/pathways_app/controls/editor_page.py index 20edadb..e176201 100644 --- a/source/package/pathways_app/pathways_app/controls/editor_page.py +++ b/source/package/pathways_app/pathways_app/controls/editor_page.py @@ -1,16 +1,17 @@ import flet as ft -import theme -from controls.actions_editor import ActionsEditor -from controls.graph_editor import GraphEditor -from controls.header import SectionHeader -from controls.metrics_editor import MetricsEditor -from controls.panel import Panel -from controls.pathways_editor import PathwaysPanel -from controls.scenarios_editor import ScenariosEditor -from controls.tabbed_panel import TabbedPanel from adaptation_pathways.app.model.pathways_project import PathwaysProject +from .. import theme +from .actions_editor import ActionsEditor +from .graph_editor import GraphEditor +from .header import SectionHeader +from .metrics_editor import MetricsEditor +from .panel import Panel +from .pathways_editor import PathwaysPanel +from .scenarios_editor import ScenariosEditor +from .tabbed_panel import TabbedPanel + class EditorPage(ft.Row): def __init__(self, project: PathwaysProject): diff --git a/source/package/pathways_app/pathways_app/controls/graph_editor.py b/source/package/pathways_app/pathways_app/controls/graph_editor.py index 0e4975a..00d0e12 100644 --- a/source/package/pathways_app/pathways_app/controls/graph_editor.py +++ b/source/package/pathways_app/pathways_app/controls/graph_editor.py @@ -2,15 +2,16 @@ import flet as ft import matplotlib.pyplot -import theme -from controls.styled_button import StyledButton -from controls.styled_dropdown import StyledDropdown from flet.matplotlib_chart import MatplotlibChart -from pathways_app.controls.header import SmallHeader from adaptation_pathways.app.model.pathways_project import PathwaysProject from adaptation_pathways.app.service.plotting_service import PlottingService +from .. import theme +from .header import SmallHeader +from .styled_button import StyledButton +from .styled_dropdown import StyledDropdown + class GraphHeader(ft.Row): def __init__( diff --git a/source/package/pathways_app/pathways_app/controls/menu_bar.py b/source/package/pathways_app/pathways_app/controls/menu_bar.py index 74948ed..976fe7b 100644 --- a/source/package/pathways_app/pathways_app/controls/menu_bar.py +++ b/source/package/pathways_app/pathways_app/controls/menu_bar.py @@ -1,11 +1,12 @@ import flet as ft -import theme -from pathways_app.app import App -from pathways_app.config import Config + +from .. import theme +from ..config import Config +from ..pathways_app import PathwaysApp class MenuBar(ft.Container): - def __init__(self, app: App): + def __init__(self, app: PathwaysApp): self.app = app super().__init__( diff --git a/source/package/pathways_app/pathways_app/controls/metric_value.py b/source/package/pathways_app/pathways_app/controls/metric_value.py index 8f8bd77..3f546bd 100644 --- a/source/package/pathways_app/pathways_app/controls/metric_value.py +++ b/source/package/pathways_app/pathways_app/controls/metric_value.py @@ -1,10 +1,11 @@ import flet as ft -import theme -from controls.editable_cell import EditableCell -from pathways_app.controls.input_filters import FloatInputFilter from adaptation_pathways.app.model.metric import Metric, MetricValue, MetricValueState +from .. import theme +from .editable_cell import EditableCell +from .input_filters import FloatInputFilter + class MetricValueCell(EditableCell): def __init__(self, metric: Metric, value: MetricValue, on_finished_editing=None): diff --git a/source/package/pathways_app/pathways_app/controls/metrics_editor.py b/source/package/pathways_app/pathways_app/controls/metrics_editor.py index c114e10..2deacee 100644 --- a/source/package/pathways_app/pathways_app/controls/metrics_editor.py +++ b/source/package/pathways_app/pathways_app/controls/metrics_editor.py @@ -1,20 +1,15 @@ # from typing import Callable import flet as ft -from controls.editable_cell import EditableTextCell -from controls.header import SmallHeader -from controls.styled_table import StyledTable, TableColumn, TableRow -from controls.unit_cell import MetricUnitCell -from pathways_app import theme -from pathways_app.controls.panel_header import PanelHeader from adaptation_pathways.app.model.metric import Metric from adaptation_pathways.app.model.pathways_project import PathwaysProject +from .. import theme from .editable_cell import EditableTextCell from .header import SmallHeader -from .styled_button import StyledButton -from .styled_table import StyledTable +from .panel_header import PanelHeader +from .styled_table import StyledTable, TableColumn, TableRow from .unit_cell import MetricUnitCell @@ -107,24 +102,24 @@ def on_criteria_selected(self, metric: Metric): self.redraw() - def on_new_condition(self, _): + def on_new_condition(self): metric = self.project.create_condition() self.project.update_pathway_values(metric.id) self.project.notify_conditions_changed() - def on_delete_conditions(self, _): - for metric_id in self.project.selected_condition_ids: - self.project.delete_condition(metric_id) + def on_delete_conditions(self, rows: list[TableRow]): + for row in rows: + self.project.delete_condition(row.row_id) self.project.notify_conditions_changed() - def on_new_criteria(self, _): + def on_new_criteria(self): metric = self.project.create_criteria() self.project.update_pathway_values(metric.id) self.project.notify_criteria_changed() - def on_delete_criteria(self, _): - for metric_id in self.project.selected_criteria_ids: - self.project.delete_criteria(metric_id) + def on_delete_criteria(self, rows: list[TableRow]): + for row in rows: + self.project.delete_criteria(row.row_id) self.project.notify_criteria_changed() def get_metric_row( diff --git a/source/package/pathways_app/pathways_app/controls/metrics_panel.py b/source/package/pathways_app/pathways_app/controls/metrics_panel.py deleted file mode 100644 index c114e10..0000000 --- a/source/package/pathways_app/pathways_app/controls/metrics_panel.py +++ /dev/null @@ -1,159 +0,0 @@ -# from typing import Callable - -import flet as ft -from controls.editable_cell import EditableTextCell -from controls.header import SmallHeader -from controls.styled_table import StyledTable, TableColumn, TableRow -from controls.unit_cell import MetricUnitCell -from pathways_app import theme -from pathways_app.controls.panel_header import PanelHeader - -from adaptation_pathways.app.model.metric import Metric -from adaptation_pathways.app.model.pathways_project import PathwaysProject - -from .editable_cell import EditableTextCell -from .header import SmallHeader -from .styled_button import StyledButton -from .styled_table import StyledTable -from .unit_cell import MetricUnitCell - - -class MetricsEditor(ft.Column): - def __init__(self, project: PathwaysProject): - super().__init__() - - self.project = project - self.header = PanelHeader("Metrics", theme.icons.metrics) - self.expand = True - self.horizontal_alignment = ft.CrossAxisAlignment.STRETCH - self.spacing = 40 - - self.conditions_table = StyledTable( - columns=[ - TableColumn(label="Name"), - TableColumn(label="Unit"), - ], - rows=[], - show_checkboxes=True, - on_add=self.on_new_condition, - on_delete=self.on_delete_conditions, - pre_operation_content=ft.Row( - [ - SmallHeader("Conditions"), - ft.Container(expand=True), - ], - expand=True, - ), - ) - - self.criteria_table = StyledTable( - columns=[ - TableColumn(label="Name"), - TableColumn(label="Unit"), - ], - rows=[], - show_checkboxes=True, - on_add=self.on_new_criteria, - on_delete=self.on_delete_criteria, - pre_operation_content=ft.Row( - [ - SmallHeader("Criteria"), - ft.Container(expand=True), - ], - expand=True, - ), - ) - - self.update_metrics() - - self.controls = [ - ft.Column( - expand=True, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - self.header, - self.conditions_table, - ], - ), - ft.Column( - expand=True, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - self.criteria_table, - ], - ), - ] - - def redraw(self): - self.update_metrics() - self.update() - - def on_metric_updated(self, _): - self.project.notify_conditions_changed() - - def on_condition_selected(self, metric: Metric): - if metric.id in self.project.selected_condition_ids: - self.project.selected_condition_ids.remove(metric.id) - else: - self.project.selected_condition_ids.add(metric.id) - - self.redraw() - - def on_criteria_selected(self, metric: Metric): - if metric.id in self.project.selected_criteria_ids: - self.project.selected_criteria_ids.remove(metric.id) - else: - self.project.selected_criteria_ids.add(metric.id) - - self.redraw() - - def on_new_condition(self, _): - metric = self.project.create_condition() - self.project.update_pathway_values(metric.id) - self.project.notify_conditions_changed() - - def on_delete_conditions(self, _): - for metric_id in self.project.selected_condition_ids: - self.project.delete_condition(metric_id) - self.project.notify_conditions_changed() - - def on_new_criteria(self, _): - metric = self.project.create_criteria() - self.project.update_pathway_values(metric.id) - self.project.notify_criteria_changed() - - def on_delete_criteria(self, _): - for metric_id in self.project.selected_criteria_ids: - self.project.delete_criteria(metric_id) - self.project.notify_criteria_changed() - - def get_metric_row( - self, - metric: Metric, - # selected_ids: set[str], - # on_metric_selected: Callable[[Metric], None], - ) -> TableRow: - row = TableRow( - row_id=metric.id, - cells=[ - EditableTextCell(metric, "name", self.on_metric_updated), - MetricUnitCell(metric, self.on_metric_updated), - ], - ) - return row - - def update_metrics(self): - self.conditions_table.set_rows( - self.get_metric_row(metric) for metric in self.project.all_conditions - ) - - self.criteria_table.set_rows( - [ - self.get_metric_row( - metric, - # self.project.selected_criteria_ids, - # self.on_criteria_selected, - ) - for metric in self.project.all_criteria - ] - ) diff --git a/source/package/pathways_app/pathways_app/controls/pathways_editor.py b/source/package/pathways_app/pathways_app/controls/pathways_editor.py index 2b8b8ce..04393c6 100644 --- a/source/package/pathways_app/pathways_app/controls/pathways_editor.py +++ b/source/package/pathways_app/pathways_app/controls/pathways_editor.py @@ -1,20 +1,13 @@ import flet as ft -import theme -from controls.action_icon import ActionIcon -from controls.metric_value import MetricValueCell -from controls.styled_table import StyledTable, TableCell, TableColumn, TableRow -from pathways_app.controls.panel_header import PanelHeader from adaptation_pathways.app.model.pathway import Pathway from adaptation_pathways.app.model.pathways_project import PathwaysProject from .. import theme from .action_icon import ActionIcon -from .header import SectionHeader from .metric_value import MetricValueCell -from .sortable_header import SortableHeader, SortMode -from .styled_button import StyledButton -from .styled_table import StyledTable +from .panel_header import PanelHeader +from .styled_table import StyledTable, TableCell, TableColumn, TableRow class PathwaysPanel(ft.Container): diff --git a/source/package/pathways_app/pathways_app/controls/pathways_panel.py b/source/package/pathways_app/pathways_app/controls/pathways_panel.py deleted file mode 100644 index 2b8b8ce..0000000 --- a/source/package/pathways_app/pathways_app/controls/pathways_panel.py +++ /dev/null @@ -1,174 +0,0 @@ -import flet as ft -import theme -from controls.action_icon import ActionIcon -from controls.metric_value import MetricValueCell -from controls.styled_table import StyledTable, TableCell, TableColumn, TableRow -from pathways_app.controls.panel_header import PanelHeader - -from adaptation_pathways.app.model.pathway import Pathway -from adaptation_pathways.app.model.pathways_project import PathwaysProject - -from .. import theme -from .action_icon import ActionIcon -from .header import SectionHeader -from .metric_value import MetricValueCell -from .sortable_header import SortableHeader, SortMode -from .styled_button import StyledButton -from .styled_table import StyledTable - - -class PathwaysPanel(ft.Container): - def __init__(self, project: PathwaysProject): - self.project = project - - self.header = PanelHeader("Pathways", ft.Icons.ACCOUNT_TREE_OUTLINED) - - self.rows_by_pathway: dict[Pathway, ft.DataRow] = {} - - self.pathway_table = StyledTable( - columns=[], rows=[], show_checkboxes=True, on_delete=self.on_delete_pathways - ) - self.update_table() - - super().__init__( - expand=True, - content=ft.Column( - expand=True, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - self.header, - self.pathway_table, - ], - ), - ) - - def redraw(self): - self.update_table() - self.update() - - def on_delete_pathways(self, rows: list[TableRow]): - self.project.delete_pathways(row.row_id for row in rows) - self.project.notify_pathways_changed() - - def update_table(self): - self.pathway_table.set_columns( - [ - TableColumn( - label="Pathway", - expand=2, - ), - *( - TableColumn( - label=metric.name, - key=metric.id, - alignment=ft.alignment.center_right, - ) - for metric in self.project.all_metrics() - ), - ] - ) - - rows = [] - - self.rows_by_pathway = {} - for pathway in self.project.all_pathways: - ancestors = self.project.get_ancestors_and_self(pathway) - path = [*reversed([*ancestors])] - row = self.get_pathway_row(pathway, path) - self.rows_by_pathway[pathway] = row - rows.append(row) - - self.pathway_table.set_rows(rows) - return rows - - def on_metric_value_edited(self, cell: MetricValueCell): - self.project.update_pathway_values(cell.metric.id) - self.project.notify_pathways_changed() - - def get_pathway_row(self, pathway: Pathway, ancestors: list[Pathway]): - children = [*self.project.get_children(pathway.id)] - pathway_action = self.project.get_action(pathway.action_id) - - unused_action_ids = [ - action.id - for action in self.project.all_actions - if not any(ancestor.action_id == action.id for ancestor in ancestors) - and not any(child.action_id == action.id for child in children) - ] - - row_controls = [ - ActionIcon(self.project.get_action(ancestor.action_id), size=26) - for ancestor in ancestors - ] - - if pathway.parent_id is None: - row_controls.append(ft.Text(" Current ", color=pathway_action.color)) - - if len(unused_action_ids) > 0: - row_controls.append( - ft.PopupMenuButton( - ft.Icon( - ft.Icons.ADD_CIRCLE_OUTLINE, - size=20, - color=theme.colors.primary_lightest, - ), - items=[ - ft.PopupMenuItem( - content=ft.Row( - [ - ActionIcon( - action, - display_tooltip=False, - ), - ft.Text( - action.name, - style=theme.text.normal, - ), - ] - ), - on_click=lambda e, action_id=action_id: self.extend_pathway( - pathway, action_id - ), - ) - for action_id in unused_action_ids - for action in [self.project.get_action(action_id)] - ], - tooltip=ft.Tooltip( - "Add", - bgcolor=theme.colors.primary_white, - ), - bgcolor=theme.colors.off_white, - menu_position=ft.PopupMenuPosition.UNDER, - ), - ) - - row = TableRow( - row_id=pathway.id, - cells=[ - TableCell( - ft.Container( - expand=True, - content=ft.Row( - spacing=0, - controls=row_controls, - ), - ), - sort_value=pathway.id, - ), - *( - MetricValueCell( - metric, - pathway.metric_data[metric.id], - on_finished_editing=self.on_metric_value_edited, - ) - for metric in self.project.all_metrics() - ), - ], - can_be_deleted=pathway.id != self.project.root_pathway_id, - ) - - return row - - def extend_pathway(self, pathway: Pathway, action_id: str): - self.project.create_pathway(action_id, pathway.id) - self.project.notify_pathways_changed() diff --git a/source/package/pathways_app/pathways_app/controls/scenarios_editor.py b/source/package/pathways_app/pathways_app/controls/scenarios_editor.py index 28fe15f..2fb41b9 100644 --- a/source/package/pathways_app/pathways_app/controls/scenarios_editor.py +++ b/source/package/pathways_app/pathways_app/controls/scenarios_editor.py @@ -2,19 +2,20 @@ from functools import partial import flet as ft -from controls.styled_dropdown import StyledDropdown -from controls.styled_table import StyledTable, TableColumn, TableRow -from pathways_app import theme -from pathways_app.controls.editable_cell import EditableIntCell, EditableTextCell -from pathways_app.controls.header import SmallHeader -from pathways_app.controls.metric_value import MetricValueCell -from pathways_app.controls.panel_header import PanelHeader -from pathways_app.utils import find_index from adaptation_pathways.app.model.metric import Metric from adaptation_pathways.app.model.pathways_project import PathwaysProject from adaptation_pathways.app.model.scenario import YearDataPoint +from .. import theme +from ..utils import find_index +from .editable_cell import EditableIntCell, EditableTextCell +from .header import SmallHeader +from .metric_value import MetricValueCell +from .panel_header import PanelHeader +from .styled_dropdown import StyledDropdown +from .styled_table import StyledTable, TableColumn, TableRow + class ScenariosEditor(ft.Column): def __init__(self, project: PathwaysProject): diff --git a/source/package/pathways_app/pathways_app/controls/styled_dropdown.py b/source/package/pathways_app/pathways_app/controls/styled_dropdown.py index eb99f76..8fafbfb 100644 --- a/source/package/pathways_app/pathways_app/controls/styled_dropdown.py +++ b/source/package/pathways_app/pathways_app/controls/styled_dropdown.py @@ -1,7 +1,8 @@ # pylint: disable=too-many-arguments import flet as ft -import theme -from utils import find_index + +from .. import theme +from ..utils import find_index class StyledDropdown(ft.Dropdown): diff --git a/source/package/pathways_app/pathways_app/controls/styled_table.py b/source/package/pathways_app/pathways_app/controls/styled_table.py index bd5c54c..bf35976 100644 --- a/source/package/pathways_app/pathways_app/controls/styled_table.py +++ b/source/package/pathways_app/pathways_app/controls/styled_table.py @@ -2,10 +2,10 @@ from typing import Callable import flet as ft -import theme -from pathways_app.controls.styled_button import StyledButton +from .. import theme from .sortable_header import SortableHeader, SortMode +from .styled_button import StyledButton class TableColumn: @@ -132,11 +132,11 @@ def __init__( sort_column_index: int | None = None, sort_ascending: bool = True, on_sorted: Callable[[], None] | None = None, - on_add: Callable[[], TableRow] | None = None, + on_add: Callable[[], None] | None = None, add_label="Add", - on_delete: Callable[[], list[TableRow]] | None = None, + on_delete: Callable[[list[TableRow]], None] | None = None, delete_label="Delete", - on_copy: Callable[[list[TableRow]], list[TableRow]] | None = None, + on_copy: Callable[[list[TableRow]], None] | None = None, copy_label="Duplicate", pre_operation_content: ft.Control | None = None, show_checkboxes=False, diff --git a/source/package/pathways_app/pathways_app/controls/unit_cell.py b/source/package/pathways_app/pathways_app/controls/unit_cell.py index fa5ab61..d46a399 100644 --- a/source/package/pathways_app/pathways_app/controls/unit_cell.py +++ b/source/package/pathways_app/pathways_app/controls/unit_cell.py @@ -1,10 +1,10 @@ from typing import Callable import flet as ft -import theme from adaptation_pathways.app.model.metric import Metric, MetricUnit, default_units +from .. import theme from .styled_table import TableCell diff --git a/source/package/pathways_app/app.py b/source/package/pathways_app/pathways_app/pathways_app.py similarity index 88% rename from source/package/pathways_app/app.py rename to source/package/pathways_app/pathways_app/pathways_app.py index eba4081..3d46994 100644 --- a/source/package/pathways_app/app.py +++ b/source/package/pathways_app/pathways_app/pathways_app.py @@ -1,9 +1,10 @@ import flet as ft -from pathways_app import example -from pathways_app.config import Config +from . import example +from .config import Config -class App: + +class PathwaysApp: def __init__(self, page: ft.Page): self.page = page self.project = example.project From 5f16f1639711817916887a3ba90c65257f0367d4 Mon Sep 17 00:00:00 2001 From: AJ Kolenc Date: Fri, 31 Jan 2025 10:46:36 +0100 Subject: [PATCH 07/12] Refactored to allow project changes --- environment/configuration/requirements.txt | 1 + pyproject.toml | 2 +- .../adaptation_pathways/app/model/metric.py | 7 +- .../app/model/pathways_project.py | 194 ++++++-------- .../adaptation_pathways/app/model/scenario.py | 8 +- .../{pathways_app => }/__init__.py | 0 .../pathways_app/assets/js/pathways.js | 39 +++ source/package/pathways_app/main.py | 9 +- .../pathways_app/pathways_app/cli/app.py | 44 ---- .../pathways_app/controls/__init__.py | 3 - .../pathways_app/controls/editor_page.py | 167 ------------ .../pathways_app/controls/menu_bar.py | 119 --------- .../pathways_app/controls/metrics_editor.py | 154 ----------- .../pathways_app/controls/unit_cell.py | 195 -------------- .../pathways_app/pathways_app/example.py | 106 -------- .../pathways_app/pathways_app/pathways_app.py | 27 -- .../{pathways_app => src}/cli/__init__.py | 0 source/package/pathways_app/src/cli/app.py | 80 ++++++ .../{pathways_app => src}/config.py | 0 .../controls/action_icon.py | 3 +- .../controls/editable_cell.py | 4 +- .../controls/editors}/actions_editor.py | 63 ++--- .../src/controls/editors/conditions_editor.py | 71 +++++ .../src/controls/editors/criteria_editor.py | 78 ++++++ .../controls/editors}/graph_editor.py | 128 +++++---- .../src/controls/editors/metrics_editor.py | 65 +++++ .../controls/editors}/pathways_editor.py | 52 ++-- .../controls/editors/project_info_editor.py | 66 +++++ .../controls/editors}/scenarios_editor.py | 87 +++---- .../{pathways_app => src}/controls/header.py | 3 +- .../controls/input_filters.py | 0 .../pathways_app/src/controls/menu_bar.py | 142 ++++++++++ .../controls/metric_effect.py | 6 +- .../controls/metric_value.py | 2 +- .../src/controls/pages/editor_page.py | 173 +++++++++++++ .../src/controls/pages/startup_page.py | 95 +++++++ .../src/controls/pages/wizard_page.py | 245 ++++++++++++++++++ .../{pathways_app => src}/controls/panel.py | 16 +- .../controls/panel_header.py | 5 +- .../controls/sortable_header.py | 3 +- .../controls/styled_button.py | 14 +- .../controls/styled_dropdown.py | 49 ++-- .../controls/styled_table.py | 21 +- .../controls/tabbed_panel.py | 15 +- .../pathways_app/src/controls/unit_cell.py | 200 ++++++++++++++ source/package/pathways_app/src/data.py | 107 ++++++++ .../package/pathways_app/src/pathways_app.py | 155 +++++++++++ .../{pathways_app => src}/theme.py | 18 ++ .../{pathways_app => src}/utils.py | 0 49 files changed, 1886 insertions(+), 1155 deletions(-) rename source/package/pathways_app/{pathways_app => }/__init__.py (100%) create mode 100644 source/package/pathways_app/assets/js/pathways.js delete mode 100644 source/package/pathways_app/pathways_app/cli/app.py delete mode 100644 source/package/pathways_app/pathways_app/controls/__init__.py delete mode 100644 source/package/pathways_app/pathways_app/controls/editor_page.py delete mode 100644 source/package/pathways_app/pathways_app/controls/menu_bar.py delete mode 100644 source/package/pathways_app/pathways_app/controls/metrics_editor.py delete mode 100644 source/package/pathways_app/pathways_app/controls/unit_cell.py delete mode 100644 source/package/pathways_app/pathways_app/example.py delete mode 100644 source/package/pathways_app/pathways_app/pathways_app.py rename source/package/pathways_app/{pathways_app => src}/cli/__init__.py (100%) create mode 100644 source/package/pathways_app/src/cli/app.py rename source/package/pathways_app/{pathways_app => src}/config.py (100%) rename source/package/pathways_app/{pathways_app => src}/controls/action_icon.py (98%) rename source/package/pathways_app/{pathways_app => src}/controls/editable_cell.py (98%) rename source/package/pathways_app/{pathways_app/controls => src/controls/editors}/actions_editor.py (74%) create mode 100644 source/package/pathways_app/src/controls/editors/conditions_editor.py create mode 100644 source/package/pathways_app/src/controls/editors/criteria_editor.py rename source/package/pathways_app/{pathways_app/controls => src/controls/editors}/graph_editor.py (68%) create mode 100644 source/package/pathways_app/src/controls/editors/metrics_editor.py rename source/package/pathways_app/{pathways_app/controls => src/controls/editors}/pathways_editor.py (74%) create mode 100644 source/package/pathways_app/src/controls/editors/project_info_editor.py rename source/package/pathways_app/{pathways_app/controls => src/controls/editors}/scenarios_editor.py (66%) rename source/package/pathways_app/{pathways_app => src}/controls/header.py (98%) rename source/package/pathways_app/{pathways_app => src}/controls/input_filters.py (100%) create mode 100644 source/package/pathways_app/src/controls/menu_bar.py rename source/package/pathways_app/{pathways_app => src}/controls/metric_effect.py (97%) rename source/package/pathways_app/{pathways_app => src}/controls/metric_value.py (99%) create mode 100644 source/package/pathways_app/src/controls/pages/editor_page.py create mode 100644 source/package/pathways_app/src/controls/pages/startup_page.py create mode 100644 source/package/pathways_app/src/controls/pages/wizard_page.py rename source/package/pathways_app/{pathways_app => src}/controls/panel.py (77%) rename source/package/pathways_app/{pathways_app => src}/controls/panel_header.py (93%) rename source/package/pathways_app/{pathways_app => src}/controls/sortable_header.py (99%) rename source/package/pathways_app/{pathways_app => src}/controls/styled_button.py (71%) rename source/package/pathways_app/{pathways_app => src}/controls/styled_dropdown.py (59%) rename source/package/pathways_app/{pathways_app => src}/controls/styled_table.py (96%) rename source/package/pathways_app/{pathways_app => src}/controls/tabbed_panel.py (86%) create mode 100644 source/package/pathways_app/src/controls/unit_cell.py create mode 100644 source/package/pathways_app/src/data.py create mode 100644 source/package/pathways_app/src/pathways_app.py rename source/package/pathways_app/{pathways_app => src}/theme.py (94%) rename source/package/pathways_app/{pathways_app => src}/utils.py (100%) diff --git a/environment/configuration/requirements.txt b/environment/configuration/requirements.txt index c20283a..7cda1bf 100644 --- a/environment/configuration/requirements.txt +++ b/environment/configuration/requirements.txt @@ -3,3 +3,4 @@ matplotlib networkx[default] pyside6>=6.6 flet==0.25.* +pyodide-py diff --git a/pyproject.toml b/pyproject.toml index baf4334..365ca98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ profile = "black" [tool.pylint] max-line-length=240 -disable = "C0103, C0114, C0115, C0116, C0302, E0401, W0212, W0511, R0801, R0902, R0903, R0913, R0914, R0904, R0917" +disable = "C0103, C0114, C0115, C0116, C0302, E0401, W0212, W0511, R0801, R0902, R0903, R0913, R0914, R0904, R0917, W0718" extension-pkg-allow-list = [ "matplotlib", ] diff --git a/source/package/adaptation_pathways/app/model/metric.py b/source/package/adaptation_pathways/app/model/metric.py index 1b939e2..e2409b4 100644 --- a/source/package/adaptation_pathways/app/model/metric.py +++ b/source/package/adaptation_pathways/app/model/metric.py @@ -147,14 +147,15 @@ class Volume: si = [ MetricUnit(name="Milliliter", symbol="ml"), MetricUnit(name="Liter", symbol="l"), - MetricUnit(name="Cubic Centimeter", symbol="cm^3"), - MetricUnit(name="Cubic Meter", symbol="m^3"), + MetricUnit(name="Cubic Centimeter", symbol="cm³"), + MetricUnit(name="Cubic Meter", symbol="m³"), ] imperial = [ MetricUnit(name="Fluid Ounce", symbol="fl oz"), MetricUnit(name="Pint", symbol="pt"), MetricUnit(name="Quart", symbol="qt"), MetricUnit(name="Gallon", symbol="gal"), + MetricUnit(name="Acre Feet", symbol="ac-ft"), ] volume = Volume() @@ -233,7 +234,7 @@ class MassWeight: ] relative = [ - MetricUnit(name="Percent", symbol="%", value_format=".2%"), + MetricUnit(name="Percent", symbol="%", value_format=".2"), MetricUnit( name="Impact", symbol="", short_name="Impact", value_format=FORMAT_SLIDER ), diff --git a/source/package/adaptation_pathways/app/model/pathways_project.py b/source/package/adaptation_pathways/app/model/pathways_project.py index 1dff851..6944506 100644 --- a/source/package/adaptation_pathways/app/model/pathways_project.py +++ b/source/package/adaptation_pathways/app/model/pathways_project.py @@ -2,9 +2,8 @@ """ The single class that stores all data needed to work on a project """ -from typing import Callable, Iterable - -from adaptation_pathways.app.model.sorting import SortingInfo +from json import JSONEncoder +from typing import Iterable from .action import Action from .metric import Metric, MetricEffect, MetricOperation, MetricValue, MetricValueState @@ -20,13 +19,22 @@ def __init__( organization: str, start_year: int, end_year: int, - conditions: list[Metric], - criteria: list[Metric], - scenarios: list[Scenario], - actions: list[Action], - pathways: list[Pathway], - root_action: Action, - root_pathway_id: str, + conditions_by_id: dict[str, Metric] | None = None, + condition_ids: list[str] | None = None, + criteria_by_id: dict[str, Metric] | None = None, + criteria_ids: list[str] | None = None, + actions_by_id: dict[str, Action] | None = None, + action_ids: list[str] | None = None, + scenarios_by_id: dict[str, Scenario] | None = None, + scenario_ids: list[str] | None = None, + pathways_by_id: dict[str, Pathway] | None = None, + pathway_ids: list[str] | None = None, + root_action_id: str = "", + root_pathway_id: str = "", + values_scenario_id: str | None = None, + graph_metric_id: str | None = None, + graph_scenario_id: str | None = None, + graph_is_time=False, ): self.id = project_id self.name = name @@ -35,108 +43,50 @@ def __init__( self.end_year = end_year self._current_id = 0 - self.conditions_by_id: dict[str, Metric] = {} - for metric in conditions: - self.conditions_by_id[metric.id] = metric - - self.criteria_by_id: dict[str, Metric] = {} - for metric in criteria: - self.criteria_by_id[metric.id] = metric - - self.scenarios_by_id: dict[str, Scenario] = {} - for scenario in scenarios: - self.scenarios_by_id[scenario.id] = scenario - - self.actions_by_id: dict[str, Action] = {} - self.actions_by_id[root_action.id] = root_action - for action in actions: - self.actions_by_id[action.id] = action - - self.pathways_by_id: dict[str, Pathway] = {} - for pathway in pathways: - self.pathways_by_id[pathway.id] = pathway - - self.condition_sorting = SortingInfo() - self.criteria_sorting = SortingInfo() - self.scenario_sorting = SortingInfo() - self.action_sorting = SortingInfo() - self.pathway_sorting = SortingInfo() - - self.root_pathway_id = root_pathway_id - self.selected_condition_ids: set[str] = set() - self.selected_criteria_ids: set[str] = set() - self.selected_action_ids: set[str] = set() - self.selected_pathway_ids: set[str] = set() - self.selected_scenario_ids: set[str] = set() - - self.values_scenario_id: str | None = ( - None if len(scenarios) == 0 else scenarios[0].id - ) - - self.graph_metric_id: str | None = ( - conditions[0].id if len(conditions) > 0 else None - ) - self.graph_is_time: bool = self.graph_metric_id is not None - self.graph_scenario_id: str | None = ( - scenarios[0].id if len(scenarios) > 0 else None - ) - - self.on_conditions_changed: list[Callable[[], None]] = [] - self.on_criteria_changed: list[Callable[[], None]] = [] - self.on_scenarios_changed: list[Callable[[], None]] = [] - self.on_actions_changed: list[Callable[[], None]] = [] - self.on_action_color_changed: list[Callable[[], None]] = [] - self.on_pathways_changed: list[Callable[[], None]] = [] - - for metric in self.all_metrics(): - self.update_pathway_values(metric.id) - - def __hash__(self): - return self.id.__hash__() + self.condition_ids = condition_ids or [] + self.conditions_by_id = conditions_by_id or {} + self.criteria_ids = criteria_ids or [] + self.criteria_by_id = criteria_by_id or {} - def notify_conditions_changed(self): - for listener in self.on_conditions_changed: - listener() + self.scenario_ids = scenario_ids or [] + self.scenarios_by_id = scenarios_by_id or {} - def notify_criteria_changed(self): - for listener in self.on_criteria_changed: - listener() + self.action_ids = action_ids or [] + self.actions_by_id = actions_by_id or {} - def notify_scenarios_changed(self): - for listener in self.on_scenarios_changed: - listener() + self.pathway_ids = pathway_ids or [] + self.pathways_by_id = pathways_by_id or {} - def notify_actions_changed(self): - for listener in self.on_actions_changed: - listener() + self.root_pathway_id = root_pathway_id or "" + self.root_action_id = root_action_id or "" - def notify_action_color_changed(self): - for listener in self.on_action_color_changed: - listener() + self.values_scenario_id = values_scenario_id or "none" + self.graph_metric_id = graph_metric_id or "none" + self.graph_scenario_id = graph_scenario_id or "none" + self.graph_is_time = graph_is_time - def notify_pathways_changed(self): - for listener in self.on_pathways_changed: - listener() + def __hash__(self): + return self.id.__hash__() @property - def all_actions(self): - return self.actions_by_id.values() + def all_conditions(self) -> Iterable[Metric]: + return (self.conditions_by_id[metric_id] for metric_id in self.condition_ids) @property - def all_conditions(self): - return self.conditions_by_id.values() + def all_criteria(self) -> Iterable[Metric]: + return (self.criteria_by_id[metric_id] for metric_id in self.criteria_ids) @property - def all_criteria(self): - return self.criteria_by_id.values() + def all_scenarios(self) -> Iterable[Scenario]: + return (self.scenarios_by_id[scenario_id] for scenario_id in self.scenario_ids) @property - def all_scenarios(self): - return self.scenarios_by_id.values() + def all_actions(self) -> Iterable[Action]: + return (self.actions_by_id[action_id] for action_id in self.action_ids) @property - def all_pathways(self): - return self.pathways_by_id.values() + def all_pathways(self) -> Iterable[Pathway]: + return (self.pathways_by_id[pathway_id] for pathway_id in self.pathway_ids) @property def root_pathway(self): @@ -168,10 +118,13 @@ def all_metrics(self): yield from self.all_conditions yield from self.all_criteria - def _create_metric(self, name: str, metrics_by_id: dict[str, Metric]) -> Metric: + def _create_metric( + self, name: str, metrics_by_id: dict[str, Metric], metric_ids: list[str] + ) -> Metric: metric_id = self._create_id() metric = Metric(metric_id, name, "") metrics_by_id[metric_id] = metric + metric_ids.append(metric_id) for action in self.all_actions: action.metric_data[metric_id] = MetricEffect(0, MetricOperation.ADD) @@ -180,21 +133,28 @@ def _create_metric(self, name: str, metrics_by_id: dict[str, Metric]) -> Metric: return metric def create_condition(self) -> Metric: - metric = self._create_metric("New Condition", self.conditions_by_id) + metric = self._create_metric( + "New Condition", self.conditions_by_id, self.condition_ids + ) + if self.graph_metric_id == "none": + self.graph_metric_id = metric.id + return metric def create_criteria(self) -> Metric: - metric = self._create_metric("New Criteria", self.criteria_by_id) + metric = self._create_metric( + "New Criteria", self.criteria_by_id, self.criteria_ids + ) return metric def delete_condition(self, metric_id: str) -> Metric | None: metric = self.conditions_by_id.pop(metric_id) - self.selected_condition_ids.remove(metric_id) + self.condition_ids.remove(metric_id) return metric def delete_criteria(self, metric_id: str) -> Metric | None: metric = self.criteria_by_id.pop(metric_id) - self.selected_criteria_ids.remove(metric_id) + self.criteria_ids.remove(metric_id) return metric def get_scenario(self, scenario_id: str) -> Scenario | None: @@ -204,8 +164,11 @@ def create_scenario(self, name: str) -> Scenario: scenario_id = self._create_id() scenario = Scenario(scenario_id, name) self.scenarios_by_id[scenario.id] = scenario - if self.graph_scenario_id is None: + self.scenario_ids.append(scenario.id) + + if self.graph_scenario_id == "none": self.graph_scenario_id = scenario_id + return scenario def copy_scenario(self, scenario_id: str, suffix=" (Copy)") -> Scenario | None: @@ -225,8 +188,11 @@ def copy_scenario(self, scenario_id: str, suffix=" (Copy)") -> Scenario | None: def delete_scenario(self, scenario_id: str) -> Scenario | None: scenario = self.scenarios_by_id.pop(scenario_id) - if self.graph_scenario_id is scenario_id: - self.graph_scenario_id = next(self.all_scenarios, None) + self.scenario_ids.remove(scenario_id) + if self.graph_scenario_id == scenario_id: + self.graph_scenario_id = ( + self.scenario_ids[0] if len(self.scenario_ids) > 0 else "none" + ) return scenario def delete_scenarios(self, scenario_ids: Iterable[str]): @@ -240,11 +206,11 @@ def update_scenario_values(self, metric_id: str): def get_action(self, action_id: str) -> Action: return self.actions_by_id[action_id] - def create_action(self, color, icon) -> Action: + def create_action(self, color: str, icon: str, name: str | None = None) -> Action: action_id = self._create_id() action = Action( action_id, - "New Action", + name or f"New Action ({action_id})", color, icon, { @@ -254,10 +220,12 @@ def create_action(self, color, icon) -> Action: ) self.actions_by_id[action.id] = action + self.action_ids.append(action.id) return action def delete_action(self, action_id: str) -> Action | None: action = self.actions_by_id.pop(action_id) + self.action_ids.remove(action_id) return action def delete_actions(self, action_ids: Iterable[str]): @@ -281,6 +249,8 @@ def create_pathway( ) -> Pathway: pathway = Pathway(action_id, parent_pathway_id) self.pathways_by_id[pathway.id] = pathway + self.pathway_ids.append(pathway.id) + for metric in self.all_metrics(): self.update_pathway_values(metric.id) @@ -340,12 +310,12 @@ def _update_pathway_value( def delete_pathway(self, pathway_id: str) -> Pathway | None: pathway = self.pathways_by_id.pop(pathway_id, None) + self.pathway_ids.append(pathway_id) return pathway def delete_pathways(self, pathway_ids: Iterable[str]): ids_to_delete: set[str] = set() ids_to_delete.update(pathway_ids) - print(ids_to_delete) # Delete any orphaned children for pathway in self.all_pathways: @@ -357,13 +327,8 @@ def delete_pathways(self, pathway_ids: Iterable[str]): ids_to_delete.add(pathway.id) for pathway_id in ids_to_delete: - print(pathway_id) self.delete_pathway(pathway_id) - def delete_selected_pathways(self): - self.delete_pathways(self.selected_pathway_ids) - self.selected_pathway_ids.clear() - def get_children(self, pathway_id: str): return ( pathway for pathway in self.all_pathways if pathway.parent_id == pathway_id @@ -388,3 +353,8 @@ def get_ancestors_and_self(self, pathway: Pathway): current_pathway = self.get_pathway(current_pathway.parent_id) else: current_pathway = None + + +class PathwaysProjectEncoder(JSONEncoder): + def default(self, o): + return o.__dict__ diff --git a/source/package/adaptation_pathways/app/model/scenario.py b/source/package/adaptation_pathways/app/model/scenario.py index 6de1b79..71dc5e7 100644 --- a/source/package/adaptation_pathways/app/model/scenario.py +++ b/source/package/adaptation_pathways/app/model/scenario.py @@ -114,8 +114,8 @@ def _get_previous_value( ) -> tuple[int, MetricValue, int] | None: for index in range(year_index - 1, -1, -1): data = self.yearly_data[index] - metric_value = data.metric_data[metric_id] - if not metric_value.is_estimate: + metric_value = data.metric_data.get(metric_id, None) + if metric_value is not None and not metric_value.is_estimate: return (data.year, metric_value, index) return None @@ -124,8 +124,8 @@ def _get_next_value( ) -> tuple[int, MetricValue, int] | None: for index in range(year_index + 1, len(self.yearly_data)): data = self.yearly_data[index] - metric_value = data.metric_data[metric_id] - if not metric_value.is_estimate: + metric_value = data.metric_data.get(metric_id, None) + if metric_value is not None and not metric_value.is_estimate: return (data.year, metric_value, index) return None diff --git a/source/package/pathways_app/pathways_app/__init__.py b/source/package/pathways_app/__init__.py similarity index 100% rename from source/package/pathways_app/pathways_app/__init__.py rename to source/package/pathways_app/__init__.py diff --git a/source/package/pathways_app/assets/js/pathways.js b/source/package/pathways_app/assets/js/pathways.js new file mode 100644 index 0000000..80c94e9 --- /dev/null +++ b/source/package/pathways_app/assets/js/pathways.js @@ -0,0 +1,39 @@ +setTimeout(function () { + var oldMessageHandler = pythonWorker.onmessage; + pythonWorker.onmessage = function (message) { + if (message.data == "open_project") { + openFile(); + } else { + oldMessageHandler(message); + } + }; +}); + +var file_open_input = document.createElement("input"); +file_open_input.type = "file"; +file_open_input.accept = ".pwproj"; +file_open_input.multiple = false; + +function openFile() { + file_open_input.onchange = (e) => { + // getting a hold of the file reference + var file = e.target.files[0]; + console.log(file); + + // setting up the reader + var reader = new FileReader(); + reader.readAsText(file, "UTF-8"); + + reader.onload = (readerEvent) => { + var content = readerEvent.target.result; + pythonWorker.postMessage( + JSON.stringify({ + action: "open_project_result", + payload: content, + }) + ); + }; + }; + + file_open_input.click(); +} diff --git a/source/package/pathways_app/main.py b/source/package/pathways_app/main.py index 1851c8b..ae8bd6b 100644 --- a/source/package/pathways_app/main.py +++ b/source/package/pathways_app/main.py @@ -1,10 +1,15 @@ import logging import flet as ft -from pathways_app.cli.app import main +from src.cli.app import main logging.basicConfig(level=logging.CRITICAL) -ft.app(target=main, assets_dir="assets", view=ft.AppView.WEB_BROWSER) +ft.app( + target=main, + assets_dir="assets", + view=ft.AppView.FLET_APP, + route_url_strategy="hash", +) print("Pathways App Started") diff --git a/source/package/pathways_app/pathways_app/cli/app.py b/source/package/pathways_app/pathways_app/cli/app.py deleted file mode 100644 index c80189e..0000000 --- a/source/package/pathways_app/pathways_app/cli/app.py +++ /dev/null @@ -1,44 +0,0 @@ -import locale - -import flet as ft - -from .. import theme -from ..controls.editor_page import EditorPage -from ..controls.menu_bar import MenuBar -from ..pathways_app import PathwaysApp - - -locale.setlocale(locale.LC_ALL, "") - - -def main(page: ft.Page): - # bitdojo_window could make a custom title bar - # page.window.frameless = True - # page.window.width = 1200 - # page.window.height = 800 - page.theme = theme.theme - page.theme_mode = ft.ThemeMode.LIGHT - - page.window.resizable = True - page.window.alignment = ft.alignment.center - page.window.maximized = True - - page.title = "Pathways Generator" - page.fonts = theme.fonts - page.bgcolor = theme.colors.primary_darker - page.padding = 1 - page.spacing = 0 - - app = PathwaysApp(page) - page.appbar = MenuBar(app) - page.overlay.append(app.file_picker) - - page.add( - ft.Container( - expand=True, - padding=theme.variables.panel_spacing, - content=EditorPage(app.project), - bgcolor=theme.colors.primary_lighter, - border_radius=ft.border_radius.only(bottom_left=8, bottom_right=8), - ) - ) diff --git a/source/package/pathways_app/pathways_app/controls/__init__.py b/source/package/pathways_app/pathways_app/controls/__init__.py deleted file mode 100644 index 800b7ae..0000000 --- a/source/package/pathways_app/pathways_app/controls/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Customized Flet controls -""" diff --git a/source/package/pathways_app/pathways_app/controls/editor_page.py b/source/package/pathways_app/pathways_app/controls/editor_page.py deleted file mode 100644 index e176201..0000000 --- a/source/package/pathways_app/pathways_app/controls/editor_page.py +++ /dev/null @@ -1,167 +0,0 @@ -import flet as ft - -from adaptation_pathways.app.model.pathways_project import PathwaysProject - -from .. import theme -from .actions_editor import ActionsEditor -from .graph_editor import GraphEditor -from .header import SectionHeader -from .metrics_editor import MetricsEditor -from .panel import Panel -from .pathways_editor import PathwaysPanel -from .scenarios_editor import ScenariosEditor -from .tabbed_panel import TabbedPanel - - -class EditorPage(ft.Row): - def __init__(self, project: PathwaysProject): - self.project = project - self.expanded_editor: ft.Control | None = None - - self.metrics_editor = MetricsEditor(project) - self.metrics_tab = ( - SectionHeader(theme.icons.metrics, size=20), - self.metrics_editor, - ) - - self.actions_editor = ActionsEditor(project) - self.actions_tab = ( - SectionHeader(theme.icons.actions, size=20), - self.actions_editor, - ) - - self.scenarios_editor = ScenariosEditor(project) - self.scenarios_tab = ( - SectionHeader(theme.icons.scenarios, size=20), - self.scenarios_editor, - ) - - self.tabbed_panel = TabbedPanel( - selected_index=0, - tabs=[ - self.metrics_tab, - self.actions_tab, - self.scenarios_tab, - ], - ) - - self.graph_editor = GraphEditor(project) - self.graph_panel = Panel(self.graph_editor) - - self.pathways_editor = PathwaysPanel(project) - self.pathways_panel = Panel( - content=ft.Column( - expand=True, - alignment=ft.MainAxisAlignment.START, - controls=[ - self.pathways_editor, - ], - ), - padding=theme.variables.panel_padding, - ) - - self.metrics_editor.header.on_expand = lambda: self.on_editor_expanded( - self.tabbed_panel - ) - self.actions_editor.header.on_expand = lambda: self.on_editor_expanded( - self.tabbed_panel - ) - self.scenarios_editor.header.on_expand = lambda: self.on_editor_expanded( - self.tabbed_panel - ) - self.graph_editor.header.on_expand = lambda: self.on_editor_expanded( - self.graph_panel - ) - self.pathways_editor.header.on_expand = lambda: self.on_editor_expanded( - self.pathways_panel - ) - - self.left_column = ft.Column( - expand=2, - spacing=theme.variables.panel_spacing, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[self.tabbed_panel], - ) - - self.right_column = ft.Column( - expand=3, - spacing=theme.variables.panel_spacing, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[self.graph_panel, self.pathways_panel], - ) - - super().__init__( - expand=True, - spacing=theme.variables.panel_spacing, - controls=[self.left_column, self.right_column], - ) - - self.update_layout() - - project.on_conditions_changed.append(self.on_metrics_changed) - project.on_criteria_changed.append(self.on_metrics_changed) - project.on_scenarios_changed.append(self.on_scenarios_changed) - project.on_actions_changed.append(self.on_actions_changed) - project.on_action_color_changed.append(self.on_action_color_changed) - project.on_pathways_changed.append(self.on_pathways_changed) - - def update_layout(self): - if self.expanded_editor is None: - self.left_column.visible = True - self.right_column.visible = True - self.graph_panel.visible = True - self.pathways_panel.visible = True - else: - self.left_column.visible = self.expanded_editor == self.tabbed_panel - self.pathways_panel.visible = self.expanded_editor == self.pathways_panel - self.graph_panel.visible = self.expanded_editor == self.graph_panel - self.right_column.visible = ( - self.pathways_panel.visible or self.graph_panel.visible - ) - - self.metrics_editor.header.set_expanded( - self.expanded_editor == self.tabbed_panel - ) - self.actions_editor.header.set_expanded( - self.expanded_editor == self.tabbed_panel - ) - self.scenarios_editor.header.set_expanded( - self.expanded_editor == self.tabbed_panel - ) - self.pathways_editor.header.set_expanded( - self.expanded_editor == self.pathways_panel - ) - self.graph_editor.header.set_expanded(self.expanded_editor == self.graph_panel) - - def on_editor_expanded(self, editor): - if self.expanded_editor == editor: - self.expanded_editor = None - else: - self.expanded_editor = editor - - self.update_layout() - self.update() - - def on_metrics_changed(self): - self.metrics_editor.redraw() - self.scenarios_editor.redraw() - self.actions_editor.redraw() - self.pathways_editor.redraw() - self.graph_editor.redraw() - - def on_scenarios_changed(self): - self.scenarios_editor.redraw() - self.graph_editor.redraw() - - def on_actions_changed(self): - self.actions_editor.redraw() - self.pathways_editor.redraw() - self.graph_editor.redraw() - - def on_pathways_changed(self): - self.pathways_editor.redraw() - self.graph_editor.redraw() - - def on_action_color_changed(self): - self.pathways_editor.redraw() - self.graph_editor.redraw() diff --git a/source/package/pathways_app/pathways_app/controls/menu_bar.py b/source/package/pathways_app/pathways_app/controls/menu_bar.py deleted file mode 100644 index 976fe7b..0000000 --- a/source/package/pathways_app/pathways_app/controls/menu_bar.py +++ /dev/null @@ -1,119 +0,0 @@ -import flet as ft - -from .. import theme -from ..config import Config -from ..pathways_app import PathwaysApp - - -class MenuBar(ft.Container): - def __init__(self, app: PathwaysApp): - self.app = app - - super().__init__( - content=ft.Stack( - [ - ft.Row( - [ - ft.Image(theme.icon, height=36, width=36), - ft.Text("PATHWAYS\nGENERATOR", style=theme.text.logo), - ft.Container(width=15), - ft.SubmenuButton( - ft.Text("Project", style=theme.text.menu_button), - controls=[ - ft.MenuItemButton( - content=ft.Text("New"), - on_click=self.on_new_project, - ), - ft.MenuItemButton( - content=ft.Text("Open..."), - on_click=self.on_open_project, - ), - ft.MenuItemButton( - content=ft.Text("Save As..."), - on_click=self.on_save_project, - ), - ], - # style=theme.buttons.menu_bar_button, - ), - ft.SubmenuButton( - ft.Text("Help", style=theme.text.menu_button), - controls=[ - ft.MenuItemButton( - content=ft.Text("About Pathways"), - on_click=lambda e: app.open_link( - Config.about_url - ), - ), - ft.MenuItemButton( - content=ft.Text("GitHub Repository"), - on_click=lambda e: app.open_link( - Config.github_url - ), - ), - ft.Container( - content=ft.Text( - "version 1.0.0", - color=theme.colors.primary_light, - ), - padding=ft.padding.symmetric(5, 10), - ), - ], - # style=theme.buttons.menu_bar_button, - menu_style=theme.buttons.submenu, - ), - ], - expand=True, - ), - ft.Row( - [ - ft.Container( - bgcolor=theme.colors.primary_medium, - border_radius=theme.variables.small_radius, - alignment=ft.alignment.center, - padding=ft.padding.symmetric(0, 15), - width=300, - content=ft.Stack( - [ - ft.Column( - controls=[ - ft.Text( - app.project.name, - color=theme.colors.true_white, - ), - ft.Text( - app.project.organization, - text_align=ft.TextAlign.CENTER, - color=theme.colors.true_white, - ), - ], - spacing=0, - alignment=ft.MainAxisAlignment.CENTER, - horizontal_alignment=ft.CrossAxisAlignment.CENTER, - ) - ] - ), - ) - ], - alignment=ft.MainAxisAlignment.CENTER, - vertical_alignment=ft.MainAxisAlignment.CENTER, - ), - ] - ), - height=50, - padding=ft.padding.symmetric(4, 5), - margin=0, - bgcolor=theme.colors.primary_dark, - border_radius=ft.border_radius.only(top_left=0, top_right=0), - border=ft.border.only( - bottom=ft.border.BorderSide(1, theme.colors.primary_darker) - ), - ) - - def on_new_project(self): - pass - - def on_open_project(self, _): - self.app.open_project() - - def on_save_project(self): - pass diff --git a/source/package/pathways_app/pathways_app/controls/metrics_editor.py b/source/package/pathways_app/pathways_app/controls/metrics_editor.py deleted file mode 100644 index 2deacee..0000000 --- a/source/package/pathways_app/pathways_app/controls/metrics_editor.py +++ /dev/null @@ -1,154 +0,0 @@ -# from typing import Callable - -import flet as ft - -from adaptation_pathways.app.model.metric import Metric -from adaptation_pathways.app.model.pathways_project import PathwaysProject - -from .. import theme -from .editable_cell import EditableTextCell -from .header import SmallHeader -from .panel_header import PanelHeader -from .styled_table import StyledTable, TableColumn, TableRow -from .unit_cell import MetricUnitCell - - -class MetricsEditor(ft.Column): - def __init__(self, project: PathwaysProject): - super().__init__() - - self.project = project - self.header = PanelHeader("Metrics", theme.icons.metrics) - self.expand = True - self.horizontal_alignment = ft.CrossAxisAlignment.STRETCH - self.spacing = 40 - - self.conditions_table = StyledTable( - columns=[ - TableColumn(label="Name"), - TableColumn(label="Unit"), - ], - rows=[], - show_checkboxes=True, - on_add=self.on_new_condition, - on_delete=self.on_delete_conditions, - pre_operation_content=ft.Row( - [ - SmallHeader("Conditions"), - ft.Container(expand=True), - ], - expand=True, - ), - ) - - self.criteria_table = StyledTable( - columns=[ - TableColumn(label="Name"), - TableColumn(label="Unit"), - ], - rows=[], - show_checkboxes=True, - on_add=self.on_new_criteria, - on_delete=self.on_delete_criteria, - pre_operation_content=ft.Row( - [ - SmallHeader("Criteria"), - ft.Container(expand=True), - ], - expand=True, - ), - ) - - self.update_metrics() - - self.controls = [ - ft.Column( - expand=True, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - self.header, - self.conditions_table, - ], - ), - ft.Column( - expand=True, - horizontal_alignment=ft.CrossAxisAlignment.STRETCH, - controls=[ - self.criteria_table, - ], - ), - ] - - def redraw(self): - self.update_metrics() - self.update() - - def on_metric_updated(self, _): - self.project.notify_conditions_changed() - - def on_condition_selected(self, metric: Metric): - if metric.id in self.project.selected_condition_ids: - self.project.selected_condition_ids.remove(metric.id) - else: - self.project.selected_condition_ids.add(metric.id) - - self.redraw() - - def on_criteria_selected(self, metric: Metric): - if metric.id in self.project.selected_criteria_ids: - self.project.selected_criteria_ids.remove(metric.id) - else: - self.project.selected_criteria_ids.add(metric.id) - - self.redraw() - - def on_new_condition(self): - metric = self.project.create_condition() - self.project.update_pathway_values(metric.id) - self.project.notify_conditions_changed() - - def on_delete_conditions(self, rows: list[TableRow]): - for row in rows: - self.project.delete_condition(row.row_id) - self.project.notify_conditions_changed() - - def on_new_criteria(self): - metric = self.project.create_criteria() - self.project.update_pathway_values(metric.id) - self.project.notify_criteria_changed() - - def on_delete_criteria(self, rows: list[TableRow]): - for row in rows: - self.project.delete_criteria(row.row_id) - self.project.notify_criteria_changed() - - def get_metric_row( - self, - metric: Metric, - # selected_ids: set[str], - # on_metric_selected: Callable[[Metric], None], - ) -> TableRow: - row = TableRow( - row_id=metric.id, - cells=[ - EditableTextCell(metric, "name", self.on_metric_updated), - MetricUnitCell(metric, self.on_metric_updated), - ], - ) - return row - - def update_metrics(self): - self.conditions_table.set_rows( - self.get_metric_row(metric) for metric in self.project.all_conditions - ) - - self.criteria_table.set_rows( - [ - self.get_metric_row( - metric, - # self.project.selected_criteria_ids, - # self.on_criteria_selected, - ) - for metric in self.project.all_criteria - ] - ) diff --git a/source/package/pathways_app/pathways_app/controls/unit_cell.py b/source/package/pathways_app/pathways_app/controls/unit_cell.py deleted file mode 100644 index d46a399..0000000 --- a/source/package/pathways_app/pathways_app/controls/unit_cell.py +++ /dev/null @@ -1,195 +0,0 @@ -from typing import Callable - -import flet as ft - -from adaptation_pathways.app.model.metric import Metric, MetricUnit, default_units - -from .. import theme -from .styled_table import TableCell - - -class MetricUnitCell(TableCell): - def __init__( - self, - metric: Metric, - on_unit_change: Callable[["MetricUnitCell"], None] | None = None, - ): - self.metric = metric - - def on_default_metric_selected(unit: str): - self.metric.unit_or_default = unit - self.sort_value = metric.unit.display_name - if on_unit_change is not None: - on_unit_change(self) - - def create_unit_button(unit: MetricUnit): - return ft.MenuItemButton( - key=unit.symbol, - content=ft.Text(unit.name, style=theme.text.normal), - style=theme.buttons.submenu_button, - on_click=lambda e, u=unit: on_default_metric_selected(u.symbol), - ) - - def create_unit_submenu(name: str, controls: list[ft.Control]): - return ft.PopupMenuItem( - content=ft.SubmenuButton( - ft.Text(name, style=theme.text.normal, expand=True), - style=theme.buttons.submenu_button, - menu_style=theme.buttons.nested_submenu, - controls=controls, - expand=True, - ), - ) - - def create_submenu_header(text: str): - return ft.Container( - content=ft.Text( - value=text.upper(), style=theme.text.submenu_header, expand=True - ), - padding=ft.padding.only(left=4, right=4, top=8, bottom=4), - bgcolor=theme.colors.true_white, - expand=True, - ) - - super().__init__( - ft.PopupMenuButton( - expand=True, - style=theme.buttons.unit_menu, - menu_padding=ft.padding.all(0), - menu_position=ft.PopupMenuPosition.UNDER, - # menu_style=theme.buttons.submenu, - content=ft.Text(metric.unit.display_name, style=theme.text.normal), - items=[ - create_unit_submenu( - "Length", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.length.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.length.imperial - ), - ], - ), - create_unit_submenu( - "Area", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.area.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.area.imperial - ), - ], - ), - create_unit_submenu( - "Volume", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.volume.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.volume.imperial - ), - ], - ), - create_unit_submenu( - "Temperature", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.temperature.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.temperature.imperial - ), - ], - ), - create_unit_submenu( - "Velocity", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.velocity.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.velocity.imperial - ), - ], - ), - create_unit_submenu( - "Acceleration", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.acceleration.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.acceleration.imperial - ), - ], - ), - create_unit_submenu( - "Mass/Weight", - [ - create_submenu_header("SI"), - *( - create_unit_button(unit) - for unit in default_units.mass_weight.si - ), - create_submenu_header("Imperial"), - *( - create_unit_button(unit) - for unit in default_units.mass_weight.imperial - ), - ], - ), - create_unit_submenu( - "Time", - [ - *(create_unit_button(unit) for unit in default_units.time), - ], - ), - create_unit_submenu( - "Currency", - [ - *( - create_unit_button(unit) - for unit in default_units.currency - ), - ], - ), - create_unit_submenu( - "Relative", - [ - *( - create_unit_button(unit) - for unit in default_units.relative - ), - ], - ), - ], - ) - ) - self.sort_value = metric.unit.display_name diff --git a/source/package/pathways_app/pathways_app/example.py b/source/package/pathways_app/pathways_app/example.py deleted file mode 100644 index 6e4e375..0000000 --- a/source/package/pathways_app/pathways_app/example.py +++ /dev/null @@ -1,106 +0,0 @@ -import flet as ft - -from adaptation_pathways.app.model.action import Action, MetricEffect -from adaptation_pathways.app.model.metric import Metric, MetricValue, MetricValueState -from adaptation_pathways.app.model.pathway import Pathway -from adaptation_pathways.app.model.pathways_project import PathwaysProject - - -metric_sea_level_rise = Metric( - "sea-level-rise", - name="Sea Level Rise", - unit_or_default="cm", -) - -metric_cost = Metric( - "cost", - name="Cost", - unit_or_default="€", -) - -metric_habitat_health = Metric( - "habitat", - name="Habitat Health", - unit_or_default="Impact", -) - -action_root = Action( - "current-situation", - name="Current", - color="#999999", - icon=ft.Icons.HOME, - metric_data={ - metric_sea_level_rise.id: MetricEffect(0), - metric_cost.id: MetricEffect(0), - metric_habitat_health.id: MetricEffect(0), - }, -) - -action_sea_wall = Action( - "sea-wall", - name="Sea Wall", - color="#5A81DB", - icon=ft.Icons.WATER, - metric_data={ - metric_sea_level_rise.id: MetricEffect(10), - metric_cost.id: MetricEffect(100000), - metric_habitat_health.id: MetricEffect(-2), - }, -) - -action_pump = Action( - "pump", - name="Pump", - color="#44C1E1", - icon=ft.Icons.WATER_DROP_SHARP, - metric_data={ - metric_sea_level_rise.id: MetricEffect(5), - metric_cost.id: MetricEffect(50000), - metric_habitat_health.id: MetricEffect(-1), - }, -) - -action_nature_based = Action( - id="nature-based", - name="Nature-Based", - color="#E0C74B", - icon=ft.Icons.PARK, - metric_data={ - metric_sea_level_rise.id: MetricEffect(1), - metric_cost.id: MetricEffect(5000), - metric_habitat_health.id: MetricEffect(2), - }, -) - -root_pathway = Pathway(action_root.id) -root_pathway.metric_data[metric_sea_level_rise.id] = MetricValue(0) -root_pathway.metric_data[metric_cost.id] = MetricValue(0) -root_pathway.metric_data[metric_habitat_health.id] = MetricValue(0) - -project = PathwaysProject( - project_id="test-id", - name="Sea Level Rise Adaptation", - organization="Cork City Council", - start_year=2024, - end_year=2054, - conditions=[metric_sea_level_rise], - criteria=[metric_cost], - scenarios=[], - actions=[action_pump, action_sea_wall, action_nature_based], - pathways=[root_pathway], - root_action=action_root, - root_pathway_id=root_pathway.id, -) - -project.create_pathway(action_pump.id, root_pathway.id) -project.create_pathway(action_sea_wall.id, root_pathway.id) -project.create_pathway(action_nature_based.id, root_pathway.id) - -best_case_scenario = project.create_scenario("Best Case") -best_case_scenario.set_data( - 2025, metric_sea_level_rise.id, MetricValue(0, MetricValueState.OVERRIDE) -) -best_case_scenario.set_data( - 2050, metric_sea_level_rise.id, MetricValue(10, MetricValueState.OVERRIDE) -) -project.values_scenario_id = best_case_scenario.id diff --git a/source/package/pathways_app/pathways_app/pathways_app.py b/source/package/pathways_app/pathways_app/pathways_app.py deleted file mode 100644 index 3d46994..0000000 --- a/source/package/pathways_app/pathways_app/pathways_app.py +++ /dev/null @@ -1,27 +0,0 @@ -import flet as ft - -from . import example -from .config import Config - - -class PathwaysApp: - def __init__(self, page: ft.Page): - self.page = page - self.project = example.project - self.file_picker = ft.FilePicker(on_result=self.on_file_opened) - - def open_link(self, url: str): - self.page.launch_url(url) - - def open_project(self): - print("Open") - self.file_picker.pick_files( - "Choose a Project File", - file_type=ft.FilePickerFileType.ANY, - allowed_extensions=[Config.project_extension], - allow_multiple=False, - ) - - def on_file_opened(self, event: ft.FilePickerResultEvent): - for file in event.files: - print(file) diff --git a/source/package/pathways_app/pathways_app/cli/__init__.py b/source/package/pathways_app/src/cli/__init__.py similarity index 100% rename from source/package/pathways_app/pathways_app/cli/__init__.py rename to source/package/pathways_app/src/cli/__init__.py diff --git a/source/package/pathways_app/src/cli/app.py b/source/package/pathways_app/src/cli/app.py new file mode 100644 index 0000000..d730796 --- /dev/null +++ b/source/package/pathways_app/src/cli/app.py @@ -0,0 +1,80 @@ +import locale + +import flet as ft +from src import theme +from src.controls.menu_bar import MenuBar +from src.controls.pages.editor_page import EditorPage +from src.controls.pages.startup_page import StartupPage +from src.controls.pages.wizard_page import WizardPage +from src.pathways_app import PathwaysApp + + +locale.setlocale(locale.LC_ALL, "") + + +def main(page: ft.Page): + # bitdojo_window could make a custom title bar + # page.window.frameless = True + # page.window.width = 1200 + # page.window.height = 800 + page.theme = theme.theme + page.theme_mode = ft.ThemeMode.LIGHT + + page.window.resizable = True + page.window.alignment = ft.alignment.center + page.window.maximized = True + + page.title = "Pathways Generator" + page.fonts = theme.fonts + page.bgcolor = theme.colors.primary_darker + page.padding = 1 + page.spacing = 0 + + app = PathwaysApp(page) + + menu_bar = MenuBar(app) + page.appbar = menu_bar + + app_container = ft.Container( + expand=True, + padding=theme.variables.panel_spacing, + content=None, + bgcolor=theme.colors.primary_lighter, + border_radius=ft.border_radius.only(bottom_left=8, bottom_right=8), + alignment=ft.alignment.center, + ) + + page.add(app_container) + + def redraw(): + menu_bar.redraw() + app_container.content.redraw() + + def rerender(): + troute = ft.TemplateRoute(page.route) + + if troute.match("/wizard"): + app_container.content = WizardPage(app) + elif troute.match("/project"): + app_container.content = EditorPage(app) + else: + app_container.content = StartupPage(app) + + menu_bar.redraw() + app_container.update() + page.update() + + def render_route(_): + rerender() + + app.on_conditions_changed.append(redraw) + app.on_criteria_changed.append(redraw) + app.on_scenarios_changed.append(redraw) + app.on_actions_changed.append(redraw) + app.on_action_color_changed.append(redraw) + app.on_pathways_changed.append(redraw) + app.on_project_info_changed.append(redraw) + app.on_project_changed.append(rerender) + + page.on_route_change = render_route + page.go("/project") diff --git a/source/package/pathways_app/pathways_app/config.py b/source/package/pathways_app/src/config.py similarity index 100% rename from source/package/pathways_app/pathways_app/config.py rename to source/package/pathways_app/src/config.py diff --git a/source/package/pathways_app/pathways_app/controls/action_icon.py b/source/package/pathways_app/src/controls/action_icon.py similarity index 98% rename from source/package/pathways_app/pathways_app/controls/action_icon.py rename to source/package/pathways_app/src/controls/action_icon.py index 0bff294..d203b42 100644 --- a/source/package/pathways_app/pathways_app/controls/action_icon.py +++ b/source/package/pathways_app/src/controls/action_icon.py @@ -1,9 +1,8 @@ import flet as ft +from src import theme from adaptation_pathways.app.model.action import Action -from .. import theme - class ActionIcon(ft.Container): def __init__(self, action: Action, display_tooltip=True, size=36): diff --git a/source/package/pathways_app/pathways_app/controls/editable_cell.py b/source/package/pathways_app/src/controls/editable_cell.py similarity index 98% rename from source/package/pathways_app/pathways_app/controls/editable_cell.py rename to source/package/pathways_app/src/controls/editable_cell.py index e85f270..8ae5224 100644 --- a/source/package/pathways_app/pathways_app/controls/editable_cell.py +++ b/source/package/pathways_app/src/controls/editable_cell.py @@ -2,10 +2,10 @@ from typing import Callable import flet as ft -from pathways_app.controls.input_filters import IntInputFilter from pyparsing import abstractmethod +from src import theme -from .. import theme +from .input_filters import IntInputFilter from .styled_table import TableCell diff --git a/source/package/pathways_app/pathways_app/controls/actions_editor.py b/source/package/pathways_app/src/controls/editors/actions_editor.py similarity index 74% rename from source/package/pathways_app/pathways_app/controls/actions_editor.py rename to source/package/pathways_app/src/controls/editors/actions_editor.py index 9227af5..1ee723b 100644 --- a/source/package/pathways_app/pathways_app/controls/actions_editor.py +++ b/source/package/pathways_app/src/controls/editors/actions_editor.py @@ -2,30 +2,27 @@ import random import flet as ft +from src import theme +from src.pathways_app import PathwaysApp from adaptation_pathways.app.model.action import Action -from adaptation_pathways.app.model.pathways_project import PathwaysProject -from .. import theme -from .action_icon import ActionIcon -from .editable_cell import EditableTextCell -from .metric_effect import MetricEffectCell -from .metric_value import MetricValueCell -from .panel_header import PanelHeader -from .styled_table import StyledTable, TableCell, TableColumn, TableRow +from ..action_icon import ActionIcon +from ..editable_cell import EditableTextCell +from ..metric_effect import MetricEffectCell +from ..metric_value import MetricValueCell +from ..styled_table import StyledTable, TableCell, TableColumn, TableRow class ActionsEditor(ft.Column): - def __init__(self, project: PathwaysProject): + def __init__(self, app: PathwaysApp): super().__init__( expand=True, horizontal_alignment=ft.CrossAxisAlignment.STRETCH, spacing=40, ) - self.project = project - - self.header = PanelHeader(title="Actions", icon=theme.icons.actions) + self.app = app self.action_table = StyledTable( columns=[], @@ -41,7 +38,6 @@ def __init__(self, project: PathwaysProject): expand=True, horizontal_alignment=ft.CrossAxisAlignment.STRETCH, controls=[ - self.header, self.action_table, ], ), @@ -49,36 +45,25 @@ def __init__(self, project: PathwaysProject): def redraw(self): self.update_table() - # self.update() + self.update() def on_name_edited(self, _): - self.project.notify_actions_changed() - self.update() + self.app.notify_actions_changed() def on_cell_edited(self, cell: MetricValueCell): - self.project.update_pathway_values(cell.metric.id) - self.project.notify_actions_changed() - self.update() - - def on_action_selected(self, action: Action): - if action.id in self.project.selected_action_ids: - self.project.selected_action_ids.remove(action.id) - else: - self.project.selected_action_ids.add(action.id) - self.redraw() - self.update() + self.app.project.update_pathway_values(cell.metric.id) + self.app.notify_actions_changed() def on_delete_actions(self, rows: list[TableRow]): - self.project.delete_actions(row.row_id for row in rows) - self.project.notify_actions_changed() - self.update() + self.app.project.delete_actions(row.row_id for row in rows) + self.app.notify_actions_changed() def on_new_action(self): - self.project.create_action( + self.app.project.create_action( random.choice(theme.action_colors), random.choice(theme.action_icons), ) - self.project.notify_actions_changed() + self.app.notify_actions_changed() self.update() def update_table(self): @@ -89,7 +74,7 @@ def update_table(self): ), *( TableColumn(label=metric.name, key=metric.id) - for metric in self.project.all_metrics() + for metric in self.app.project.all_metrics() ), ] self.action_table.set_columns(columns) @@ -100,15 +85,15 @@ def create_icon_editor(self, action: Action): def on_color_picked(color: str): action.color = color action_icon.update_action(action) - self.project.notify_action_color_changed() + self.app.notify_action_color_changed() def on_icon_picked(icon: str): action.icon = icon action_icon.update_action(action) - self.project.notify_action_color_changed() + self.app.notify_action_color_changed() def on_editor_closed(_): - self.project.notify_actions_changed() + self.app.notify_actions_changed() def update_items(): action_button.items = [ @@ -170,12 +155,12 @@ def update_items(): def update_rows(self): rows = [] - for action in self.project.all_actions: - if action.id == self.project.root_pathway.action_id: + for action in self.app.project.all_actions: + if action.id == self.app.project.root_pathway.action_id: continue metric_cells = [] - for metric in self.project.all_metrics(): + for metric in self.app.project.all_metrics(): effect = action.metric_data[metric.id] metric_cells.append( MetricEffectCell( diff --git a/source/package/pathways_app/src/controls/editors/conditions_editor.py b/source/package/pathways_app/src/controls/editors/conditions_editor.py new file mode 100644 index 0000000..f936fa7 --- /dev/null +++ b/source/package/pathways_app/src/controls/editors/conditions_editor.py @@ -0,0 +1,71 @@ +# from typing import Callable + +import flet as ft +from src.pathways_app import PathwaysApp + +from adaptation_pathways.app.model.metric import Metric + +from ..editable_cell import EditableTextCell +from ..styled_table import StyledTable, TableColumn, TableRow +from ..unit_cell import MetricUnitCell + + +class ConditionsEditor(ft.Column): + def __init__( + self, app: PathwaysApp, pre_operation_content: ft.Control | None = None + ): + self.app = app + + self.conditions_table = StyledTable( + columns=[ + TableColumn(label="Name"), + TableColumn(label="Unit"), + ], + rows=[], + pre_operation_content=pre_operation_content, + on_add=self.on_new_condition, + on_delete=self.on_delete_conditions, + ) + + self.update_metrics() + + super().__init__( + [self.conditions_table], + expand=True, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + ) + + def redraw(self): + self.update_metrics() + self.update() + + def on_metric_updated(self, _): + self.app.notify_conditions_changed() + + def on_new_condition(self): + metric = self.app.project.create_condition() + self.app.project.update_pathway_values(metric.id) + self.app.notify_conditions_changed() + + def on_delete_conditions(self, rows: list[TableRow]): + for row in rows: + self.app.project.delete_condition(row.row_id) + self.app.notify_conditions_changed() + + def get_metric_row( + self, + metric: Metric, + ) -> TableRow: + row = TableRow( + row_id=metric.id, + cells=[ + EditableTextCell(metric, "name", self.on_metric_updated), + MetricUnitCell(metric, self.on_metric_updated), + ], + ) + return row + + def update_metrics(self): + self.conditions_table.set_rows( + self.get_metric_row(metric) for metric in self.app.project.all_conditions + ) diff --git a/source/package/pathways_app/src/controls/editors/criteria_editor.py b/source/package/pathways_app/src/controls/editors/criteria_editor.py new file mode 100644 index 0000000..ccee633 --- /dev/null +++ b/source/package/pathways_app/src/controls/editors/criteria_editor.py @@ -0,0 +1,78 @@ +# from typing import Callable + +import flet as ft +from src.pathways_app import PathwaysApp + +from adaptation_pathways.app.model.metric import Metric + +from ..editable_cell import EditableTextCell +from ..styled_table import StyledTable, TableColumn, TableRow +from ..unit_cell import MetricUnitCell + + +class CriteriaEditor(ft.Column): + def __init__( + self, app: PathwaysApp, pre_operation_content: ft.Control | None = None + ): + super().__init__() + + self.app = app + self.expand = True + self.horizontal_alignment = ft.CrossAxisAlignment.STRETCH + + self.criteria_table = StyledTable( + columns=[ + TableColumn(label="Name"), + TableColumn(label="Unit"), + ], + rows=[], + pre_operation_content=pre_operation_content, + on_add=self.on_new_criteria, + on_delete=self.on_delete_criteria, + ) + + self.update_metrics() + + self.controls = [ + self.criteria_table, + ] + + def redraw(self): + self.update_metrics() + self.update() + + def on_metric_updated(self, _): + self.app.notify_conditions_changed() + + def on_new_criteria(self): + metric = self.app.project.create_criteria() + self.app.project.update_pathway_values(metric.id) + self.app.notify_criteria_changed() + + def on_delete_criteria(self, rows: list[TableRow]): + for row in rows: + self.app.project.delete_criteria(row.row_id) + self.app.notify_criteria_changed() + + def get_metric_row( + self, + metric: Metric, + ) -> TableRow: + row = TableRow( + row_id=metric.id, + cells=[ + EditableTextCell(metric, "name", self.on_metric_updated), + MetricUnitCell(metric, self.on_metric_updated), + ], + ) + return row + + def update_metrics(self): + self.criteria_table.set_rows( + [ + self.get_metric_row( + metric, + ) + for metric in self.app.project.all_criteria + ] + ) diff --git a/source/package/pathways_app/pathways_app/controls/graph_editor.py b/source/package/pathways_app/src/controls/editors/graph_editor.py similarity index 68% rename from source/package/pathways_app/pathways_app/controls/graph_editor.py rename to source/package/pathways_app/src/controls/editors/graph_editor.py index 00d0e12..33e156f 100644 --- a/source/package/pathways_app/pathways_app/controls/graph_editor.py +++ b/source/package/pathways_app/src/controls/editors/graph_editor.py @@ -1,16 +1,17 @@ +import traceback from typing import Callable import flet as ft import matplotlib.pyplot from flet.matplotlib_chart import MatplotlibChart +from src import theme +from src.pathways_app import PathwaysApp -from adaptation_pathways.app.model.pathways_project import PathwaysProject from adaptation_pathways.app.service.plotting_service import PlottingService -from .. import theme -from .header import SmallHeader -from .styled_button import StyledButton -from .styled_dropdown import StyledDropdown +from ..header import SmallHeader +from ..styled_button import StyledButton +from ..styled_dropdown import StyledDropdown class GraphHeader(ft.Row): @@ -66,10 +67,10 @@ def on_sidebar_clicked(self, _): class GraphEditor(ft.Row): - def __init__(self, project: PathwaysProject): + def __init__(self, app: PathwaysApp): super().__init__(expand=False, spacing=0) - self.project = project + self.app = app self.header = GraphHeader(on_sidebar=self.on_sidebar_toggle) @@ -78,14 +79,24 @@ def __init__(self, project: PathwaysProject): ) self.metric_dropdown = StyledDropdown( - value="", options=[], width=200, on_change=self.on_graph_metric_changed + value="none", + options=[], + width=200, + on_change=self.on_graph_metric_changed, ) - self.metric_dropdown.value = ( - "time" if self.project.graph_is_time else self.project.graph_metric_id + self.graph_options = ft.Column([], expand=False, spacing=3) + self.time_metric_option = StyledDropdown( + self.app.project.graph_metric_id or "none", + options=[], + on_change=self.on_time_metric_changed, ) - self.graph_options = ft.Column([], expand=False, spacing=3) + self.graph_scenario_option = StyledDropdown( + self.app.project.graph_scenario_id or "none", + options=[], + on_change=self.on_graph_scenario_changed, + ) self.update_parameters() @@ -139,6 +150,7 @@ def __init__(self, project: PathwaysProject): self.metric_dropdown, ], expand=True, + alignment=ft.MainAxisAlignment.END, horizontal_alignment=ft.CrossAxisAlignment.CENTER, ), ft.Container( @@ -161,19 +173,17 @@ def on_sidebar_toggle(self): self.update() def on_graph_metric_changed(self, _): - self.project.graph_is_time = self.metric_dropdown.value == "time" - if not self.project.graph_is_time: - self.project.graph_metric_id = self.metric_dropdown.value - print(self.project.graph_is_time) - print(self.project.graph_metric_id) + self.app.project.graph_is_time = self.metric_dropdown.value == "time" + if not self.app.project.graph_is_time: + self.app.project.graph_metric_id = self.metric_dropdown.value self.redraw() def on_time_metric_changed(self, _): - self.project.graph_metric_id = self.time_metric_option.value + self.app.project.graph_metric_id = self.time_metric_option.value self.redraw() def on_graph_scenario_changed(self, _): - self.project.graph_scenario_id = self.graph_scenario_option.value + self.app.project.graph_scenario_id = self.graph_scenario_option.value self.redraw() def redraw(self): @@ -182,35 +192,24 @@ def redraw(self): self.update() def update_parameters(self): - if self.project.graph_is_time: - self.time_metric_option = StyledDropdown( - ( - self.project.graph_metric_id - if self.project.graph_metric_id is not None - else "none" - ), - options=[ + if self.app.project.graph_is_time: + self.time_metric_option.set_options( + [ ft.dropdown.Option( - key=None, text="- Select a Condition -", disabled=True + key="none", text="- Select a Condition -", disabled=True ), *( ft.dropdown.Option( key=metric.id, text=f"{metric.name} ({metric.unit.symbol})", ) - for metric in self.project.all_conditions + for metric in self.app.project.all_conditions ), - ], - on_change=self.on_time_metric_changed, + ] ) - self.graph_scenario_option = StyledDropdown( - ( - self.project.graph_scenario_id - if self.project.graph_scenario_id is not None - else "none" - ), - options=[ + self.graph_scenario_option.set_options( + [ ft.dropdown.Option( key="none", text="- Select a Scenario -", disabled=True ), @@ -219,10 +218,9 @@ def update_parameters(self): key=scenario.id, text=f"{scenario.name}", ) - for scenario in self.project.all_scenarios + for scenario in self.app.project.all_scenarios ), - ], - on_change=self.on_graph_scenario_changed, + ] ) self.graph_options.controls = [ @@ -236,19 +234,43 @@ def update_parameters(self): else: self.graph_options.controls = [] - self.metric_dropdown.options = [ - ft.dropdown.Option( - key="time", text="Time", disabled=len(self.project.scenarios_by_id) == 0 - ), - *( + self.metric_dropdown.value = ( + "time" + if self.app.project.graph_is_time + else self.app.project.graph_metric_id or "none" + ) + self.metric_dropdown.set_options( + [ ft.dropdown.Option( - key=metric.id, text=f"{metric.name} ({metric.unit.symbol})" - ) - for metric in self.project.all_conditions - ), - ] + key="none", + text="- Choose X-Axis -", + disabled=True, + ), + ft.dropdown.Option( + key="time", + text="Time", + disabled=len(self.app.project.scenarios_by_id) == 0, + ), + *( + ft.dropdown.Option( + key=metric.id, text=f"{metric.name} ({metric.unit.symbol})" + ) + for metric in self.app.project.all_conditions + ), + ] + ) def update_graph(self): - figure, _ = PlottingService.draw_metro_map(self.project) - self.graph_container.content = MatplotlibChart(figure) - matplotlib.pyplot.close(figure) + if self.app.project.graph_metric is None: + self.graph_container.visible = False + return + + try: + figure, _ = PlottingService.draw_metro_map(self.app.project) + self.graph_container.visible = True + self.graph_container.content = MatplotlibChart(figure) + matplotlib.pyplot.close(figure) + except Exception: + print("Error when attempting to draw graph") + print(traceback.format_exc()) + self.graph_container.visible = False diff --git a/source/package/pathways_app/src/controls/editors/metrics_editor.py b/source/package/pathways_app/src/controls/editors/metrics_editor.py new file mode 100644 index 0000000..1fc7487 --- /dev/null +++ b/source/package/pathways_app/src/controls/editors/metrics_editor.py @@ -0,0 +1,65 @@ +# from typing import Callable + +import flet as ft +from src.pathways_app import PathwaysApp + +from ..editors.conditions_editor import ConditionsEditor +from ..editors.criteria_editor import CriteriaEditor +from ..header import SmallHeader + + +class MetricsEditor(ft.Column): + def __init__(self, app: PathwaysApp): + super().__init__() + + self.app = app + self.expand = True + self.horizontal_alignment = ft.CrossAxisAlignment.STRETCH + self.spacing = 40 + + self.conditions_editor = ConditionsEditor( + app, + pre_operation_content=ft.Row( + [ + SmallHeader("Conditions"), + ft.Container(expand=True), + ], + expand=True, + ), + ) + + self.criteria_editor = CriteriaEditor( + app, + pre_operation_content=ft.Row( + [ + SmallHeader("Criteria"), + ft.Container(expand=True), + ], + expand=True, + ), + ) + + self.controls = [ + ft.Column( + expand=1, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[ + self.conditions_editor, + ], + ), + ft.Column( + expand=3, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[ + self.criteria_editor, + ], + ), + ] + + def redraw(self): + self.update_metrics() + self.update() + + def update_metrics(self): + self.conditions_editor.update_metrics() + self.criteria_editor.update_metrics() diff --git a/source/package/pathways_app/pathways_app/controls/pathways_editor.py b/source/package/pathways_app/src/controls/editors/pathways_editor.py similarity index 74% rename from source/package/pathways_app/pathways_app/controls/pathways_editor.py rename to source/package/pathways_app/src/controls/editors/pathways_editor.py index 04393c6..931841c 100644 --- a/source/package/pathways_app/pathways_app/controls/pathways_editor.py +++ b/source/package/pathways_app/src/controls/editors/pathways_editor.py @@ -1,20 +1,17 @@ import flet as ft +from src import theme +from src.pathways_app import PathwaysApp from adaptation_pathways.app.model.pathway import Pathway -from adaptation_pathways.app.model.pathways_project import PathwaysProject -from .. import theme -from .action_icon import ActionIcon -from .metric_value import MetricValueCell -from .panel_header import PanelHeader -from .styled_table import StyledTable, TableCell, TableColumn, TableRow +from ..action_icon import ActionIcon +from ..metric_value import MetricValueCell +from ..styled_table import StyledTable, TableCell, TableColumn, TableRow -class PathwaysPanel(ft.Container): - def __init__(self, project: PathwaysProject): - self.project = project - - self.header = PanelHeader("Pathways", ft.Icons.ACCOUNT_TREE_OUTLINED) +class PathwaysEditor(ft.Container): + def __init__(self, app: PathwaysApp): + self.app = app self.rows_by_pathway: dict[Pathway, ft.DataRow] = {} @@ -29,7 +26,6 @@ def __init__(self, project: PathwaysProject): expand=True, horizontal_alignment=ft.CrossAxisAlignment.STRETCH, controls=[ - self.header, self.pathway_table, ], ), @@ -40,8 +36,8 @@ def redraw(self): self.update() def on_delete_pathways(self, rows: list[TableRow]): - self.project.delete_pathways(row.row_id for row in rows) - self.project.notify_pathways_changed() + self.app.project.delete_pathways(row.row_id for row in rows) + self.app.notify_pathways_changed() def update_table(self): self.pathway_table.set_columns( @@ -56,7 +52,7 @@ def update_table(self): key=metric.id, alignment=ft.alignment.center_right, ) - for metric in self.project.all_metrics() + for metric in self.app.project.all_metrics() ), ] ) @@ -64,8 +60,8 @@ def update_table(self): rows = [] self.rows_by_pathway = {} - for pathway in self.project.all_pathways: - ancestors = self.project.get_ancestors_and_self(pathway) + for pathway in self.app.project.all_pathways: + ancestors = self.app.project.get_ancestors_and_self(pathway) path = [*reversed([*ancestors])] row = self.get_pathway_row(pathway, path) self.rows_by_pathway[pathway] = row @@ -75,22 +71,22 @@ def update_table(self): return rows def on_metric_value_edited(self, cell: MetricValueCell): - self.project.update_pathway_values(cell.metric.id) - self.project.notify_pathways_changed() + self.app.project.update_pathway_values(cell.metric.id) + self.app.notify_pathways_changed() def get_pathway_row(self, pathway: Pathway, ancestors: list[Pathway]): - children = [*self.project.get_children(pathway.id)] - pathway_action = self.project.get_action(pathway.action_id) + children = [*self.app.project.get_children(pathway.id)] + pathway_action = self.app.project.get_action(pathway.action_id) unused_action_ids = [ action.id - for action in self.project.all_actions + for action in self.app.project.all_actions if not any(ancestor.action_id == action.id for ancestor in ancestors) and not any(child.action_id == action.id for child in children) ] row_controls = [ - ActionIcon(self.project.get_action(ancestor.action_id), size=26) + ActionIcon(self.app.project.get_action(ancestor.action_id), size=26) for ancestor in ancestors ] @@ -124,7 +120,7 @@ def get_pathway_row(self, pathway: Pathway, ancestors: list[Pathway]): ), ) for action_id in unused_action_ids - for action in [self.project.get_action(action_id)] + for action in [self.app.project.get_action(action_id)] ], tooltip=ft.Tooltip( "Add", @@ -154,14 +150,14 @@ def get_pathway_row(self, pathway: Pathway, ancestors: list[Pathway]): pathway.metric_data[metric.id], on_finished_editing=self.on_metric_value_edited, ) - for metric in self.project.all_metrics() + for metric in self.app.project.all_metrics() ), ], - can_be_deleted=pathway.id != self.project.root_pathway_id, + can_be_deleted=pathway.id != self.app.project.root_pathway_id, ) return row def extend_pathway(self, pathway: Pathway, action_id: str): - self.project.create_pathway(action_id, pathway.id) - self.project.notify_pathways_changed() + self.app.project.create_pathway(action_id, pathway.id) + self.app.notify_pathways_changed() diff --git a/source/package/pathways_app/src/controls/editors/project_info_editor.py b/source/package/pathways_app/src/controls/editors/project_info_editor.py new file mode 100644 index 0000000..6ce3d94 --- /dev/null +++ b/source/package/pathways_app/src/controls/editors/project_info_editor.py @@ -0,0 +1,66 @@ +import flet as ft +from src import theme +from src.pathways_app import PathwaysApp + +from ..header import SmallHeader + + +class ProjectInfoEditor(ft.Container): + def __init__(self, app: PathwaysApp): + self.app = app + + self.project_name_input = ft.TextField( + dense=True, + enable_suggestions=False, + value=self.app.project.name, + keyboard_type=ft.KeyboardType.TEXT, + bgcolor=theme.colors.true_white, + border_color=theme.colors.primary_medium, + focused_border_color=theme.colors.primary_light, + cursor_color=theme.colors.primary_medium, + text_style=theme.text.textfield, + expand=False, + content_padding=ft.padding.symmetric(12, 8), + on_blur=self.on_name_edited, + width=400, + ) + + self.project_org_input = ft.TextField( + dense=True, + enable_suggestions=False, + value=self.app.project.organization, + keyboard_type=ft.KeyboardType.TEXT, + bgcolor=theme.colors.true_white, + border_color=theme.colors.primary_medium, + focused_border_color=theme.colors.primary_light, + cursor_color=theme.colors.primary_medium, + text_style=theme.text.textfield, + expand=False, + content_padding=ft.padding.symmetric(12, 8), + on_blur=self.on_organization_edited, + width=400, + ) + + super().__init__( + ft.Column( + controls=[ + SmallHeader("Project Name"), + self.project_name_input, + SmallHeader("Organization"), + self.project_org_input, + ], + expand=False, + ), + expand=False, + ) + + def redraw(self): + pass + + def on_name_edited(self, _): + self.app.project.name = self.project_name_input.value + self.app.notify_project_info_changed() + + def on_organization_edited(self, _): + self.app.project.organization = self.project_org_input.value + self.app.notify_project_info_changed() diff --git a/source/package/pathways_app/pathways_app/controls/scenarios_editor.py b/source/package/pathways_app/src/controls/editors/scenarios_editor.py similarity index 66% rename from source/package/pathways_app/pathways_app/controls/scenarios_editor.py rename to source/package/pathways_app/src/controls/editors/scenarios_editor.py index 2fb41b9..d11e1f4 100644 --- a/source/package/pathways_app/pathways_app/controls/scenarios_editor.py +++ b/source/package/pathways_app/src/controls/editors/scenarios_editor.py @@ -2,31 +2,27 @@ from functools import partial import flet as ft +from src.pathways_app import PathwaysApp +from src.utils import find_index from adaptation_pathways.app.model.metric import Metric -from adaptation_pathways.app.model.pathways_project import PathwaysProject from adaptation_pathways.app.model.scenario import YearDataPoint -from .. import theme -from ..utils import find_index -from .editable_cell import EditableIntCell, EditableTextCell -from .header import SmallHeader -from .metric_value import MetricValueCell -from .panel_header import PanelHeader -from .styled_dropdown import StyledDropdown -from .styled_table import StyledTable, TableColumn, TableRow +from ..editable_cell import EditableIntCell, EditableTextCell +from ..header import SmallHeader +from ..metric_value import MetricValueCell +from ..styled_dropdown import StyledDropdown +from ..styled_table import StyledTable, TableColumn, TableRow class ScenariosEditor(ft.Column): - def __init__(self, project: PathwaysProject): + def __init__(self, app: PathwaysApp): super().__init__( expand=True, horizontal_alignment=ft.CrossAxisAlignment.STRETCH, ) - self.project = project - - self.header = PanelHeader("Scenarios", theme.icons.scenarios) + self.app = app self.scenario_table = StyledTable( columns=[TableColumn("Name", key="name")], @@ -42,11 +38,7 @@ def __init__(self, project: PathwaysProject): key="none", text="- Choose a Scenario -", disabled=True ) self.scenario_dropdown = StyledDropdown( - value=( - "none" - if self.project.values_scenario_id is None - else self.project.values_scenario_id - ), + value=self.app.project.values_scenario_id or "none", options=[], on_change=self.on_scenario_changed, ) @@ -64,7 +56,6 @@ def __init__(self, project: PathwaysProject): ) self.controls = [ - self.header, self.scenario_table, ft.Container(height=20), SmallHeader("Scenario Data"), @@ -80,7 +71,7 @@ def update_dropdown(self): self.no_scenario_option, *( ft.dropdown.Option(key=scenario.id, text=scenario.name) - for scenario in self.project.all_scenarios + for scenario in self.app.project.all_scenarios ), ] ) @@ -92,26 +83,26 @@ def update_scenario_table(self): scenario.id, [EditableTextCell(scenario, "name", self.on_scenario_name_edited)], ) - for scenario in self.project.all_scenarios + for scenario in self.app.project.all_scenarios ] ) def on_add_scenario(self): - self.project.create_scenario("New Scenario") - self.project.notify_scenarios_changed() + self.app.project.create_scenario("New Scenario") + self.app.notify_scenarios_changed() def on_copy_scenarios(self, rows: list[TableRow]): for row in rows: - self.project.copy_scenario(row.row_id) + self.app.project.copy_scenario(row.row_id) - self.project.notify_scenarios_changed() + self.app.notify_scenarios_changed() def on_scenario_name_edited(self): - self.project.notify_scenarios_changed() + self.app.notify_scenarios_changed() def on_delete_scenarios(self, rows: list[TableRow]): - self.project.delete_scenarios(row.row_id for row in rows) - self.project.notify_scenarios_changed() + self.app.project.delete_scenarios(row.row_id for row in rows) + self.app.notify_scenarios_changed() def update_values_table(self): self.update_values_headers() @@ -123,31 +114,31 @@ def update_values_headers(self): TableColumn(label="Year", sortable=False), *( TableColumn(label=metric.name, sortable=False) - for metric in self.project.all_conditions + for metric in self.app.project.all_conditions ), ] ) def update_values_rows(self): - if self.project.values_scenario is None: + if self.app.project.values_scenario is None: self.scenario_values_table.set_rows([]) return self.scenario_values_table.set_rows( [ self._get_year_row(point) - for point in self.project.values_scenario.yearly_data + for point in self.app.project.values_scenario.yearly_data ] ) def _get_year_row(self, point: YearDataPoint): return TableRow( - row_id=point.year, + row_id=str(point.year), cells=[ EditableIntCell(point, "year", self.on_year_edited), *( self._get_metric_cell(metric, point) - for metric in self.project.all_conditions + for metric in self.app.project.all_conditions ), ], ) @@ -160,29 +151,29 @@ def _get_metric_cell(self, metric: Metric, point: YearDataPoint): ) def on_year_edited(self, _): - for metric in self.project.all_conditions: - self.project.values_scenario.recalculate_values(metric.id) + for metric in self.app.project.all_conditions: + self.app.project.values_scenario.recalculate_values(metric.id) - self.project.values_scenario.sort_yearly_data() - self.project.notify_scenarios_changed() + self.app.project.values_scenario.sort_yearly_data() + self.app.notify_scenarios_changed() def on_metric_value_edited(self, cell: MetricValueCell): - self.project.values_scenario.recalculate_values(cell.metric.id) - self.project.notify_scenarios_changed() + self.app.project.values_scenario.recalculate_values(cell.metric.id) + self.app.notify_scenarios_changed() def on_scenario_changed(self, _): - self.project.values_scenario_id = ( + self.app.project.values_scenario_id = ( self.scenario_dropdown.value - if self.scenario_dropdown.value in self.project.scenarios_by_id + if self.scenario_dropdown.value in self.app.project.scenarios_by_id else None ) self.redraw() def on_add_year(self): - if self.project.values_scenario is None: + if self.app.project.values_scenario is None: return - scenario = self.project.values_scenario + scenario = self.app.project.values_scenario year = datetime.datetime.now().year year_count = len(scenario.yearly_data) @@ -191,10 +182,10 @@ def on_add_year(self): year = scenario.yearly_data[year_count - 1].year + 1 scenario.get_or_add_year(year) - for metric in self.project.all_conditions: + for metric in self.app.project.all_conditions: scenario.recalculate_values(metric.id) - self.project.notify_scenarios_changed() + self.app.notify_scenarios_changed() def on_delete_years(self, rows: list[TableRow]): def is_year(data: YearDataPoint, year: int): @@ -204,14 +195,14 @@ def is_year(data: YearDataPoint, year: int): row_year = int(row.row_id) data_index = find_index( - self.project.values_scenario.yearly_data, + self.app.project.values_scenario.yearly_data, partial(is_year, year=row_year), ) if data_index is None: continue - self.project.values_scenario.yearly_data.pop(data_index) + self.app.project.values_scenario.yearly_data.pop(data_index) - self.project.notify_scenarios_changed() + self.app.notify_scenarios_changed() def redraw(self): self.update_scenario_table() diff --git a/source/package/pathways_app/pathways_app/controls/header.py b/source/package/pathways_app/src/controls/header.py similarity index 98% rename from source/package/pathways_app/pathways_app/controls/header.py rename to source/package/pathways_app/src/controls/header.py index 3175713..84dbda1 100644 --- a/source/package/pathways_app/pathways_app/controls/header.py +++ b/source/package/pathways_app/src/controls/header.py @@ -1,7 +1,6 @@ # pylint: disable=too-many-arguments import flet as ft - -from .. import theme +from src import theme class SectionHeader(ft.Container): diff --git a/source/package/pathways_app/pathways_app/controls/input_filters.py b/source/package/pathways_app/src/controls/input_filters.py similarity index 100% rename from source/package/pathways_app/pathways_app/controls/input_filters.py rename to source/package/pathways_app/src/controls/input_filters.py diff --git a/source/package/pathways_app/src/controls/menu_bar.py b/source/package/pathways_app/src/controls/menu_bar.py new file mode 100644 index 0000000..0583390 --- /dev/null +++ b/source/package/pathways_app/src/controls/menu_bar.py @@ -0,0 +1,142 @@ +import flet as ft +from src import theme +from src.config import Config +from src.pathways_app import PathwaysApp + + +class MenuBar(ft.Container): + def __init__(self, app: PathwaysApp): + self.app = app + + self.project_name = ft.Text( + app.project.name, + color=theme.colors.true_white, + font_family=theme.font_family_semibold, + size=14, + ) + self.project_org = ft.Text( + app.project.organization, + text_align=ft.TextAlign.CENTER, + color=theme.colors.true_white, + size=10, + ) + + self.update_project_info() + + super().__init__( + content=ft.Stack( + [ + ft.Row( + [ + ft.Image(theme.icon, height=36, width=36), + ft.Text("PATHWAYS\nGENERATOR", style=theme.text.logo), + ft.Container(width=15), + ft.MenuBar( + style=theme.buttons.menu_bar, + controls=[ + ft.SubmenuButton( + ft.Text( + "Project", style=theme.text.menu_button + ), + controls=[ + ft.MenuItemButton( + content=ft.Text("New"), + on_click=self.on_new_project, + ), + ft.MenuItemButton( + content=ft.Text("Open..."), + on_click=self.on_open_project, + ), + ft.MenuItemButton( + content=ft.Text("Save As..."), + on_click=self.on_save_project, + ), + ], + style=theme.buttons.menu_bar_button, + ), + ft.Container(width=10), + ft.SubmenuButton( + ft.Text("Help", style=theme.text.menu_button), + controls=[ + ft.MenuItemButton( + content=ft.Text("About Pathways"), + on_click=lambda e: app.open_link( + Config.about_url + ), + ), + ft.MenuItemButton( + content=ft.Text("GitHub Repository"), + on_click=lambda e: app.open_link( + Config.github_url + ), + ), + ft.Container( + content=ft.Text( + "version 1.0.0", + color=theme.colors.primary_light, + ), + padding=ft.padding.symmetric(5, 10), + ), + ], + style=theme.buttons.menu_bar_button, + menu_style=theme.buttons.submenu, + ), + ], + ), + ], + expand=True, + ), + ft.Row( + [ + ft.Container( + bgcolor=theme.colors.primary_medium, + border_radius=theme.variables.small_radius, + alignment=ft.alignment.center, + padding=ft.padding.symmetric(0, 15), + width=300, + content=ft.Stack( + [ + ft.Column( + controls=[ + self.project_name, + self.project_org, + ], + spacing=0, + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + ) + ] + ), + ) + ], + alignment=ft.MainAxisAlignment.CENTER, + vertical_alignment=ft.MainAxisAlignment.CENTER, + ), + ] + ), + height=50, + padding=ft.padding.symmetric(4, 5), + margin=0, + bgcolor=theme.colors.primary_dark, + border_radius=ft.border_radius.only(top_left=0, top_right=0), + border=ft.border.only( + bottom=ft.border.BorderSide(1, theme.colors.primary_darker) + ), + ) + + def update_project_info(self): + self.project_name.value = self.app.project.name + self.project_org.value = self.app.project.organization.upper() + + def redraw(self): + self.update_project_info() + self.update() + + def on_new_project(self, _): + self.app.new_project() + + def on_open_project(self, _): + self.app.open_project() + + def on_save_project(self, _): + self.app.save_project() diff --git a/source/package/pathways_app/pathways_app/controls/metric_effect.py b/source/package/pathways_app/src/controls/metric_effect.py similarity index 97% rename from source/package/pathways_app/pathways_app/controls/metric_effect.py rename to source/package/pathways_app/src/controls/metric_effect.py index 84c5820..55a8a4b 100644 --- a/source/package/pathways_app/pathways_app/controls/metric_effect.py +++ b/source/package/pathways_app/src/controls/metric_effect.py @@ -1,11 +1,11 @@ from typing import Callable import flet as ft +from src import theme from adaptation_pathways.app.model.action import MetricEffect from adaptation_pathways.app.model.metric import Metric, MetricOperation -from .. import theme from .editable_cell import EditableCell from .metric_value import FloatInputFilter from .styled_dropdown import StyledDropdown @@ -32,7 +32,7 @@ def __init__( def on_input_blurred(_): if not self.input_focused and not self.dropdown_focused: - self.toggle_editing(_) + self.set_not_editing(_) self.operation_dropdown = StyledDropdown( "", @@ -116,5 +116,5 @@ def update_input(self): self.value_input.value = self.effect.value - def on_reset_to_calculated(self): + def on_reset_to_calculated(self, _): pass diff --git a/source/package/pathways_app/pathways_app/controls/metric_value.py b/source/package/pathways_app/src/controls/metric_value.py similarity index 99% rename from source/package/pathways_app/pathways_app/controls/metric_value.py rename to source/package/pathways_app/src/controls/metric_value.py index 3f546bd..bded0bf 100644 --- a/source/package/pathways_app/pathways_app/controls/metric_value.py +++ b/source/package/pathways_app/src/controls/metric_value.py @@ -1,8 +1,8 @@ import flet as ft +from src import theme from adaptation_pathways.app.model.metric import Metric, MetricValue, MetricValueState -from .. import theme from .editable_cell import EditableCell from .input_filters import FloatInputFilter diff --git a/source/package/pathways_app/src/controls/pages/editor_page.py b/source/package/pathways_app/src/controls/pages/editor_page.py new file mode 100644 index 0000000..163a4f9 --- /dev/null +++ b/source/package/pathways_app/src/controls/pages/editor_page.py @@ -0,0 +1,173 @@ +import flet as ft +from src import theme +from src.pathways_app import PathwaysApp + +from ..editors.actions_editor import ActionsEditor +from ..editors.graph_editor import GraphEditor +from ..editors.metrics_editor import MetricsEditor +from ..editors.pathways_editor import PathwaysEditor +from ..editors.project_info_editor import ProjectInfoEditor +from ..editors.scenarios_editor import ScenariosEditor +from ..panel import Panel +from ..panel_header import PanelHeader +from ..tabbed_panel import TabbedPanel + + +class EditorPage(ft.Row): + def __init__(self, app: PathwaysApp): + self.app = app + self.expanded_editor: ft.Control | None = None + + self.metrics_editor = MetricsEditor(app) + self.metrics_header = PanelHeader("Metrics", theme.icons.metrics) + self.metrics_tab = ( + self.get_tab_button(theme.icons.metrics, "Metrics"), + ft.Column([self.metrics_header, self.metrics_editor]), + ) + + self.actions_editor = ActionsEditor(app) + self.actions_header = PanelHeader(title="Actions", icon=theme.icons.actions) + self.actions_tab = ( + self.get_tab_button(theme.icons.actions, "Actions"), + ft.Column([self.actions_header, self.actions_editor], expand=True), + ) + + self.scenarios_editor = ScenariosEditor(app) + self.scenarios_header = PanelHeader("Scenarios", theme.icons.scenarios) + self.scenarios_tab = ( + self.get_tab_button(theme.icons.scenarios, "Scenarios"), + ft.Column([self.scenarios_header, self.scenarios_editor], expand=True), + ) + + self.project_info_editor = ProjectInfoEditor(app) + self.project_info_header = PanelHeader("Project Info", theme.icons.project_info) + self.project_info_tab = ( + self.get_tab_button(theme.icons.project_info, "Project Info"), + ft.Column( + [self.project_info_header, self.project_info_editor], expand=True + ), + ) + + self.tabbed_panel = TabbedPanel( + selected_index=0, + tabs=[ + self.metrics_tab, + self.actions_tab, + self.scenarios_tab, + self.project_info_tab, + ], + on_tab_changed=self.redraw, + ) + + self.graph_editor = GraphEditor(app) + self.graph_panel = Panel(self.graph_editor) + + self.pathways_editor = PathwaysEditor(app) + self.pathways_header = PanelHeader("Pathways", ft.Icons.ACCOUNT_TREE_OUTLINED) + self.pathways_panel = Panel( + content=ft.Column( + expand=True, + alignment=ft.MainAxisAlignment.START, + controls=[ + self.pathways_header, + self.pathways_editor, + ], + ), + padding=theme.variables.panel_padding, + ) + + self.metrics_header.on_expand = lambda: self.on_editor_expanded( + self.tabbed_panel + ) + self.actions_header.on_expand = lambda: self.on_editor_expanded( + self.tabbed_panel + ) + self.scenarios_header.on_expand = lambda: self.on_editor_expanded( + self.tabbed_panel + ) + self.graph_editor.header.on_expand = lambda: self.on_editor_expanded( + self.graph_panel + ) + self.pathways_header.on_expand = lambda: self.on_editor_expanded( + self.pathways_panel + ) + + self.left_column = ft.Column( + expand=2, + spacing=theme.variables.panel_spacing, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[self.tabbed_panel], + ) + + self.right_column = ft.Column( + expand=3, + spacing=theme.variables.panel_spacing, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + controls=[self.graph_panel, self.pathways_panel], + ) + + super().__init__( + expand=True, + spacing=theme.variables.panel_spacing, + controls=[self.left_column, self.right_column], + ) + + self.update_layout() + + def get_tab_button(self, icon: str, tooltip: str): + return ft.Container( + ft.Icon( + icon, + size=20, + color=theme.colors.primary_dark, + ), + width=50, + height=50, + tooltip=ft.Tooltip( + tooltip, + wait_duration=0, + bgcolor=theme.colors.primary_medium, + ), + ) + + def update_layout(self): + if self.expanded_editor is None: + self.left_column.visible = True + self.right_column.visible = True + self.graph_panel.visible = True + self.pathways_panel.visible = True + else: + self.left_column.visible = self.expanded_editor == self.tabbed_panel + self.pathways_panel.visible = self.expanded_editor == self.pathways_panel + self.graph_panel.visible = self.expanded_editor == self.graph_panel + self.right_column.visible = ( + self.pathways_panel.visible or self.graph_panel.visible + ) + + self.metrics_header.set_expanded(self.expanded_editor == self.tabbed_panel) + self.actions_header.set_expanded(self.expanded_editor == self.tabbed_panel) + self.scenarios_header.set_expanded(self.expanded_editor == self.tabbed_panel) + self.pathways_header.set_expanded(self.expanded_editor == self.pathways_panel) + self.graph_editor.header.set_expanded(self.expanded_editor == self.graph_panel) + + def on_editor_expanded(self, editor): + if self.expanded_editor == editor: + self.expanded_editor = None + else: + self.expanded_editor = editor + + self.update_layout() + self.update() + + def redraw(self): + if self.metrics_editor.page: + self.metrics_editor.redraw() + if self.scenarios_editor.page: + self.scenarios_editor.redraw() + if self.actions_editor.page: + self.actions_editor.redraw() + if self.project_info_editor.page: + self.project_info_editor.redraw() + self.pathways_editor.redraw() + self.graph_editor.redraw() + self.update() diff --git a/source/package/pathways_app/src/controls/pages/startup_page.py b/source/package/pathways_app/src/controls/pages/startup_page.py new file mode 100644 index 0000000..25cb56f --- /dev/null +++ b/source/package/pathways_app/src/controls/pages/startup_page.py @@ -0,0 +1,95 @@ +import flet as ft +from src import theme +from src.pathways_app import PathwaysApp + +from ..styled_button import StyledButton + + +class StartupPage(ft.Row): + def __init__(self, app: PathwaysApp): + self.app = app + + super().__init__( + expand=False, + alignment=ft.MainAxisAlignment.CENTER, + vertical_alignment=ft.CrossAxisAlignment.CENTER, + ) + + self.panel = ft.Container( + ft.Column( + [ + ft.Text( + "Welcome to the Pathways Generator!", + text_align=ft.TextAlign.CENTER, + size=48, + expand=True, + ), + ft.Text( + "Use this tool to generate and analyze pathways.", + text_align=ft.TextAlign.CENTER, + size=18, + expand=True, + ), + ft.Row( + [ + StyledButton( + "Start a New Project", + ft.Icons.NOTE_ADD_OUTLINED, + size=20, + on_click=self.on_new_project, + ), + ft.Column( + [ + ft.Container( + height=16, + width=1, + bgcolor=theme.colors.primary_dark, + ), + ft.Text("or", size=16), + ft.Container( + height=16, + width=1, + bgcolor=theme.colors.primary_dark, + ), + ], + spacing=10, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + ), + StyledButton( + "Open an Existing Project", + ft.Icons.FILE_OPEN_OUTLINED, + size=20, + on_click=self.on_open_project, + ), + ], + expand=False, + spacing=20, + ), + ], + expand=False, + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + ), + expand=False, + padding=40, + bgcolor=theme.colors.primary_white, + border_radius=theme.variables.large_radius, + border=ft.border.all(1, theme.colors.primary_dark), + ) + + self.controls = [ + ft.Column( + [self.panel], + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + ) + ] + + def redraw(self): + pass + + def on_new_project(self, _): + self.app.new_project() + + def on_open_project(self, _): + self.app.open_project() diff --git a/source/package/pathways_app/src/controls/pages/wizard_page.py b/source/package/pathways_app/src/controls/pages/wizard_page.py new file mode 100644 index 0000000..e841b87 --- /dev/null +++ b/source/package/pathways_app/src/controls/pages/wizard_page.py @@ -0,0 +1,245 @@ +from enum import Enum +from typing import Callable + +import flet as ft +from src import theme +from src.pathways_app import PathwaysApp + +from ..editors.actions_editor import ActionsEditor +from ..editors.conditions_editor import ConditionsEditor +from ..editors.pathways_editor import PathwaysEditor +from ..editors.project_info_editor import ProjectInfoEditor +from ..header import SectionHeader +from ..panel import Panel +from ..styled_button import StyledButton + + +class WizardStepState(Enum): + INACTIVE = 0 + ACTIVE = 1 + COMPLETE = 2 + + +class WizardStepTab(ft.Container): + def __init__( + self, + name: str, + index: int, + total: int, + state=WizardStepState.INACTIVE, + on_click: Callable[["WizardStepTab"], None] | None = None, + ): + self.icon = ft.Icon() + self.index = index + self.total = total + self.state = state + self.click_handler = on_click + + super().__init__( + ft.Column( + [self.icon, ft.Text(name)], + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + spacing=4, + ), + padding=ft.padding.symmetric(10, 16), + on_click=self.on_click_event, + expand=True, + ) + self.set_state(state) + + def on_click_event(self, _): + if self.click_handler is None: + return + + self.click_handler(self) + + def set_state(self, state: WizardStepState): + self.icon.name = ( + ft.Icons.CHECK_CIRCLE_OUTLINE + if state is WizardStepState.COMPLETE + else ft.Icons.CIRCLE_OUTLINED + ) + self.icon.color = ( + theme.colors.completed + if state is WizardStepState.COMPLETE + else theme.colors.primary_dark + ) + + self.border = ft.border.only( + bottom=( + None + if state is WizardStepState.ACTIVE + else ft.BorderSide(1, theme.colors.primary_medium) + ), + left=( + None + if state is not WizardStepState.ACTIVE or self.index == 0 + else ft.BorderSide(1, theme.colors.primary_medium) + ), + right=( + None + if state is not WizardStepState.ACTIVE or self.index == self.total - 1 + else ft.BorderSide(1, theme.colors.primary_medium) + ), + ) + + self.bgcolor = ( + theme.colors.primary_lightest + if state is not WizardStepState.ACTIVE + else theme.colors.primary_white + ) + + +class WizardPage(ft.Row): + def __init__(self, app: PathwaysApp): + self.app = app + self.explainer_text = ft.Text("This is some sort of explanation") + self.finish_button = StyledButton("Finish", on_click=self.on_finish) + self.next_button = StyledButton("Next", on_click=self.on_next) + self.back_button = StyledButton("Back", on_click=self.on_back) + self.buttons = ft.Row( + [ + ft.Container(expand=True), + self.back_button, + self.next_button, + self.finish_button, + ] + ) + self.step_content = ft.Container(expand=True) + + tab_names = ["Project", "Conditions", "Actions", "Pathways"] + tab_count = len(tab_names) + self.step_tabs = [ + WizardStepTab(name, index, tab_count, on_click=self.on_tab_clicked) + for index, name in enumerate(tab_names) + ] + + self.step_explanations = [ + "To create a project, you'll need to fill in some critical info.", + "Conditions are the metrics you want to analyze the tipping points of.", + "Actions are the steps you can take to adapt to the conditions you defined earlier.", + "Pathways are ordered sequences of actions that adapt to a condition, up to a tipping point.", + ] + + self.selected_tab_index = 0 + + self.header = ft.Container( + ft.Row( + [ + SectionHeader( + ft.Icons.EDIT_DOCUMENT, + "New Project Wizard", + ), + ft.Container(expand=True), + ft.Container( + ft.Icon(ft.Icons.CLOSE), + on_click=self.on_close, + ), + ], + expand=True, + ), + bgcolor=theme.colors.primary_lightest, + padding=ft.padding.symmetric(10, 16), + border=ft.border.only(bottom=ft.BorderSide(1, theme.colors.primary_medium)), + ) + + self.body = ft.Container( + ft.Column( + [ + self.explainer_text, + self.step_content, + self.buttons, + ] + ), + padding=theme.variables.panel_padding, + expand=True, + ) + + super().__init__( + controls=[ + ft.Column( + [ + Panel( + ft.Column( + [ + self.header, + ft.Row(self.step_tabs, spacing=0, expand=False), + self.body, + ], + expand=True, + spacing=0, + ), + expand=False, + width=600, + height=600, + ) + ], + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + ) + ], + alignment=ft.MainAxisAlignment.CENTER, + vertical_alignment=ft.CrossAxisAlignment.CENTER, + ) + self.set_selected_tab(self.selected_tab_index) + + def set_selected_tab(self, index: int): + self.selected_tab_index = index + + for i, tab in enumerate(self.step_tabs): + tab.set_state( + WizardStepState.ACTIVE + if i == self.selected_tab_index + else ( + WizardStepState.COMPLETE + if i < self.selected_tab_index + else WizardStepState.INACTIVE + ) + ) + + match self.selected_tab_index: + case 0: + # self.step_content.content = ft.Container(expand=True, bgcolor="#333020") + self.step_content.content = ProjectInfoEditor(self.app) + case 1: + self.step_content.content = ConditionsEditor(self.app) + case 2: + self.step_content.content = ActionsEditor(self.app) + case 3: + self.step_content.content = PathwaysEditor(self.app) + + self.explainer_text.value = self.step_explanations[self.selected_tab_index] + self.next_button.visible = self.selected_tab_index < (len(self.step_tabs) - 1) + self.finish_button.visible = not self.next_button.visible + + def redraw(self): + for tab in self.step_tabs: + tab.update() + + self.step_content.update() + self.step_content.content.redraw() + self.buttons.update() + self.next_button.update() + self.finish_button.update() + self.update() + + def on_tab_clicked(self, tab: WizardStepTab): + self.set_selected_tab(tab.index) + self.redraw() + + def on_back(self, _): + if self.selected_tab_index == 0: + self.app.page.go("/") + else: + self.set_selected_tab(self.selected_tab_index - 1) + self.redraw() + + def on_next(self, _): + self.set_selected_tab(self.selected_tab_index + 1) + self.redraw() + + def on_close(self, _): + self.app.page.go("/") + + def on_finish(self, _): + self.app.page.go("/project") diff --git a/source/package/pathways_app/pathways_app/controls/panel.py b/source/package/pathways_app/src/controls/panel.py similarity index 77% rename from source/package/pathways_app/pathways_app/controls/panel.py rename to source/package/pathways_app/src/controls/panel.py index 69d374f..2391639 100644 --- a/source/package/pathways_app/pathways_app/controls/panel.py +++ b/source/package/pathways_app/src/controls/panel.py @@ -1,18 +1,26 @@ import flet as ft - -from .. import theme +from src import theme class Panel(ft.Container): - def __init__(self, content=None, padding=0): + def __init__( + self, + content=None, + padding=0, + expand=True, + width: int | None = None, + height: int | None = None, + ): super().__init__( - expand=True, + expand=expand, margin=0, padding=ft.padding.symmetric(padding, padding), bgcolor=theme.colors.primary_white, border=ft.border.all(1, theme.colors.primary_light), border_radius=theme.variables.small_radius, content=content, + width=width, + height=height, clip_behavior=ft.ClipBehavior.HARD_EDGE, ) # [ diff --git a/source/package/pathways_app/pathways_app/controls/panel_header.py b/source/package/pathways_app/src/controls/panel_header.py similarity index 93% rename from source/package/pathways_app/pathways_app/controls/panel_header.py rename to source/package/pathways_app/src/controls/panel_header.py index a115085..6b9e450 100644 --- a/source/package/pathways_app/pathways_app/controls/panel_header.py +++ b/source/package/pathways_app/src/controls/panel_header.py @@ -1,8 +1,9 @@ from typing import Callable import flet as ft -from pathways_app import theme -from pathways_app.controls.header import SectionHeader +from src import theme + +from .header import SectionHeader class PanelHeader(ft.Container): diff --git a/source/package/pathways_app/pathways_app/controls/sortable_header.py b/source/package/pathways_app/src/controls/sortable_header.py similarity index 99% rename from source/package/pathways_app/pathways_app/controls/sortable_header.py rename to source/package/pathways_app/src/controls/sortable_header.py index cee2027..583fd72 100644 --- a/source/package/pathways_app/pathways_app/controls/sortable_header.py +++ b/source/package/pathways_app/src/controls/sortable_header.py @@ -2,11 +2,10 @@ from typing import Callable import flet as ft +from src import theme from adaptation_pathways.app.model.sorting import SortingInfo -from .. import theme - class SortMode(Enum): NONE = 0 diff --git a/source/package/pathways_app/pathways_app/controls/styled_button.py b/source/package/pathways_app/src/controls/styled_button.py similarity index 71% rename from source/package/pathways_app/pathways_app/controls/styled_button.py rename to source/package/pathways_app/src/controls/styled_button.py index 48ef260..6964021 100644 --- a/source/package/pathways_app/pathways_app/controls/styled_button.py +++ b/source/package/pathways_app/src/controls/styled_button.py @@ -1,6 +1,5 @@ import flet as ft - -from .. import theme +from src import theme class StyledButton(ft.FilledButton): @@ -8,11 +7,12 @@ def __init__( self, text: str | None = None, icon: str | None = None, + size=14, on_click=None, ): super().__init__( style=theme.buttons.primary, - height=28, + height=size * 2, on_click=on_click, ) @@ -20,9 +20,11 @@ def __init__( controls=[], spacing=6, vertical_alignment=ft.CrossAxisAlignment.CENTER ) if icon is not None: - row.controls.append(ft.Icon(icon, color=theme.colors.true_white, size=16)) + row.controls.append(ft.Icon(icon, color=theme.colors.true_white, size=size)) if text is not None: - row.controls.append(ft.Text(text, style=theme.buttons.primary)) + row.controls.append(ft.Text(text, style=theme.buttons.primary, size=size)) - self.content = ft.Container(content=row, padding=ft.padding.symmetric(4, 8)) + self.content = ft.Container( + content=row, padding=ft.padding.symmetric(size * 0.25, size * 0.5) + ) diff --git a/source/package/pathways_app/pathways_app/controls/styled_dropdown.py b/source/package/pathways_app/src/controls/styled_dropdown.py similarity index 59% rename from source/package/pathways_app/pathways_app/controls/styled_dropdown.py rename to source/package/pathways_app/src/controls/styled_dropdown.py index 8fafbfb..6fbcf21 100644 --- a/source/package/pathways_app/pathways_app/controls/styled_dropdown.py +++ b/source/package/pathways_app/src/controls/styled_dropdown.py @@ -1,8 +1,7 @@ # pylint: disable=too-many-arguments import flet as ft - -from .. import theme -from ..utils import find_index +from src import theme +from src.utils import find_index class StyledDropdown(ft.Dropdown): @@ -19,7 +18,7 @@ def __init__( ): super().__init__( value=value, - text_style=text_style, + text_style=text_style or theme.text.dropdown_normal, expand=False, options=[], width=width, @@ -32,32 +31,44 @@ def __init__( on_blur=on_blur, ) + self.option_icons = option_icons + + self.internal_icon = ft.Icon( + "", + color=theme.colors.primary_dark, + expand=False, + ) + self.internal_text = ft.Text("", style=self.text_style) + # self.suffix = ft.Container( + + # width=18, + # height=18, + # alignment=ft.alignment.center, + # bgcolor="#000000", + # ) + self.suffix_icon = ft.Icon( + ft.Icons.ARROW_DROP_DOWN, color=theme.colors.primary_medium + ) self.prefix = ft.Row( spacing=5, - controls=[], + controls=[self.internal_icon, self.internal_text], ) - self.text_style = text_style self.set_options(options, option_icons) self.change_callback = on_change self.on_change = self.on_value_changed def update_icon(self): - if self.option_icons is None: + option_index = find_index(self.options, lambda el: el.key == self.value) + if option_index is None: return - option_index = find_index(self.options, lambda el: el.key == self.value) - if option_index is not None: - self.prefix.visible = True - self.prefix.controls = [ - ft.Icon( - self.option_icons[option_index], - color=theme.colors.primary_dark, - expand=False, - ), - ft.Text(self.value, style=self.text_style), - ] + option = self.options[option_index] + if self.option_icons is not None: + self.internal_icon.visible = True + self.internal_icon.name = self.option_icons[option_index] else: - self.prefix.visible = False + self.internal_icon.visible = False + self.internal_text.value = option.text or option.key def set_options( self, options: list[ft.dropdown.Option], icons: list[str] | None = None diff --git a/source/package/pathways_app/pathways_app/controls/styled_table.py b/source/package/pathways_app/src/controls/styled_table.py similarity index 96% rename from source/package/pathways_app/pathways_app/controls/styled_table.py rename to source/package/pathways_app/src/controls/styled_table.py index bf35976..61a9d7f 100644 --- a/source/package/pathways_app/pathways_app/controls/styled_table.py +++ b/source/package/pathways_app/src/controls/styled_table.py @@ -2,8 +2,8 @@ from typing import Callable import flet as ft +from src import theme -from .. import theme from .sortable_header import SortableHeader, SortMode from .styled_button import StyledButton @@ -158,21 +158,34 @@ def __init__( self.selected_row_ids: set[str] = set() self.add_button = StyledButton( - add_label, ft.Icons.ADD_CIRCLE_OUTLINE, on_click=self.on_add_clicked + add_label, + ft.Icons.ADD_CIRCLE_OUTLINE, + size=12, + on_click=self.on_add_clicked, ) self.copy_button = StyledButton( - copy_label, ft.Icons.ADD_CIRCLE_OUTLINE, on_click=self.on_copy_clicked + copy_label, + ft.Icons.ADD_CIRCLE_OUTLINE, + size=12, + on_click=self.on_copy_clicked, ) self.delete_button = StyledButton( delete_label, ft.Icons.REMOVE_CIRCLE_OUTLINE, + size=12, on_click=self.on_delete_clicked, ) + self.operation_row = ft.Row( - [self.delete_button, self.copy_button, self.add_button], + [ + self.delete_button, + self.copy_button, + self.add_button, + ], expand=True, alignment=ft.MainAxisAlignment.END, ) + if pre_operation_content is not None: self.operation_row.controls.insert(0, pre_operation_content) diff --git a/source/package/pathways_app/pathways_app/controls/tabbed_panel.py b/source/package/pathways_app/src/controls/tabbed_panel.py similarity index 86% rename from source/package/pathways_app/pathways_app/controls/tabbed_panel.py rename to source/package/pathways_app/src/controls/tabbed_panel.py index 740b25b..3a588c0 100644 --- a/source/package/pathways_app/pathways_app/controls/tabbed_panel.py +++ b/source/package/pathways_app/src/controls/tabbed_panel.py @@ -1,13 +1,18 @@ +from typing import Callable + import flet as ft +from src import theme -from .. import theme from .header import SectionHeader class TabbedPanel(ft.Container): def __init__( - self, tabs: list[tuple[SectionHeader, ft.Control]], selected_index: int + self, + tabs: list[tuple[SectionHeader, ft.Control]], + selected_index: int, + on_tab_changed: Callable[[], None] | None = None, ): super().__init__( expand=True, @@ -18,11 +23,12 @@ def __init__( self.selected_index = selected_index self.tabs = tabs + self.on_tab_changed = on_tab_changed self.tab_buttons = [ ft.Container( content=tab[0], - padding=ft.padding.symmetric(10, 15), + padding=0, opacity=self.get_opacity(index), bgcolor=self.get_tab_bgcolor(index), on_click=self.on_tab_clicked, @@ -77,3 +83,6 @@ def on_tab_clicked(self, e): self.tab_content.content = self.tabs[self.selected_index][1] self.update() + + if self.on_tab_changed is not None: + self.on_tab_changed() diff --git a/source/package/pathways_app/src/controls/unit_cell.py b/source/package/pathways_app/src/controls/unit_cell.py new file mode 100644 index 0000000..fe44b63 --- /dev/null +++ b/source/package/pathways_app/src/controls/unit_cell.py @@ -0,0 +1,200 @@ +from typing import Callable + +import flet as ft +from src import theme + +from adaptation_pathways.app.model.metric import Metric, MetricUnit, default_units + +from .styled_table import TableCell + + +class MetricUnitCell(TableCell): + def __init__( + self, + metric: Metric, + on_unit_change: Callable[["MetricUnitCell"], None] | None = None, + ): + self.metric = metric + + def on_default_metric_selected(unit: str): + self.metric.unit_or_default = unit + self.sort_value = metric.unit.display_name + if on_unit_change is not None: + on_unit_change(self) + + def create_unit_button(unit: MetricUnit): + return ft.MenuItemButton( + key=unit.symbol, + content=ft.Text(unit.name, style=theme.text.normal), + style=theme.buttons.submenu_button, + on_click=lambda e, u=unit: on_default_metric_selected(u.symbol), + ) + + def create_unit_submenu(name: str, controls: list[ft.Control]): + return ft.SubmenuButton( + ft.Text(name, style=theme.text.normal, expand=True), + style=theme.buttons.submenu_button, + menu_style=theme.buttons.nested_submenu, + controls=controls, + expand=True, + ) + + def create_submenu_header(text: str): + return ft.Container( + content=ft.Text( + value=text.upper(), style=theme.text.submenu_header, expand=True + ), + padding=ft.padding.only(left=4, right=4, top=8, bottom=4), + bgcolor=theme.colors.true_white, + expand=True, + ) + + super().__init__( + ft.MenuBar( + expand=True, + style=theme.buttons.menu_bar, + controls=[ + ft.SubmenuButton( + content=ft.Text( + metric.unit.display_name, style=theme.text.normal + ), + menu_style=theme.buttons.submenu, + controls=[ + create_unit_submenu( + "Length", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.length.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.length.imperial + ), + ], + ), + create_unit_submenu( + "Area", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.area.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.area.imperial + ), + ], + ), + create_unit_submenu( + "Volume", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.volume.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.volume.imperial + ), + ], + ), + create_unit_submenu( + "Temperature", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.temperature.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.temperature.imperial + ), + ], + ), + create_unit_submenu( + "Velocity", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.velocity.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.velocity.imperial + ), + ], + ), + create_unit_submenu( + "Acceleration", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.acceleration.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.acceleration.imperial + ), + ], + ), + create_unit_submenu( + "Mass/Weight", + [ + create_submenu_header("SI"), + *( + create_unit_button(unit) + for unit in default_units.mass_weight.si + ), + create_submenu_header("Imperial"), + *( + create_unit_button(unit) + for unit in default_units.mass_weight.imperial + ), + ], + ), + create_unit_submenu( + "Time", + [ + *( + create_unit_button(unit) + for unit in default_units.time + ), + ], + ), + create_unit_submenu( + "Currency", + [ + *( + create_unit_button(unit) + for unit in default_units.currency + ), + ], + ), + create_unit_submenu( + "Relative", + [ + *( + create_unit_button(unit) + for unit in default_units.relative + ), + ], + ), + ], + ), + ], + ) + ) + self.sort_value = metric.unit.display_name diff --git a/source/package/pathways_app/src/data.py b/source/package/pathways_app/src/data.py new file mode 100644 index 0000000..8c0f51a --- /dev/null +++ b/source/package/pathways_app/src/data.py @@ -0,0 +1,107 @@ +import datetime +from uuid import uuid4 + +import flet as ft + +from adaptation_pathways.app.model.metric import ( + MetricEffect, + MetricValue, + MetricValueState, +) +from adaptation_pathways.app.model.pathways_project import PathwaysProject + + +def create_empty_project(name: str, project_id: str | None = None) -> PathwaysProject: + start_year = datetime.datetime.now().year + + project = PathwaysProject( + project_id=project_id or str(uuid4()), + name=name, + organization="", + start_year=start_year, + end_year=start_year + 100, + ) + + root_action = project.create_action("#999999", ft.Icons.HOME, name="Current") + project.root_action_id = root_action.id + + root_pathway = project.create_pathway(root_action.id) + project.root_pathway_id = root_pathway.id + + return project + + +def create_example_project() -> PathwaysProject: + project = PathwaysProject( + project_id="test-id", + name="Sea Level Rise Adaptation", + organization="Cork City Council", + start_year=2024, + end_year=2054, + graph_is_time=False, + ) + + metric_sea_level_rise = project.create_condition() + metric_sea_level_rise.name = "Sea Level Rise" + metric_sea_level_rise.unit_or_default = "cm" + + metric_cost = project.create_criteria() + metric_cost.name = "Cost" + metric_cost.unit_or_default = "€" + + metric_habitat_health = project.create_criteria() + metric_habitat_health.name = "Habitat Health" + metric_habitat_health.unit_or_default = "Impact" + + action_root = project.create_action("#999999", ft.Icons.HOME) + action_root.name = "Current" + action_root.metric_data = { + metric_sea_level_rise.id: MetricEffect(0), + metric_cost.id: MetricEffect(0), + metric_habitat_health.id: MetricEffect(0), + } + project.root_action_id = action_root.id + + action_sea_wall = project.create_action("#5A81DB", ft.Icons.WATER) + action_sea_wall.name = "Sea Wall" + action_sea_wall.metric_data = { + metric_sea_level_rise.id: MetricEffect(10), + metric_cost.id: MetricEffect(100000), + metric_habitat_health.id: MetricEffect(-2), + } + + action_pump = project.create_action("#44C1E1", ft.Icons.WATER_DROP_SHARP) + action_pump.name = "Pump" + action_pump.metric_data = { + metric_sea_level_rise.id: MetricEffect(5), + metric_cost.id: MetricEffect(50000), + metric_habitat_health.id: MetricEffect(-1), + } + + action_nature_based = project.create_action("#E0C74B", ft.Icons.PARK) + action_nature_based.name = "Nature-Based" + action_nature_based.metric_data = { + metric_sea_level_rise.id: MetricEffect(1), + metric_cost.id: MetricEffect(5000), + metric_habitat_health.id: MetricEffect(2), + } + + root_pathway = project.create_pathway(action_root.id, None) + root_pathway.metric_data[metric_sea_level_rise.id] = MetricValue(0) + root_pathway.metric_data[metric_cost.id] = MetricValue(0) + root_pathway.metric_data[metric_habitat_health.id] = MetricValue(0) + project.root_pathway_id = root_pathway.id + + project.create_pathway(action_pump.id, root_pathway.id) + project.create_pathway(action_sea_wall.id, root_pathway.id) + project.create_pathway(action_nature_based.id, root_pathway.id) + + best_case_scenario = project.create_scenario("Best Case") + best_case_scenario.set_data( + 2025, metric_sea_level_rise.id, MetricValue(0, MetricValueState.OVERRIDE) + ) + best_case_scenario.set_data( + 2050, metric_sea_level_rise.id, MetricValue(10, MetricValueState.OVERRIDE) + ) + project.values_scenario_id = best_case_scenario.id + return project diff --git a/source/package/pathways_app/src/pathways_app.py b/source/package/pathways_app/src/pathways_app.py new file mode 100644 index 0000000..be83112 --- /dev/null +++ b/source/package/pathways_app/src/pathways_app.py @@ -0,0 +1,155 @@ +import base64 +import json +from typing import Callable + +import flet as ft +import jsonpickle +from pyodide.code import run_js +from src.config import Config +from src.data import create_empty_project + +from adaptation_pathways.app.model.pathways_project import PathwaysProject + + +is_web = True +try: + import flet_js +except ModuleNotFoundError: + is_web = False + + +class PathwaysApp: + def __init__(self, page: ft.Page): + self.page = page + self.project = create_empty_project("Blank Project") + self.project.organization = "Deltares" + self.file_opener = ft.FilePicker(on_result=self.on_file_opened) + self.file_saver = ft.FilePicker(on_result=self.on_file_saved) + self.page.overlay.append(self.file_opener) + self.page.overlay.append(self.file_saver) + + self.on_conditions_changed: list[Callable[[], None]] = [] + self.on_criteria_changed: list[Callable[[], None]] = [] + self.on_scenarios_changed: list[Callable[[], None]] = [] + self.on_actions_changed: list[Callable[[], None]] = [] + self.on_action_color_changed: list[Callable[[], None]] = [] + self.on_pathways_changed: list[Callable[[], None]] = [] + self.on_project_info_changed: list[Callable[[], None]] = [] + self.on_project_changed: list[Callable[[], None]] = [] + + if is_web: + old_send = flet_js.send + + def new_send(data: str): + message = json.loads(data) + action = message.get("action", None) + if action == "open_project_result": + self.on_project_text_received(message.payload) + else: + old_send(data) + + flet_js.send = new_send + + def notify_conditions_changed(self): + for listener in self.on_conditions_changed: + listener() + + def notify_criteria_changed(self): + for listener in self.on_criteria_changed: + listener() + + def notify_scenarios_changed(self): + for listener in self.on_scenarios_changed: + listener() + + def notify_actions_changed(self): + for listener in self.on_actions_changed: + listener() + + def notify_action_color_changed(self): + for listener in self.on_action_color_changed: + listener() + + def notify_pathways_changed(self): + for listener in self.on_pathways_changed: + listener() + + def notify_project_changed(self): + for listener in self.on_project_changed: + listener() + + def notify_project_info_changed(self): + for listener in self.on_project_info_changed: + listener() + + def on_event(self, event): + print(event) + + def open_link(self, url: str): + self.page.launch_url(url) + + def new_project(self): + self.project = create_empty_project("New Project") + self.page.go("/wizard") + self.notify_project_changed() + + def open_project(self): + if is_web: + run_js( + """ + self.postMessage('open_project'); + """ + ) + else: + self.file_opener.pick_files( + "Choose a Project File", + file_type=ft.FilePickerFileType.ANY, + allowed_extensions=[Config.project_extension], + allow_multiple=False, + ) + + def on_file_opened(self, event: ft.FilePickerResultEvent): + if len(event.files) == 0: + return + + print(event.files[0].path) + with open(event.files[0].path, encoding="utf-8") as file: + text = file.read() + file.close() + + self.project = jsonpickle.decode(text) + self.page.go("/project") + self.notify_project_changed() + + def on_project_text_received(self, content: str): + project_dict = json.loads(content) + self.project = PathwaysProject(**project_dict) + self.page.go("/project") + self.notify_project_changed() + + def save_project(self): + if is_web: + text: str = jsonpickle.encode(self.project) + text_bytes = text.encode("uft-8") + text_64_bytes = base64.b64encode(text_bytes) + text_64_text = text_64_bytes.decode("utf-8") + self.open_link(f"data:text/plain;base64,{text_64_text}") + else: + print("Saving on desktop") + self.file_saver.save_file( + "Save Pathways Project", + f"{self.project.name}.{Config.project_extension}", + ) + + def on_file_saved(self, event: ft.FilePickerResultEvent): + if event.path is None: + print("NO PATH") + return + + try: + with open(event.path, "w", encoding="utf-8") as file: + text = jsonpickle.encode(self.project) + file.write(text) + file.close() + except Exception as error: + print(error) diff --git a/source/package/pathways_app/pathways_app/theme.py b/source/package/pathways_app/src/theme.py similarity index 94% rename from source/package/pathways_app/pathways_app/theme.py rename to source/package/pathways_app/src/theme.py index c4401fe..3ca6d6a 100644 --- a/source/package/pathways_app/pathways_app/theme.py +++ b/source/package/pathways_app/src/theme.py @@ -16,6 +16,7 @@ class DefaultThemeColors: calculated_bg = "#60CCCCEE" calculated_icon = "#8888AA" row_selected = "#D5F9FF" + completed = "#1BAC46" colors = DefaultThemeColors() @@ -105,6 +106,12 @@ class DefaultThemeTextStyles: color=colors.primary_dark, ) + dropdown_normal = ft.TextStyle( + font_family=font_family, + size=12, + color=colors.primary_dark, + ) + textfield = ft.TextStyle( font_family=font_family, size=12, color=colors.primary_dark ) @@ -146,6 +153,7 @@ class DefaultThemeIcons: actions = ft.Icons.CONSTRUCTION_OUTLINED metrics = ft.Icons.TUNE scenarios = ft.Icons.PUBLIC + project_info = ft.Icons.INFO_OUTLINE maximize = ft.Icons.OPEN_IN_FULL minimize = ft.Icons.CLOSE_FULLSCREEN sidebar_open = ft.Icons.VIEW_SIDEBAR_OUTLINED @@ -183,6 +191,16 @@ class DefaultThemeButtons: mouse_cursor=ft.MouseCursor.CELL, ) + menu_bar = ft.MenuStyle( + alignment=ft.alignment.center_left, + bgcolor="#00000000", + padding=ft.padding.symmetric(0, 0), + shape=ft.RoundedRectangleBorder(radius=0), + shadow_color="#00000000", + side=None, + mouse_cursor=ft.MouseCursor.CLICK, + ) + submenu = ft.MenuStyle( alignment=ft.alignment.bottom_left, bgcolor=colors.true_white, diff --git a/source/package/pathways_app/pathways_app/utils.py b/source/package/pathways_app/src/utils.py similarity index 100% rename from source/package/pathways_app/pathways_app/utils.py rename to source/package/pathways_app/src/utils.py From cacb8440c8ef3011d717ef18f5438f99779f2cd2 Mon Sep 17 00:00:00 2001 From: Kor de Jong Date: Fri, 31 Jan 2025 13:47:02 +0100 Subject: [PATCH 08/12] Patch index.html, add jsonpickle dependency, make base-url configurable --- CMakeLists.txt | 5 +++ environment/cmake/FindSed.cmake | 13 +++++++ source/package/pathways_app/CMakeLists.txt | 38 +++++++++++-------- .../package/pathways_app/patch_index_html.sed | 1 + source/package/pathways_app/pyproject.toml.in | 3 +- 5 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 environment/cmake/FindSed.cmake create mode 100644 source/package/pathways_app/patch_index_html.sed diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ebfe49..b7e28a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,9 +19,14 @@ enable_testing() list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/environment/cmake) +set(AP_BASE_URL "AdaptationPathways" CACHE STRING + "Base url for web app. Use empty string for testing locally." +) + find_package(Flet REQUIRED) find_package(Python3 REQUIRED COMPONENTS Interpreter) find_package(Quarto) +find_package(Sed) find_package(Sphinx REQUIRED) set(AP_PLOT_GRAPHS diff --git a/environment/cmake/FindSed.cmake b/environment/cmake/FindSed.cmake new file mode 100644 index 0000000..aa4185b --- /dev/null +++ b/environment/cmake/FindSed.cmake @@ -0,0 +1,13 @@ +find_program(Sed_EXECUTABLE + NAMES sed +) + +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args(Sed DEFAULT_MSG + Sed_EXECUTABLE +) + +mark_as_advanced( + Sed_EXECUTABLE +) diff --git a/source/package/pathways_app/CMakeLists.txt b/source/package/pathways_app/CMakeLists.txt index 88bf2cc..6fa4df8 100644 --- a/source/package/pathways_app/CMakeLists.txt +++ b/source/package/pathways_app/CMakeLists.txt @@ -4,18 +4,26 @@ configure_file( @ONLY ) -add_custom_target(web_app - COMMAND - ${CMAKE_COMMAND} -E copy_directory_if_different - ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} - COMMAND - ${CMAKE_COMMAND} -E env "PIP_FIND_LINKS=file://${PROJECT_BINARY_DIR}/dist" - ${Flet_EXECUTABLE} build web ${CMAKE_CURRENT_BINARY_DIR} - COMMAND - ${CMAKE_COMMAND} -E echo - "Run a command like this to start the web app:" - "${Python3_EXECUTABLE} -m http.server --directory ${CMAKE_CURRENT_BINARY_DIR}/build/web" - DEPENDS - package - VERBATIM -) +if(NOT Sed_FOUND) + # Sed is used to patch index.html. + message(WARNING "Skipping the web_app target since sed was not found") +else() + add_custom_target(web_app + COMMAND + ${CMAKE_COMMAND} -E copy_directory_if_different + ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} + COMMAND + ${CMAKE_COMMAND} -E env "PIP_FIND_LINKS=file://${PROJECT_BINARY_DIR}/dist" + ${Flet_EXECUTABLE} build web ${CMAKE_CURRENT_BINARY_DIR} + COMMAND + ${Sed_EXECUTABLE} -i -f ${CMAKE_CURRENT_SOURCE_DIR}/patch_index_html.sed + ${CMAKE_CURRENT_BINARY_DIR}/build/web/index.html + COMMAND + ${CMAKE_COMMAND} -E echo + "Run a command like this to start the web app:" + "${Python3_EXECUTABLE} -m http.server --directory ${CMAKE_CURRENT_BINARY_DIR}/build/web" + DEPENDS + package + VERBATIM + ) +endif() diff --git a/source/package/pathways_app/patch_index_html.sed b/source/package/pathways_app/patch_index_html.sed new file mode 100644 index 0000000..d1f149b --- /dev/null +++ b/source/package/pathways_app/patch_index_html.sed @@ -0,0 +1 @@ +s/\(' + replace_with = f'{search_for}\n ' + + path = Path(html_pathname) + content = path.read_text(encoding="utf-8") + content = content.replace(search_for, replace_with) + path.write_text(content, encoding="utf-8") + + status = 0 + except Exception as exception: + sys.stderr.write(f"{exception}\n") + + return status + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/source/package/pathways_app/patch_index_html.sed b/source/package/pathways_app/patch_index_html.sed deleted file mode 100644 index d1f149b..0000000 --- a/source/package/pathways_app/patch_index_html.sed +++ /dev/null @@ -1 +0,0 @@ -s/\(