diff --git a/src/controllers/navigator/navigator.py b/src/controllers/navigator/navigator.py index b52deac..e30085b 100644 --- a/src/controllers/navigator/navigator.py +++ b/src/controllers/navigator/navigator.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Generic, List, Optional, Tuple, TypeVar +from typing import Dict, Generic, List, Optional, Tuple, TypeVar from PyQt6.QtWidgets import QLabel, QStackedWidget, QVBoxLayout, QWidget from reactivex import Observable @@ -14,12 +14,30 @@ class Navigator(Generic[RouteName]): + __navigators: Dict[str, "Navigator"] = {} + __history_stack: BehaviorSubject[List[RouteName]] __routes: List[Route] __not_found_widget: Optional[QWidget] __config: NavigatorConfig - def __init__( + def __init__(self) -> None: + self.__history_stack = BehaviorSubject([]) + self.__routes = [] + self.__not_found_widget = None + self.__config = NavigatorConfig() + + @staticmethod + def get_navigator(name: str) -> "Navigator": + navigator = Navigator.__navigators.get(name) + + if navigator is None: + navigator = Navigator() + Navigator.__navigators[name] = navigator + + return navigator + + def init( self, routes: List[Route], default_name: RouteName, @@ -36,11 +54,10 @@ def __init__( Returns: None """ - self.__routes = routes - self.__history_stack = BehaviorSubject([default_name]) self.__not_found_widget = not_found_widget self.__config = config + self.__history_stack.on_next([default_name]) @property def history_stack(self) -> Observable[List[str]]: @@ -146,7 +163,9 @@ def on_route_change(res: Tuple[int, Route]) -> None: return widget def __match_name(self, route_name: str, search_name: str) -> bool: - return route_name == search_name + return route_name == search_name or ( + route_name.value == search_name if isinstance(route_name, Enum) else False + ) def __resolve_route(self, name: str) -> Tuple[int, Route]: for i, route in enumerate(self.__routes): diff --git a/src/controllers/navigator/tests/test_navigator.py b/src/controllers/navigator/tests/test_navigator.py index fa123a3..33e9384 100644 --- a/src/controllers/navigator/tests/test_navigator.py +++ b/src/controllers/navigator/tests/test_navigator.py @@ -43,7 +43,8 @@ class TestNavigator: @pytest.fixture(autouse=True) def init_navigator(self) -> Navigator[RoutesTest]: - self.navigator = Navigator[RoutesTest]( + self.navigator = Navigator[RoutesTest]() + self.navigator.init( routes=ROUTES, default_name=DEFAULT_ROUTE_NAME, not_found_widget=NotFoundWidget, diff --git a/src/main.py b/src/main.py index 9154018..eb2c53a 100644 --- a/src/main.py +++ b/src/main.py @@ -3,9 +3,12 @@ from PyQt6.QtWidgets import QApplication from src.views.window import MainWindow +from views.modules.navigators import init_navigators app = QApplication(sys.argv) +init_navigators() + window = MainWindow() window.show() diff --git a/src/models/map/__init__.py b/src/models/map/__init__.py new file mode 100644 index 0000000..b4da3f4 --- /dev/null +++ b/src/models/map/__init__.py @@ -0,0 +1,7 @@ +from src.models.map.errors import * +from src.models.map.intersection import Intersection +from src.models.map.map import Map +from src.models.map.map_size import MapSize +from src.models.map.marker import Marker +from src.models.map.position import Position +from src.models.map.segment import Segment diff --git a/src/models/map/errors.py b/src/models/map/errors.py new file mode 100644 index 0000000..5e78b01 --- /dev/null +++ b/src/models/map/errors.py @@ -0,0 +1,2 @@ +class MapLoadingError(Exception): + pass diff --git a/src/models/map/intersection.py b/src/models/map/intersection.py new file mode 100644 index 0000000..5000db9 --- /dev/null +++ b/src/models/map/intersection.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +from xml.etree.ElementTree import Element + +from src.models.map.position import Position + + +@dataclass +class Intersection(Position): + """Represent an intersection on the map.""" + + id: int + """ID of the intersection. + """ + + @staticmethod + def from_element(element: Element) -> "Intersection": + """Creates an intersection instance from an XML element. + + Args: + element (Element): XML element + + Returns: + Intersection: Intersection instance + """ + return Intersection( + id=int(element.attrib["id"]), + latitude=float(element.attrib["latitude"]), + longitude=float(element.attrib["longitude"]), + ) diff --git a/src/models/map/map.py b/src/models/map/map.py new file mode 100644 index 0000000..12e9db8 --- /dev/null +++ b/src/models/map/map.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import Dict, List + +from src.models.map.intersection import Intersection +from src.models.map.map_size import MapSize +from src.models.map.segment import Segment + + +@dataclass +class Map: + intersections: Dict[int, Intersection] + segments: List[Segment] + warehouse: Intersection + size: MapSize diff --git a/src/models/map/map_size.py b/src/models/map/map_size.py new file mode 100644 index 0000000..4e9aecb --- /dev/null +++ b/src/models/map/map_size.py @@ -0,0 +1,58 @@ +import sys +from dataclasses import dataclass +from typing import Type, TypeVar + +from src.models.map.position import Position + +T = TypeVar("T", bound="MapSize") + + +@dataclass +class MapSize: + __min: Position + __max: Position + area: float + + def __init__(self, min: Position, max: Position) -> None: + self.__min = min + self.__max = max + self.area = self.__calculate_area() + + @classmethod + def inverse_max_size(cls: Type[T]) -> T: + """Creates a MapSize instance with the inverted maximum possible size. (min = System MAX, max = System MIN)""" + return cls( + Position(sys.maxsize, sys.maxsize), + Position(sys.maxsize * -1, sys.maxsize * -1), + ) + + @property + def min(self) -> Position: + return self.__min + + @min.setter + def min(self, value: Position) -> None: + self.__min = value + self.area = self.__calculate_area() + + @property + def max(self) -> Position: + return self.__max + + @max.setter + def max(self, value: Position) -> None: + self.__max = value + self.area = self.__calculate_area() + + @property + def width(self) -> float: + return self.__max.longitude - self.__min.longitude + + @property + def height(self) -> float: + return self.__max.latitude - self.__min.latitude + + def __calculate_area(self) -> float: + return (self.__max.latitude - self.__min.latitude) * ( + self.__max.longitude - self.__min.longitude + ) diff --git a/src/models/map/marker.py b/src/models/map/marker.py new file mode 100644 index 0000000..f0efa2f --- /dev/null +++ b/src/models/map/marker.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +from src.models.map.position import Position + + +@dataclass +class Marker: + position: Position diff --git a/src/models/map/position.py b/src/models/map/position.py new file mode 100644 index 0000000..78700e8 --- /dev/null +++ b/src/models/map/position.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass +class Position: + longitude: float + """Longitude of the position. + """ + latitude: float + """Latitude of the position. + """ + + @property + def x(self) -> float: + """Get the value of the X axis. Equivalent the longitude of the position.""" + return self.longitude + + @x.setter + def x(self, value: float) -> None: + """Set the value of the X axis. Equivalent the longitude of the position.""" + self.longitude = value + + @property + def y(self) -> float: + """Get the value of the Y axis. Equivalent the latitude of the position.""" + return self.latitude + + @y.setter + def y(self, value: float) -> None: + """Set the value of the Y axis. Equivalent the latitude of the position.""" + self.latitude = value + + def max(self, p: "Position", *args: List["Position"]) -> "Position": + """Get the maximum position between the current position and the given position. + + Args: + p (Position): Other position to compare. + + Returns: + Position: New position instance with the maximum values. + """ + return Position( + max(self.longitude, p.longitude, *map(lambda p: p.longitude, args)), + max(self.latitude, p.latitude, *map(lambda p: p.latitude, args)), + ) + + def min(self, p: "Position", *args: List["Position"]) -> "Position": + """Get the minimum position between the current position and the given position. + + Args: + p (Position): Other position to compare. + + Returns: + Position: New position instance with the minimum values. + """ + return Position( + min(self.longitude, p.longitude, *map(lambda p: p.longitude, args)), + min(self.latitude, p.latitude, *map(lambda p: p.latitude, args)), + ) diff --git a/src/models/map/segment.py b/src/models/map/segment.py new file mode 100644 index 0000000..91a5d01 --- /dev/null +++ b/src/models/map/segment.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass +from typing import Dict +from xml.etree.ElementTree import Element + +from src.models.map.errors import MapLoadingError +from src.models.map.intersection import Intersection + + +@dataclass +class Segment: + name: str + origin: Intersection + destination: Intersection + length: float + + @staticmethod + def from_element( + element: Element, intersections: Dict[int, Intersection] + ) -> "Segment": + """Creates a Segment instance from an XML element. + + Args: + element (Element): XML element + intersections (Dict[int, Intersection]): Dictionary of intersections + + Returns: + Segment: Segment instance + """ + name = element.attrib["name"] + origin = intersections[int(element.attrib["origin"])] + destination = intersections[int(element.attrib["destination"])] + + if origin is None: + raise MapLoadingError( + f"No intersection with ID {element.attrib['origin']} for origin on {element.tag} {name}" + ) + if destination is None: + raise MapLoadingError( + f"No intersection with ID {element.attrib['destination']} for destination on {element.tag} {name}" + ) + + return Segment( + name=name, + origin=origin, + destination=destination, + length=float(element.attrib["length"]), + ) diff --git a/src/models/map/tests/test_map_size.py b/src/models/map/tests/test_map_size.py new file mode 100644 index 0000000..273681d --- /dev/null +++ b/src/models/map/tests/test_map_size.py @@ -0,0 +1,43 @@ +from src.models.map.map_size import MapSize +from src.models.map.position import Position + + +class TestMapSize: + def test_should_create(self): + assert MapSize(Position(0, 0), Position(0, 0)) is not None + + def test_should_create_inverse_max_size(self): + assert MapSize.inverse_max_size() is not None + + def test_should_calculate_area(self): + assert MapSize(Position(0, 0), Position(1, 1)).area == 1 + assert MapSize(Position(0, 0), Position(2, 2)).area == 4 + assert MapSize(Position(0, 0), Position(3, 3)).area == 9 + + def test_should_get_min(self): + assert MapSize(Position(0, 0), Position(1, 1)).min == Position(0, 0) + + def test_should_set_min(self): + map_size = MapSize(Position(0, 0), Position(1, 1)) + map_size.min = Position(1, 1) + + assert map_size.min == Position(1, 1) + + def test_should_get_max(self): + assert MapSize(Position(0, 0), Position(1, 1)).max == Position(1, 1) + + def test_should_set_max(self): + map_size = MapSize(Position(0, 0), Position(1, 1)) + map_size.max = Position(0, 0) + + assert map_size.max == Position(0, 0) + + def test_should_get_width(self): + assert MapSize(Position(0, 0), Position(1, 1)).width == 1 + assert MapSize(Position(0, 0), Position(2, 2)).width == 2 + assert MapSize(Position(0, 0), Position(3, 3)).width == 3 + + def test_should_get_height(self): + assert MapSize(Position(0, 0), Position(1, 1)).height == 1 + assert MapSize(Position(0, 0), Position(2, 2)).height == 2 + assert MapSize(Position(0, 0), Position(3, 3)).height == 3 diff --git a/src/models/map/tests/test_position.py b/src/models/map/tests/test_position.py new file mode 100644 index 0000000..186ba08 --- /dev/null +++ b/src/models/map/tests/test_position.py @@ -0,0 +1,26 @@ +from src.models.map.position import Position + + +class TestPosition: + def test_should_create(self): + assert Position(0, 0) is not None + + def test_should_get_longitude_equals_x(self): + LONGITUDE = 420 + + assert Position(LONGITUDE, 0).longitude == LONGITUDE + assert Position(LONGITUDE, 0).x == LONGITUDE + + def test_should_get_latitude_equals_y(self): + LATITUDE = 69 + + assert Position(0, LATITUDE).latitude == LATITUDE + assert Position(0, LATITUDE).y == LATITUDE + + def test_should_get_max(self): + assert Position(1, 1).max(Position(2, 2)) == Position(2, 2) + assert Position(1, 1).max(Position(2, 2), Position(3, 3)) == Position(3, 3) + + def test_should_get_min(self): + assert Position(1, 1).min(Position(2, 2)) == Position(1, 1) + assert Position(1, 1).min(Position(2, 2), Position(3, 3)) == Position(1, 1) diff --git a/src/models/temporary_map_loader.py b/src/models/temporary_map_loader.py deleted file mode 100644 index 3d7123e..0000000 --- a/src/models/temporary_map_loader.py +++ /dev/null @@ -1,93 +0,0 @@ -"""THIS IS ONLY FOR DEVELOPMENT, A BETTER MAP LOADER NEEDS TO BE IMPLEMENTED LATER -""" -import sys -import xml.etree.ElementTree as ET -from dataclasses import dataclass -from typing import Dict, List - - -@dataclass -class Position: - longitude: float - latitude: float - - -@dataclass -class Intersection(Position): - id: int - - -@dataclass -class Segment: - name: str - origin: Intersection - destination: Intersection - length: float - - -@dataclass -class Map: - intersections: Dict[int, Intersection] - segments: List[Segment] - max_latitude: float - min_latitude: float - max_longitude: float - min_longitude: float - - def get_size(self) -> float: - return (self.max_latitude - self.min_latitude) * ( - self.max_longitude - self.min_longitude - ) - - -class TemporaryMapLoader: - def load_map(self) -> Map: - intersections = {} - segments = [] - max_latitude: float = sys.maxsize * -1 - min_latitude: float = sys.maxsize - max_longitude: float = sys.maxsize * -1 - min_longitude: float = sys.maxsize - - tree = ET.parse("src/assets/largeMap.xml") - - for el in tree.getroot(): - if el.tag == "intersection": - intersection = Intersection( - id=int(el.attrib["id"]), - latitude=float(el.attrib["latitude"]), - longitude=float(el.attrib["longitude"]), - ) - - if intersection.latitude > max_latitude: - max_latitude = intersection.latitude - if intersection.latitude < min_latitude: - min_latitude = intersection.latitude - if intersection.longitude > max_longitude: - max_longitude = intersection.longitude - if intersection.longitude < min_longitude: - min_longitude = intersection.longitude - - intersections[intersection.id] = intersection - elif el.tag == "segment": - segment = Segment( - name=el.attrib["name"], - origin=intersections[int(el.attrib["origin"])], - destination=intersections[int(el.attrib["destination"])], - length=float(el.attrib["length"]), - ) - segments.append(segment) - - return Map( - intersections, - segments, - max_latitude, - min_latitude, - max_longitude, - min_longitude, - ) - - -if __name__ == "__main__": - loader = TemporaryMapLoader() - print(loader.load_map()) diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/services/map/__init__.py b/src/services/map/__init__.py new file mode 100644 index 0000000..d601775 --- /dev/null +++ b/src/services/map/__init__.py @@ -0,0 +1,2 @@ +from src.services.map.map_loader_service import MapLoaderService +from src.services.map.map_service import MapService diff --git a/src/services/map/map_loader_service.py b/src/services/map/map_loader_service.py new file mode 100644 index 0000000..68d764e --- /dev/null +++ b/src/services/map/map_loader_service.py @@ -0,0 +1,63 @@ +import xml.etree.ElementTree as ET +from typing import Dict, List +from xml.etree.ElementTree import Element + +from src.models.map.errors import MapLoadingError +from src.models.map.intersection import Intersection +from src.models.map.map import Map +from src.models.map.map_size import MapSize +from src.models.map.position import Position +from src.models.map.segment import Segment +from src.services.map.map_service import MapService +from src.services.singleton import Singleton + + +class MapLoaderService(Singleton): + def load_map_from_xml(self, path: str) -> Map: + """Loads an XML file, create a Map instance from it and pass it to the MapService. + + Args: + path (str): Path to the XML file to import (relative to the project root) + + Returns: + Map: Map instance + """ + return self.create_map_from_xml(ET.parse(path).getroot()) + + def create_map_from_xml(self, root_element: Element) -> Map: + """Creates a Map instance from an XML element and pass it to the MapService. + + Args: + root_element (Element): Root element of the XML + + Returns: + Map: Map instance + """ + intersections: Dict[int, Intersection] = {} + segments: List[Segment] = [] + map_size = MapSize.inverse_max_size() + warehouse: Intersection = None + + for element in root_element.findall("intersection"): + intersection = Intersection.from_element(element) + intersections[intersection.id] = intersection + self.__update_map_size(map_size, intersection) + + for element in root_element.findall("segment"): + segments.append(Segment.from_element(element, intersections)) + + for element in root_element.findall("warehouse"): + warehouse = intersections[int(element.attrib["address"])] + + if not warehouse: + raise MapLoadingError("No warehouse found in the XML file") + + map = Map(intersections, segments, warehouse, map_size) + + MapService.instance().set_map(map) + + return map + + def __update_map_size(self, map_size: MapSize, position: Position) -> None: + map_size.max = Position.max(map_size.max, position) + map_size.min = Position.min(map_size.min, position) diff --git a/src/services/map/map_service.py b/src/services/map/map_service.py new file mode 100644 index 0000000..c2bd8f6 --- /dev/null +++ b/src/services/map/map_service.py @@ -0,0 +1,38 @@ +from typing import List, Optional + +from reactivex import Observable +from reactivex.operators import map +from reactivex.subject import BehaviorSubject + +from src.models.map import Map, Marker +from src.services.singleton import Singleton + + +class MapService(Singleton): + __map: BehaviorSubject[Optional[Map]] + __markers: BehaviorSubject[List[Marker]] + + def __init__(self) -> None: + self.__map = BehaviorSubject(None) + self.__markers = BehaviorSubject([]) + + @property + def map(self) -> Observable[Optional[Map]]: + return self.__map + + @property + def is_loaded(self) -> Observable[bool]: + return self.__map.pipe(map(lambda map: map is not None)) + + def markers(self) -> Observable[List[Marker]]: + return self.__markers + + def set_map(self, map: Map) -> None: + self.__map.on_next(map) + + def clear_map(self) -> None: + self.__map.on_next(None) + self.__markers.on_next([]) + + def add_marker(self, marker: Marker) -> None: + self.__markers.on_next(self.__markers.value + [marker]) diff --git a/src/services/map/tests/test_map_loader_service.py b/src/services/map/tests/test_map_loader_service.py new file mode 100644 index 0000000..d250efa --- /dev/null +++ b/src/services/map/tests/test_map_loader_service.py @@ -0,0 +1,97 @@ +from xml.etree.ElementTree import Element + +from pytest import fixture + +from src.services.map.map_loader_service import MapLoaderService + + +class TestMapLoaderService: + map_loader_service: MapLoaderService + + @fixture(autouse=True) + def setup_method(self): + self.map_loader_service = MapLoaderService.instance() + + yield + + MapLoaderService.reset() + + @fixture + def root(self): + root = Element("map") + + root.append(Element("warehouse", attrib={"address": "1"})) + + root.append( + Element( + "intersection", attrib={"id": "1", "latitude": "0", "longitude": "0"} + ) + ) + root.append( + Element( + "intersection", attrib={"id": "2", "latitude": "0", "longitude": "1"} + ) + ) + root.append( + Element( + "intersection", attrib={"id": "3", "latitude": "1", "longitude": "0"} + ) + ) + + root.append( + Element( + "segment", + attrib={ + "name": "segment1", + "origin": "1", + "destination": "2", + "length": "1.1", + }, + ) + ) + root.append( + Element( + "segment", + attrib={ + "name": "segment2", + "origin": "1", + "destination": "3", + "length": "10.23", + }, + ) + ) + + return root + + def test_should_create(self): + assert self.map_loader_service is not None + + def test_should_create_map_from_xml(self, root): + map = self.map_loader_service.create_map_from_xml(root) + + assert map is not None + + def test_should_create_map_from_xml_with_intersections(self, root): + map = self.map_loader_service.create_map_from_xml(root) + + assert len(map.intersections) == 3 + + def test_should_create_map_from_xml_with_segments(self, root): + map = self.map_loader_service.create_map_from_xml(root) + + assert len(map.segments) == 2 + + def test_should_create_map_from_xml_with_warehouse(self, root): + map = self.map_loader_service.create_map_from_xml(root) + + assert map.warehouse is not None + + def test_should_throw_if_create_map_from_xml_without_warehouse(self, root): + root.remove(root.find("warehouse")) + + try: + self.map_loader_service.create_map_from_xml(root) + + assert False + except Exception as e: + assert str(e) == "No warehouse found in the XML file" diff --git a/src/services/map/tests/test_map_service.py b/src/services/map/tests/test_map_service.py new file mode 100644 index 0000000..b3ac8f7 --- /dev/null +++ b/src/services/map/tests/test_map_service.py @@ -0,0 +1,61 @@ +from pytest import fixture + +from src.services.map.map_service import MapService + + +class TestMapService: + map_service: MapService + + @fixture(autouse=True) + def setup_method(self): + self.map_service = MapService.instance() + + yield + + MapService.reset() + + def test_should_create(self): + assert self.map_service is not None + + def test_should_set_map(self): + self.map_service.set_map("MAP") + + def on_next(map): + assert map is not None + + self.map_service.map.subscribe(on_next) + + def test_should_clear_map(self): + self.map_service.set_map("MAP") + self.map_service.clear_map() + + def on_next(map): + assert map is None + + self.map_service.map.subscribe(on_next) + + def test_should_add_marker(self): + self.map_service.add_marker("MARKER") + + def on_next(markers): + assert markers == ["MARKER"] + + self.map_service.markers().subscribe(on_next) + + def test_should_add_multiple_markers(self): + self.map_service.add_marker("MARKER") + self.map_service.add_marker("MARKER2") + + def on_next(markers): + assert markers == ["MARKER", "MARKER2"] + + self.map_service.markers().subscribe(on_next) + + def test_should_clear_markers(self): + self.map_service.add_marker("MARKER") + self.map_service.clear_map() + + def on_next(markers): + assert markers == [] + + self.map_service.markers().subscribe(on_next) diff --git a/src/services/singleton.py b/src/services/singleton.py new file mode 100644 index 0000000..860d3a1 --- /dev/null +++ b/src/services/singleton.py @@ -0,0 +1,24 @@ +from abc import ABC +from typing import Type, TypeVar + +T = TypeVar("T", bound="Singleton") + + +class Singleton(ABC): + __instance = None + + @classmethod + def instance(cls: Type[T]) -> T: + """Static access method.""" + if cls.__instance == None: + cls.__instance = cls() + + return cls.__instance + + @classmethod + def reset(cls: Type[T]) -> None: + cls.__instance = None + + def __init__(self) -> None: + if self.__instance != None: + raise Exception("This class is a singleton!") diff --git a/src/views/layout/header.py b/src/views/layout/header.py index 3643128..c2f4266 100644 --- a/src/views/layout/header.py +++ b/src/views/layout/header.py @@ -1,8 +1,9 @@ from PyQt6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QWidget +from src.views.modules.app_navigator.navigator import get_app_navigator +from src.views.modules.app_navigator.routes import AppNavigationRoutes from src.views.ui.button_group import ButtonGroup from src.views.ui.nav_button import NavigationButton -from views.modules.app_navigation import AppNavigationRoutes, app_navigation class Header(QWidget): @@ -16,12 +17,12 @@ def __init__(self): home_button = NavigationButton( text="Home", link=AppNavigationRoutes.MAIN, - navigator=app_navigation, + navigator=get_app_navigator(), ) delivery_button = NavigationButton( text="Delivery", link=AppNavigationRoutes.MANAGE_DELIVERY_MAIN, - navigator=app_navigation, + navigator=get_app_navigator(), ) button_group = ButtonGroup([home_button, delivery_button]) diff --git a/src/views/main_page/current_tour_page.py b/src/views/main_page/current_tour_page.py index 43aa626..96daad3 100644 --- a/src/views/main_page/current_tour_page.py +++ b/src/views/main_page/current_tour_page.py @@ -1,14 +1,28 @@ -from PyQt6.QtWidgets import QHBoxLayout, QLabel +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QLabel, QVBoxLayout from src.controllers.navigator.page import Page +from src.services.map.map_service import MapService +from src.views.modules.main_page_navigator.navigator import get_main_page_navigator +from src.views.modules.main_page_navigator.routes import MainPageNavigationRoutes +from src.views.ui.button import Button class CurrentTourPage(Page): def __init__(self): super().__init__() - layout = QHBoxLayout() + layout = QVBoxLayout() + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + exit_map_button = Button("Exit map") + exit_map_button.clicked.connect(self.exit_map) layout.addWidget(QLabel("CurrentTourPage works!")) + layout.addWidget(exit_map_button) self.setLayout(layout) + + def exit_map(self): + MapService.instance().clear_map() + get_main_page_navigator().replace(MainPageNavigationRoutes.LOAD_MAP) diff --git a/src/views/main_page/load_map_page.py b/src/views/main_page/load_map_page.py new file mode 100644 index 0000000..1b74b63 --- /dev/null +++ b/src/views/main_page/load_map_page.py @@ -0,0 +1,54 @@ +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QFileDialog, QLabel, QVBoxLayout + +from src.controllers.navigator.page import Page +from src.services.map import MapLoaderService +from src.views.modules.main_page_navigator.navigator import get_main_page_navigator +from src.views.modules.main_page_navigator.routes import MainPageNavigationRoutes +from src.views.ui.button import Button +from src.views.ui.button_group import ButtonGroup + +DEFAULT_BUTTONS = [ + ("small", "src/assets/smallMap.xml"), + ("medium", "src/assets/mediumMap.xml"), + ("large", "src/assets/largeMap.xml"), +] + + +class LoadMapPage(Page): + def __init__(self): + super().__init__() + + layout = QVBoxLayout() + + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + load_map_button = Button("Load map") + load_map_button.clicked.connect(self.ask_user_for_map) + + default_buttons = [] + + for name, path in DEFAULT_BUTTONS: + button = Button(name) + button.clicked.connect(lambda _, path=path: self.load_map(path)) + default_buttons.append(button) + + load_map_default_button_group = ButtonGroup(default_buttons) + + layout.addWidget(QLabel("Load from file:")) + layout.addWidget(load_map_button) + layout.addWidget(QLabel("Load from default maps:")) + layout.addWidget(load_map_default_button_group) + + self.setLayout(layout) + + def ask_user_for_map(self) -> None: + file_name, _ = QFileDialog.getOpenFileName( + self, "Choose map", "${HOME}", "XML files (*.xml)" + ) + if file_name: + self.load_map(file_name) + + def load_map(self, path: str) -> None: + MapLoaderService.instance().load_map_from_xml(path) + get_main_page_navigator().replace(MainPageNavigationRoutes.CURRENT_TOUR) diff --git a/src/views/main_page/main_page.py b/src/views/main_page/main_page.py index db2f4a2..88027f1 100644 --- a/src/views/main_page/main_page.py +++ b/src/views/main_page/main_page.py @@ -1,3 +1,5 @@ +from typing import List, Tuple + from PyQt6.QtCore import Qt from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import ( @@ -11,15 +13,11 @@ ) from src.controllers.navigator.page import Page -from src.models.temporary_map_loader import TemporaryMapLoader -from src.views.main_page.map_view import MapView -from src.views.modules.main_page_navigation import ( - MainPageNavigationRoutes, - main_page_navigation, -) +from src.views.modules.main_page_navigator.navigator import get_main_page_navigator from src.views.ui.button import Button from src.views.ui.button_group import ButtonGroup from src.views.utils.theme import Theme +from views.main_page.map.map_view import MapView class MainPage(Page): @@ -29,7 +27,7 @@ def __init__(self): layout = QHBoxLayout() layout.addLayout(self.__build_map_view()) - layout.addWidget(main_page_navigation.get_router_outlet()) + layout.addWidget(get_main_page_navigator().get_router_outlet()) self.setLayout(layout) @@ -37,14 +35,14 @@ def __build_map_view(self) -> QLayout: map_layout = QVBoxLayout() map_view = MapView() - # TODO: Remove this - map_view.set_map(TemporaryMapLoader().load_map()) - buttons_layout = QHBoxLayout() + buttons_layout.setAlignment(Qt.AlignmentFlag.AlignRight) - reset_map_button = Button("Reset zoom") - map_zoom_out_button = Button(icon="minus") - map_zoom_in_button = Button(icon="plus") + ( + reset_map_button, + map_zoom_out_button, + map_zoom_in_button, + ) = self.__build_map_action_buttons(map_view) map_zoom_buttons = ButtonGroup([map_zoom_out_button, map_zoom_in_button]) buttons_layout.addWidget(reset_map_button) @@ -53,8 +51,26 @@ def __build_map_view(self) -> QLayout: map_layout.addWidget(map_view) map_layout.addLayout(buttons_layout) + return map_layout + + def __build_map_action_buttons(self, map_view: MapView) -> Tuple[QWidget]: + reset_map_button = Button("Reset zoom") + map_zoom_out_button = Button(icon="minus") + map_zoom_in_button = Button(icon="plus") + reset_map_button.clicked.connect(map_view.fit_map) map_zoom_out_button.clicked.connect(map_view.zoom_out) map_zoom_in_button.clicked.connect(map_view.zoom_in) - return map_layout + map_view.ready.subscribe( + lambda ready: [ + button.setDisabled(not ready) + for button in [ + reset_map_button, + map_zoom_out_button, + map_zoom_in_button, + ] + ] + ) + + return reset_map_button, map_zoom_out_button, map_zoom_in_button diff --git a/src/views/main_page/map/map_marker.py b/src/views/main_page/map/map_marker.py new file mode 100644 index 0000000..a9fee73 --- /dev/null +++ b/src/views/main_page/map/map_marker.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + +from PyQt6.QtWidgets import QAbstractGraphicsShapeItem + +AlignBottom = bool + + +@dataclass +class MapMarker: + shape: QAbstractGraphicsShapeItem + align_bottom: AlignBottom + scale: float diff --git a/src/views/main_page/map_view.py b/src/views/main_page/map/map_view.py similarity index 64% rename from src/views/main_page/map_view.py rename to src/views/main_page/map/map_view.py index 9835f02..7e3eff4 100644 --- a/src/views/main_page/map_view.py +++ b/src/views/main_page/map/map_view.py @@ -1,6 +1,6 @@ -from typing import List, Optional, Tuple +from typing import List, Literal, Optional, Tuple -from PyQt6.QtCore import QPointF, Qt +from PyQt6.QtCore import QPointF, QRectF, Qt from PyQt6.QtGui import ( QBrush, QColor, @@ -18,14 +18,15 @@ QSizePolicy, QWidget, ) -from reactivex import Observable, Subject +from reactivex import Observable +from reactivex.subject import BehaviorSubject, Subject -from src.models.temporary_map_loader import Map, Position, Segment +from src.models.map import Map, Marker, Position, Segment +from src.services.map.map_service import MapService +from src.views.main_page.map.map_marker import AlignBottom, MapMarker from src.views.utils.icon import get_icon_pixmap from src.views.utils.theme import Theme -AlignBottom = bool - class MapView(QGraphicsView): """Widget to display a Map""" @@ -58,19 +59,25 @@ class MapView(QGraphicsView): __scene: Optional[QGraphicsScene] = None __map: Optional[Map] = None __scale_factor: int = 1 - __on_map_click: Subject[Position] = Subject() __segments: List[QAbstractGraphicsShapeItem] = [] - __markers: List[Tuple[QAbstractGraphicsShapeItem, AlignBottom]] = [] + __markers: List[MapMarker] = [] + __route_markers: List[MapMarker] = [] __marker_size: Optional[int] = None + __ready: BehaviorSubject[bool] = BehaviorSubject(False) def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.__set_config() + MapService.instance().map.subscribe( + lambda map: self.set_map(map) if map else self.reset() + ) + MapService.instance().markers().subscribe(self.__on_markers_change) + @property - def on_map_click(self) -> Observable[Position]: - """Subject that emit the position on the map when a user double clicks on it""" - return self.__on_map_click + def ready(self) -> Observable[bool]: + """Subject that emit a boolean when the map is ready to be used""" + return self.__ready def set_map(self, map: Map): """Set the map and initialize the view @@ -78,14 +85,21 @@ def set_map(self, map: Map): Arguments: map (Map): Map to display """ - self.__map = map - self.__scene = QGraphicsScene( - map.min_longitude, - map.min_latitude, - map.max_longitude - map.min_longitude, - map.max_latitude - map.min_latitude, + scene_rect = QRectF( + map.size.min.longitude, + map.size.min.latitude, + map.size.width, + map.size.height, ) + if self.__scene: + self.reset() + self.__scene.setSceneRect(scene_rect) + else: + self.__scene = QGraphicsScene(scene_rect) + + self.__map = map + self.__scene.setBackgroundBrush(QBrush(Qt.GlobalColor.white)) self.setScene(self.__scene) @@ -94,13 +108,24 @@ def set_map(self, map: Map): self.__marker_size = self.__scene.sceneRect().width() * self.MARKER_INITIAL_SIZE + self.add_marker( + position=map.warehouse, + icon="warehouse", + color=QColor("#1e8239"), + align_bottom=False, + scale=0.5, + ) + self.fit_map() + self.__ready.on_next(True) + def fit_map(self): """Adjust the view to fit the all map""" if self.__scene: self.fitInView(self.__scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio) self.__scale_factor = 1 + self.__scale_map(1) def add_marker( self, @@ -108,7 +133,8 @@ def add_marker( icon: QIcon | str = "map-marker-alt", color: QColor = QColor("#f54242"), align_bottom: AlignBottom = True, - ): + scale: float = 1, + ) -> MapMarker: """Add a marker on the map at a given position Args: @@ -117,24 +143,22 @@ def add_marker( color (QColor, optional): Color of the icon. Defaults to QColor("#f54242"). align_bottom (AlignBottom, optional): Whether the icon should be aligned at the bottom (ex: for map pin). Set to false if is a normal icon like a X. Defaults to True. """ + marker_size = self.__marker_size * scale + icon_pixmap = get_icon_pixmap(icon, self.MARKER_RESOLUTION_RESOLUTION, color) + icon_position = self.__get_marker_position(position, marker_size, align_bottom) icon_shape = self.__scene.addPixmap(icon_pixmap) - icon_shape.setPos( - QPointF( - # Longitude - half of the icon size (to center it) - position.longitude - self.__marker_size / 2, - # Latitude - icon size + 1% of the icon size (align it with the bottom of the icon which includes a little margin) - (position.latitude - self.__marker_size + (self.__marker_size * 0.01)) - if align_bottom - else (position.latitude - self.__marker_size / 2), - ) - ) - icon_shape.setScale(self.__marker_size / self.MARKER_RESOLUTION_RESOLUTION) + icon_shape.setPos(icon_position) + icon_shape.setScale(marker_size / self.MARKER_RESOLUTION_RESOLUTION) - self.__adjust_marker(icon_shape, align_bottom) + marker = MapMarker(icon_shape, align_bottom, scale) - self.__markers.append((icon_shape, align_bottom)) + self.__adjust_marker(marker) + + self.__markers.append(marker) + + return marker def zoom_in(self): """Zoom in the map""" @@ -144,6 +168,18 @@ def zoom_out(self): """Zoom out the map""" self.__scale_map(1 / self.DEFAULT_ZOOM_ACTION) + def reset(self): + """Reset the map to its initial state""" + self.__ready.on_next(False) + if self.__scene: + self.__scene.clear() + self.__segments = [] + self.__markers = [] + self.__route_markers = [] + self.__scale_factor = 1 + self.__marker_size = None + self.__map = None + def wheelEvent(self, event: QWheelEvent) -> None: """Method called when the user scrolls on the map @@ -152,7 +188,7 @@ def wheelEvent(self, event: QWheelEvent) -> None: if self.__scene and event.angleDelta().y() != 0: self.__scale_map( 1 - + (self.__map.get_size() * self.SCROLL_INTENSITY) + + (self.__map.size.area * self.SCROLL_INTENSITY) * event.angleDelta().y() ) @@ -161,12 +197,23 @@ def mouseDoubleClickEvent(self, event: QMouseEvent | None) -> None: Send the position of the click to the on_map_click subject """ + if not self.__scene: + return + position = self.mapToScene(event.pos()) position = Position(position.x(), position.y()) - self.add_marker(position) + MapService.instance().add_marker(Marker(position)) + + def __on_markers_change(self, markers: List[Marker]) -> None: + for marker in self.__route_markers: + self.__scene.removeItem(marker.shape) - self.__on_map_click.on_next(position) + self.__route_markers = [] + + for marker in markers: + map_marker = self.add_marker(marker.position) + self.__route_markers.append(map_marker) def __add_segment( self, segment: Segment, color: QColor = QColor("#9c9c9c") @@ -182,7 +229,12 @@ def __add_segment( segment.origin.latitude, segment.destination.longitude, segment.destination.latitude, - QPen(QBrush(color), self.__get_pen_size(), Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap), + QPen( + QBrush(color), + self.__get_pen_size(), + Qt.PenStyle.SolidLine, + Qt.PenCapStyle.RoundCap, + ), ) self.__segments.append(segmentLine) @@ -192,6 +244,9 @@ def __scale_map(self, factor: float): Args: factor (float): Scale factor """ + if not self.__scene: + return + updated_scale = self.__scale_factor * factor if updated_scale < 1: @@ -213,36 +268,50 @@ def __adjust_map_graphics(self) -> None: pen.setWidthF(self.__get_pen_size()) segment.setPen(pen) - for marker, align_bottom in self.__markers: - self.__adjust_marker(marker, align_bottom) + for marker in self.__markers: + self.__adjust_marker(marker) - def __adjust_marker( - self, marker: QAbstractGraphicsShapeItem, align_bottom: AlignBottom - ) -> None: + def __adjust_marker(self, marker: MapMarker) -> None: """Adjust a marker to the current map scale Args: marker (QAbstractGraphicsShapeItem): Marker to adjust align_bottom (AlignBottom): Whether the icon should be aligned at the bottom (ex: for map pin). Set to false if is a normal icon like a X. Defaults to True. """ - origin = marker.transformOriginPoint() + origin = marker.shape.transformOriginPoint() - translateX, translateY = ( - origin.x() + self.__marker_size / 2, - (origin.y() + self.__marker_size) - if align_bottom - else (origin.y() + self.__marker_size / 2), + marker_size = self.__marker_size * marker.scale + + translate = self.__get_marker_position( + origin, marker_size, marker.align_bottom, direction=-1 ) scale_factor = 1 / ( self.__scale_factor * self.MARKER_ZOOM_ADJUSTMENT + (1 - self.MARKER_ZOOM_ADJUSTMENT) ) - marker.setTransform( + marker.shape.setTransform( QTransform() - .translate(translateX, translateY) + .translate(translate.x(), translate.y()) .scale(scale_factor, scale_factor) - .translate(-translateX, -translateY) + .translate(-translate.x(), -translate.y()) + ) + + def __get_marker_position( + self, + position: QPointF | Position, + marker_size: float, + align_bottom: bool, + direction: Literal[1, -1] = 1, + ) -> QPointF: + x = position.x() if isinstance(position, QPointF) else position.x + y = position.y() if isinstance(position, QPointF) else position.y + + return QPointF( + x - (marker_size / 2 * direction), + (y - (marker_size - (marker_size * 0.01)) * direction) + if align_bottom + else (y - (marker_size / 2 * direction)), ) def __get_pen_size(self, scale: float = 1) -> float: diff --git a/src/views/manage_delivery_man_page/manage_delivery_man_page.py b/src/views/manage_delivery_man_page/manage_delivery_man_page.py index df2790e..9cf77fc 100644 --- a/src/views/manage_delivery_man_page/manage_delivery_man_page.py +++ b/src/views/manage_delivery_man_page/manage_delivery_man_page.py @@ -1,9 +1,11 @@ from PyQt6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget from src.controllers.navigator.page import Page -from src.views.modules.manage_delivery_man_navigation import ( +from src.views.modules.manage_delivery_man_navigator.navigator import ( + get_manage_delivery_man_navigator, +) +from src.views.modules.manage_delivery_man_navigator.routes import ( ManageDeliveryManNavigationRoutes, - manage_delivery_man_navigation, ) @@ -19,13 +21,15 @@ def __init__(self): for route_name in ManageDeliveryManNavigationRoutes: button = QPushButton(route_name.name) button.clicked.connect( - lambda _, name=route_name: manage_delivery_man_navigation.replace(name) + lambda _, name=route_name: get_manage_delivery_man_navigator().replace( + name + ) ) sub_layout.addWidget(button) sub_layout_widget.setLayout(sub_layout) layout.addWidget(sub_layout_widget) - layout.addWidget(manage_delivery_man_navigation.get_router_outlet()) + layout.addWidget(get_manage_delivery_man_navigator().get_router_outlet()) self.setLayout(layout) diff --git a/src/views/modules/app_navigation.py b/src/views/modules/app_navigation.py deleted file mode 100644 index 9293a07..0000000 --- a/src/views/modules/app_navigation.py +++ /dev/null @@ -1,24 +0,0 @@ -from enum import Enum - -from src.controllers.navigator import Navigator, Route -from src.views.main_page.main_page import MainPage -from src.views.manage_delivery_man_page.manage_delivery_man_page import ( - ManageDeliveryManPage, -) - - -class AppNavigationRoutes(Enum): - MAIN = "main" - MANAGE_DELIVERY_MAIN = "manage_delivery_main" - - -app_navigation = Navigator[AppNavigationRoutes]( - routes=[ - Route(name=AppNavigationRoutes.MAIN, widget=MainPage), - Route( - name=AppNavigationRoutes.MANAGE_DELIVERY_MAIN, - widget=ManageDeliveryManPage, - ), - ], - default_name=AppNavigationRoutes.MAIN, -) diff --git a/src/views/modules/app_navigator/config.py b/src/views/modules/app_navigator/config.py new file mode 100644 index 0000000..b693155 --- /dev/null +++ b/src/views/modules/app_navigator/config.py @@ -0,0 +1,20 @@ +from src.controllers.navigator.navigator import Route +from src.views.main_page.main_page import MainPage +from src.views.manage_delivery_man_page.manage_delivery_man_page import ( + ManageDeliveryManPage, +) +from src.views.modules.app_navigator.navigator import get_app_navigator +from src.views.modules.app_navigator.routes import AppNavigationRoutes + + +def init_app_navigator(): + get_app_navigator().init( + routes=[ + Route(name=AppNavigationRoutes.MAIN, widget=MainPage), + Route( + name=AppNavigationRoutes.MANAGE_DELIVERY_MAIN, + widget=ManageDeliveryManPage, + ), + ], + default_name=AppNavigationRoutes.MAIN, + ) diff --git a/src/views/modules/app_navigator/navigator.py b/src/views/modules/app_navigator/navigator.py new file mode 100644 index 0000000..df1f94e --- /dev/null +++ b/src/views/modules/app_navigator/navigator.py @@ -0,0 +1,7 @@ +from src.controllers.navigator.navigator import Navigator + +APP_NAVIGATOR = "app_navigator" + + +def get_app_navigator(): + return Navigator.get_navigator(APP_NAVIGATOR) diff --git a/src/views/modules/app_navigator/routes.py b/src/views/modules/app_navigator/routes.py new file mode 100644 index 0000000..b239b2e --- /dev/null +++ b/src/views/modules/app_navigator/routes.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class AppNavigationRoutes(Enum): + MAIN = "main" + MANAGE_DELIVERY_MAIN = "manage_delivery_main" diff --git a/src/views/modules/main_page_navigation.py b/src/views/modules/main_page_navigation.py deleted file mode 100644 index 6879736..0000000 --- a/src/views/modules/main_page_navigation.py +++ /dev/null @@ -1,43 +0,0 @@ -from enum import Enum - -from src.controllers.navigator import Navigator, Route -from src.views.main_page.add_delivery_address_page import AddDeliveryAddressPage -from src.views.main_page.add_delivery_time_window_page import AddDeliveryTimeWindowPage -from src.views.main_page.confirm_delivery_address_page import ConfirmDeliveryAddressPage -from src.views.main_page.current_tour_page import CurrentTourPage -from src.views.main_page.select_delivery_man_page import SelectDeliveryManPage - - -class MainPageNavigationRoutes(Enum): - CURRENT_TOUR = "current_tour" - ADD_DELIVERY_ADDRESS = "add_delivery_address" - CONFIRM_DELIVERY_ADDRESS = "confirm_delivery_address" - ADD_DELIVERY_TIME_WINDOW = "add_delivery_time_window" - SELECT_DELIVERY_MAN_PAGE = "select" - - -main_page_navigation = Navigator[MainPageNavigationRoutes]( - routes=[ - Route( - name=MainPageNavigationRoutes.CURRENT_TOUR, - widget=CurrentTourPage, - ), - Route( - name=MainPageNavigationRoutes.ADD_DELIVERY_ADDRESS, - widget=AddDeliveryAddressPage, - ), - Route( - name=MainPageNavigationRoutes.CONFIRM_DELIVERY_ADDRESS, - widget=ConfirmDeliveryAddressPage, - ), - Route( - name=MainPageNavigationRoutes.ADD_DELIVERY_TIME_WINDOW, - widget=AddDeliveryTimeWindowPage, - ), - Route( - name=MainPageNavigationRoutes.SELECT_DELIVERY_MAN_PAGE, - widget=SelectDeliveryManPage, - ), - ], - default_name=MainPageNavigationRoutes.CURRENT_TOUR, -) diff --git a/src/views/modules/main_page_navigator/config.py b/src/views/modules/main_page_navigator/config.py new file mode 100644 index 0000000..b12771b --- /dev/null +++ b/src/views/modules/main_page_navigator/config.py @@ -0,0 +1,41 @@ +from src.controllers.navigator import Route +from src.views.main_page.add_delivery_address_page import AddDeliveryAddressPage +from src.views.main_page.add_delivery_time_window_page import AddDeliveryTimeWindowPage +from src.views.main_page.confirm_delivery_address_page import ConfirmDeliveryAddressPage +from src.views.main_page.current_tour_page import CurrentTourPage +from src.views.main_page.load_map_page import LoadMapPage +from src.views.main_page.select_delivery_man_page import SelectDeliveryManPage +from src.views.modules.main_page_navigator.navigator import get_main_page_navigator +from src.views.modules.main_page_navigator.routes import MainPageNavigationRoutes + + +def init_main_page_navigator(): + get_main_page_navigator().init( + routes=[ + Route( + name=MainPageNavigationRoutes.LOAD_MAP, + widget=LoadMapPage, + ), + Route( + name=MainPageNavigationRoutes.CURRENT_TOUR, + widget=CurrentTourPage, + ), + Route( + name=MainPageNavigationRoutes.ADD_DELIVERY_ADDRESS, + widget=AddDeliveryAddressPage, + ), + Route( + name=MainPageNavigationRoutes.CONFIRM_DELIVERY_ADDRESS, + widget=ConfirmDeliveryAddressPage, + ), + Route( + name=MainPageNavigationRoutes.ADD_DELIVERY_TIME_WINDOW, + widget=AddDeliveryTimeWindowPage, + ), + Route( + name=MainPageNavigationRoutes.SELECT_DELIVERY_MAN_PAGE, + widget=SelectDeliveryManPage, + ), + ], + default_name=MainPageNavigationRoutes.LOAD_MAP, + ) diff --git a/src/views/modules/main_page_navigator/navigator.py b/src/views/modules/main_page_navigator/navigator.py new file mode 100644 index 0000000..7cb9f81 --- /dev/null +++ b/src/views/modules/main_page_navigator/navigator.py @@ -0,0 +1,7 @@ +from src.controllers.navigator.navigator import Navigator + +MAIN_PAGE_NAVIGATOR = "main_page_navigator" + + +def get_main_page_navigator(): + return Navigator.get_navigator(MAIN_PAGE_NAVIGATOR) diff --git a/src/views/modules/main_page_navigator/routes.py b/src/views/modules/main_page_navigator/routes.py new file mode 100644 index 0000000..5d8e42f --- /dev/null +++ b/src/views/modules/main_page_navigator/routes.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class MainPageNavigationRoutes(Enum): + LOAD_MAP = "load_map" + CURRENT_TOUR = "current_tour" + ADD_DELIVERY_ADDRESS = "add_delivery_address" + CONFIRM_DELIVERY_ADDRESS = "confirm_delivery_address" + ADD_DELIVERY_TIME_WINDOW = "add_delivery_time_window" + SELECT_DELIVERY_MAN_PAGE = "select" diff --git a/src/views/modules/manage_delivery_man_navigation.py b/src/views/modules/manage_delivery_man_navigation.py deleted file mode 100644 index 0a1c810..0000000 --- a/src/views/modules/manage_delivery_man_navigation.py +++ /dev/null @@ -1,40 +0,0 @@ -from enum import Enum - -from src.controllers.navigator import Navigator, Route -from src.views.manage_delivery_man_page.add_delivery_man_form_view import ( - AddDeliveryManFormView, -) -from src.views.manage_delivery_man_page.delete_delivery_man_form_view import ( - DeleteDeliveryManFormView, -) -from src.views.manage_delivery_man_page.menu_view import MenuView -from src.views.manage_delivery_man_page.modify_delivery_man_form_view import ( - ModifyDeliveryManFormView, -) - - -class ManageDeliveryManNavigationRoutes(Enum): - MENU = "menu" - ADD_DELIVERY_MAN_FORM = "add_delivery_man_form" - MODIFY_DELIVERY_MAN_FORM = "modify_delivery_man_form" - DELETE_DELIVERY_MAN_FORM = "delete_delivery_man_form" - - -manage_delivery_man_navigation = Navigator[ManageDeliveryManNavigationRoutes]( - routes=[ - Route(name=ManageDeliveryManNavigationRoutes.MENU, widget=MenuView), - Route( - name=ManageDeliveryManNavigationRoutes.ADD_DELIVERY_MAN_FORM, - widget=AddDeliveryManFormView, - ), - Route( - name=ManageDeliveryManNavigationRoutes.MODIFY_DELIVERY_MAN_FORM, - widget=ModifyDeliveryManFormView, - ), - Route( - name=ManageDeliveryManNavigationRoutes.DELETE_DELIVERY_MAN_FORM, - widget=DeleteDeliveryManFormView, - ), - ], - default_name=ManageDeliveryManNavigationRoutes.MENU, -) diff --git a/src/views/modules/manage_delivery_man_navigator/config.py b/src/views/modules/manage_delivery_man_navigator/config.py new file mode 100644 index 0000000..236bc9b --- /dev/null +++ b/src/views/modules/manage_delivery_man_navigator/config.py @@ -0,0 +1,38 @@ +from src.controllers.navigator import Route +from src.views.manage_delivery_man_page.add_delivery_man_form_view import ( + AddDeliveryManFormView, +) +from src.views.manage_delivery_man_page.delete_delivery_man_form_view import ( + DeleteDeliveryManFormView, +) +from src.views.manage_delivery_man_page.menu_view import MenuView +from src.views.manage_delivery_man_page.modify_delivery_man_form_view import ( + ModifyDeliveryManFormView, +) +from src.views.modules.manage_delivery_man_navigator.navigator import ( + get_manage_delivery_man_navigator, +) +from src.views.modules.manage_delivery_man_navigator.routes import ( + ManageDeliveryManNavigationRoutes, +) + + +def init_manage_delivery_man_navigator(): + get_manage_delivery_man_navigator().init( + routes=[ + Route(name=ManageDeliveryManNavigationRoutes.MENU, widget=MenuView), + Route( + name=ManageDeliveryManNavigationRoutes.ADD_DELIVERY_MAN_FORM, + widget=AddDeliveryManFormView, + ), + Route( + name=ManageDeliveryManNavigationRoutes.MODIFY_DELIVERY_MAN_FORM, + widget=ModifyDeliveryManFormView, + ), + Route( + name=ManageDeliveryManNavigationRoutes.DELETE_DELIVERY_MAN_FORM, + widget=DeleteDeliveryManFormView, + ), + ], + default_name=ManageDeliveryManNavigationRoutes.MENU, + ) diff --git a/src/views/modules/manage_delivery_man_navigator/navigator.py b/src/views/modules/manage_delivery_man_navigator/navigator.py new file mode 100644 index 0000000..b17cab8 --- /dev/null +++ b/src/views/modules/manage_delivery_man_navigator/navigator.py @@ -0,0 +1,7 @@ +from src.controllers.navigator.navigator import Navigator + +MANAGE_DELIVERY_MAN_NAVIGATOR = "manage_delivery_man_navigator" + + +def get_manage_delivery_man_navigator(): + return Navigator.get_navigator(MANAGE_DELIVERY_MAN_NAVIGATOR) diff --git a/src/views/modules/manage_delivery_man_navigator/routes.py b/src/views/modules/manage_delivery_man_navigator/routes.py new file mode 100644 index 0000000..e6dd9e5 --- /dev/null +++ b/src/views/modules/manage_delivery_man_navigator/routes.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class ManageDeliveryManNavigationRoutes(Enum): + MENU = "menu" + ADD_DELIVERY_MAN_FORM = "add_delivery_man_form" + MODIFY_DELIVERY_MAN_FORM = "modify_delivery_man_form" + DELETE_DELIVERY_MAN_FORM = "delete_delivery_man_form" diff --git a/src/views/modules/navigators.py b/src/views/modules/navigators.py new file mode 100644 index 0000000..1836113 --- /dev/null +++ b/src/views/modules/navigators.py @@ -0,0 +1,11 @@ +from src.views.modules.app_navigator.config import init_app_navigator +from src.views.modules.main_page_navigator.config import init_main_page_navigator +from src.views.modules.manage_delivery_man_navigator.config import ( + init_manage_delivery_man_navigator, +) + + +def init_navigators(): + init_app_navigator() + init_main_page_navigator() + init_manage_delivery_man_navigator() diff --git a/src/views/ui/button.py b/src/views/ui/button.py index 360fe97..ae2313d 100644 --- a/src/views/ui/button.py +++ b/src/views/ui/button.py @@ -4,7 +4,7 @@ import qtawesome as qta from PyQt6.QtCore import Qt from PyQt6.QtGui import QCursor, QIcon -from PyQt6.QtWidgets import QPushButton +from PyQt6.QtWidgets import QPushButton, QWidget from src.views.utils.theme import Color, Theme @@ -20,18 +20,26 @@ def __init__( text: Optional[str] = None, icon: Optional[QIcon | str] = None, corners: ButtonCorners = ButtonCorners.ALL, + parent: Optional[QWidget] = None, ): if icon: super().__init__( - qta.icon(f"fa5s.{icon}") if isinstance(icon, str) else icon, text + icon=qta.icon(f"fa5s.{icon}") if isinstance(icon, str) else icon, + text=text, + parent=parent, ) else: - super().__init__(text) + super().__init__(text, parent) self.__corners = corners self._update_style() + def setEnabled(self, enabled: bool) -> None: + self.__disabled = not enabled + self._update_style() + super().setDisabled(not enabled) + def setDisabled(self, disabled: bool) -> None: self.__disabled = disabled self._update_style() @@ -46,13 +54,13 @@ def _update_style(self) -> None: f""" QPushButton {{ background-color: {self.__get_background_color()}; - color: {Color.PRIMARY_CONTRAST.value}; + color: {self.__get_color()}; padding: 8px 16px; font-weight: 500; {self.__get_corners()} }} QPushButton:pressed {{ - background-color: {Color.PRIMARY_DISABLED.value}; + background-color: {"#80" + Color.PRIMARY.value[1:]}; }} """ ) @@ -60,10 +68,16 @@ def _update_style(self) -> None: def __get_background_color(self) -> str: if self.__disabled: - return Color.PRIMARY_DISABLED.value + return "#80" + Color.PRIMARY.value[1:] else: return Color.PRIMARY.value + def __get_color(self) -> str: + if self.__disabled: + return "#80" + Color.PRIMARY_CONTRAST.value[1:] + else: + return Color.PRIMARY_CONTRAST.value + def __get_corners(self) -> str: top_right, top_left, bottom_right, bottom_left = "0px", "0px", "0px", "0px" diff --git a/src/views/ui/button_group.py b/src/views/ui/button_group.py index 4b6895f..413edc9 100644 --- a/src/views/ui/button_group.py +++ b/src/views/ui/button_group.py @@ -13,6 +13,7 @@ def __init__(self, buttons: List[Button]) -> None: layout = QHBoxLayout() layout.setSpacing(1) + layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) diff --git a/src/views/utils/theme.py b/src/views/utils/theme.py index 11acfb2..1c64175 100644 --- a/src/views/utils/theme.py +++ b/src/views/utils/theme.py @@ -7,7 +7,6 @@ class Color(Enum): PRIMARY = "#3875ff" PRIMARY_CONTRAST = "#ffffff" - PRIMARY_DISABLED = "#566ea3" BACKGROUND = "#2e3440" BACKGROUND_CONTRAST = "#ffffff" diff --git a/src/views/window.py b/src/views/window.py index bbbc39b..6930e9a 100644 --- a/src/views/window.py +++ b/src/views/window.py @@ -1,8 +1,8 @@ -from PyQt6.QtWidgets import QGridLayout, QMainWindow, QPushButton, QVBoxLayout, QWidget +from PyQt6.QtWidgets import QGridLayout, QMainWindow, QWidget from src.views.layout import Header +from src.views.modules.app_navigator.navigator import get_app_navigator from src.views.utils.theme import Color, Theme -from views.modules.app_navigation import app_navigation class MainWindow(QMainWindow): @@ -25,7 +25,7 @@ def build_central_widget(self) -> QWidget: layout.setContentsMargins(0, 0, 0, 0) header = Header() - router_outlet = app_navigation.get_router_outlet() + router_outlet = get_app_navigator().get_router_outlet() widget.setLayout(layout) layout.addWidget(header, 0, 0)