|
|
@ -1,7 +1,8 @@ |
|
|
|
#!/usr/bin/env python3 |
|
|
|
|
|
|
|
import math |
|
|
|
import pygame |
|
|
|
import json |
|
|
|
from typing import Self |
|
|
|
from typing import Tuple |
|
|
|
|
|
|
|
|
|
|
|
class Position: |
|
|
@ -9,11 +10,31 @@ class Position: |
|
|
|
self.x = x |
|
|
|
self.y = y |
|
|
|
|
|
|
|
def as_tuple(self): |
|
|
|
def __add__(self, position: Self) -> Self: |
|
|
|
return Position(self.x + position.x, self.y + position.y) |
|
|
|
|
|
|
|
def __sub__(self, position: Self) -> Self: |
|
|
|
return Position(self.x - position.x, self.y - position.y) |
|
|
|
|
|
|
|
def __mul__(self, position: Self) -> Self: |
|
|
|
return Position(self.x * position.x, self.y * position.y) |
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
return 'Position(%d, %d)' % (self.x, self.y) |
|
|
|
|
|
|
|
def as_tuple(self) -> Tuple: |
|
|
|
return (self.x, self.y) |
|
|
|
|
|
|
|
def get_distance(self, position: Self) -> Self: |
|
|
|
delta = position - self |
|
|
|
|
|
|
|
return math.sqrt(math.pow(delta.x, 2) + math.pow(delta.y, 2)) |
|
|
|
|
|
|
|
def get_duplicate(self) -> Self: |
|
|
|
return Position(self.x, self.y) |
|
|
|
|
|
|
|
@staticmethod |
|
|
|
def from_tuple(tuple): |
|
|
|
def from_tuple(tuple) -> Self: |
|
|
|
return Position(tuple[0], tuple[1]) |
|
|
|
|
|
|
|
|
|
|
@ -23,7 +44,7 @@ class Color: |
|
|
|
self.green = green |
|
|
|
self.blue = blue |
|
|
|
|
|
|
|
def as_tuple(self): |
|
|
|
def as_tuple(self) -> Tuple: |
|
|
|
return (self.red, self.green, self.blue) |
|
|
|
|
|
|
|
|
|
|
@ -40,23 +61,12 @@ class Dimensions: |
|
|
|
return Dimensions(tuple[0], tuple[1]) |
|
|
|
|
|
|
|
|
|
|
|
class Image: |
|
|
|
def __init__(self, path = None): |
|
|
|
self.surface = None |
|
|
|
|
|
|
|
if path is not None: |
|
|
|
self.surface = pygame.image.load(path) |
|
|
|
|
|
|
|
def get_dimensions(self) -> Dimensions: |
|
|
|
return Dimensions.from_tuple(self.surface.get_size()) |
|
|
|
|
|
|
|
|
|
|
|
class Canvas: |
|
|
|
def __init__(self, size: Dimensions): |
|
|
|
self.surface = pygame.Surface(size.as_tuple(), pygame.SRCALPHA) |
|
|
|
class Drawable: |
|
|
|
def __init__(self, dimensions = Dimensions(0, 0)): |
|
|
|
self.surface = pygame.Surface(dimensions.as_tuple(), pygame.SRCALPHA) |
|
|
|
|
|
|
|
def draw_image(self, image: Image, position: Position): |
|
|
|
self.surface.blit(image.surface, position.as_tuple()) |
|
|
|
def draw_inside(self, drawable: Self, position: Position): |
|
|
|
self.surface.blit(drawable.surface, position.as_tuple()) |
|
|
|
|
|
|
|
def clear(self, color = Color(0, 0, 0)): |
|
|
|
self.surface.fill(color.as_tuple()) |
|
|
@ -67,6 +77,23 @@ class Canvas: |
|
|
|
|
|
|
|
return image |
|
|
|
|
|
|
|
def get_dimensions(self) -> Dimensions: |
|
|
|
return Dimensions.from_tuple(self.surface.get_size()) |
|
|
|
|
|
|
|
|
|
|
|
class Image(Drawable): |
|
|
|
def __init__(self, path = None): |
|
|
|
Drawable.__init__(self) |
|
|
|
|
|
|
|
if path is not None: |
|
|
|
self.surface = pygame.image.load(path) |
|
|
|
|
|
|
|
|
|
|
|
class Canvas(Drawable): |
|
|
|
def __init__(self, size: Dimensions): |
|
|
|
Drawable.__init__(self, size) |
|
|
|
|
|
|
|
|
|
|
|
class Screen: |
|
|
|
def __init__(self, title: str, resolution: Dimensions): |
|
|
|
self.resolution = resolution |
|
|
@ -82,7 +109,7 @@ class Screen: |
|
|
|
def get_image(self): |
|
|
|
image = Image() |
|
|
|
|
|
|
|
def update(self): |
|
|
|
def update(self) -> None: |
|
|
|
pygame.dispay.flip() |
|
|
|
|
|
|
|
@staticmethod |
|
|
@ -99,11 +126,12 @@ class Visualization(): |
|
|
|
def __init__(self, surface: Canvas, position: Position): |
|
|
|
self.surface = surface |
|
|
|
self.position = position |
|
|
|
self.on_hover = lambda : None |
|
|
|
|
|
|
|
def draw(self, canvas: Canvas): |
|
|
|
canvas.draw_image(self.surface, self.position) |
|
|
|
def draw(self, canvas: Canvas) -> None: |
|
|
|
canvas.draw_inside(self.surface, self.position) |
|
|
|
|
|
|
|
def is_inside(self, position: Position): |
|
|
|
def is_inside(self, position: Position) -> bool: |
|
|
|
if position.x < self.position.x: |
|
|
|
return False |
|
|
|
|
|
|
@ -122,45 +150,79 @@ class Visualization(): |
|
|
|
|
|
|
|
|
|
|
|
class Clickable(Visualization): |
|
|
|
def __init__(self, surface: Canvas, position: Position, on_click_callback: callable): |
|
|
|
Visualization.__init__(self, surface) |
|
|
|
self.on_click = on_click_callback |
|
|
|
def __init__(self, surface: Canvas, position: Position): |
|
|
|
Visualization.__init__(self, surface, position) |
|
|
|
self.on_click = lambda : None |
|
|
|
|
|
|
|
def click(self): |
|
|
|
def click(self) -> None: |
|
|
|
self.on_click() |
|
|
|
|
|
|
|
|
|
|
|
class Dragable(Clickable): |
|
|
|
def __init__(self, canvas: Canvas, position: Position): |
|
|
|
Clickable.__init__(self, canvas, position) |
|
|
|
self.on_drag = lambda : None |
|
|
|
self.on_drop = lambda : None |
|
|
|
self.on_fail = lambda drag : None |
|
|
|
|
|
|
|
def drag(self) -> None: |
|
|
|
self.on_drag() |
|
|
|
|
|
|
|
def drop(self) -> None: |
|
|
|
self.on_drop() |
|
|
|
|
|
|
|
def fail(self, drag: 'Drag'): |
|
|
|
self.on_fail(drag) |
|
|
|
|
|
|
|
|
|
|
|
class Dropable(Visualization): |
|
|
|
def __init__(self, canvas: Canvas, position: Position): |
|
|
|
Visualization.__init__(self, canvas, position) |
|
|
|
self.on_drop = lambda dragable: None |
|
|
|
|
|
|
|
def drop(self, dragable: Dragable) -> None: |
|
|
|
self.on_drop(dragable) |
|
|
|
|
|
|
|
|
|
|
|
class Container(Visualization): |
|
|
|
ROWS = 0 |
|
|
|
COLUMNS = 1 |
|
|
|
|
|
|
|
def __init__(self, position: Position, dimensions: Dimensions): |
|
|
|
Visualization.__init__(self, Canvas(dimensions), position) |
|
|
|
|
|
|
|
self.background_image = None |
|
|
|
self.background_color = Color(0, 0, 0) |
|
|
|
self.visualizations = [] |
|
|
|
self.clickables = [] |
|
|
|
|
|
|
|
def set_background_image(self, image: Image): |
|
|
|
def set_background_image(self, image: Image) -> None: |
|
|
|
self.background_image = image |
|
|
|
|
|
|
|
def render(self): |
|
|
|
def render(self) -> None: |
|
|
|
self.surface.clear(self.background_color) |
|
|
|
|
|
|
|
if self.background_image is not None: |
|
|
|
self.surface.draw_image(self.background_image, Dimensions(0, 0)) |
|
|
|
self.surface.draw_inside(self.background_image, Dimensions(0, 0)) |
|
|
|
|
|
|
|
for visualization in self.visualizations: |
|
|
|
visualization.draw(self.surface) |
|
|
|
|
|
|
|
def add_visualization(self, visualization: Visualization): |
|
|
|
def add_visualization(self, visualization: Visualization) -> None: |
|
|
|
self.visualizations.append(visualization) |
|
|
|
|
|
|
|
def add_clickable(self, clickable: Clickable): |
|
|
|
def add_clickable(self, clickable: Clickable) -> None: |
|
|
|
self.add_visualization(clickable) |
|
|
|
self.clickables.append(clickable) |
|
|
|
|
|
|
|
|
|
|
|
class Drag: |
|
|
|
def __init__(self, dragable: Dragable, cursor_offset: Position): |
|
|
|
self.dragable = dragable |
|
|
|
self.origin = dragable.position.get_duplicate() |
|
|
|
self.cursor_offset = cursor_offset |
|
|
|
|
|
|
|
|
|
|
|
class UI: |
|
|
|
def __init__(self, title: str, resolution: Dimensions): |
|
|
|
pygame.init() |
|
|
@ -169,7 +231,7 @@ class UI: |
|
|
|
self.clock = pygame.time.Clock() |
|
|
|
self.screen = Screen(title, resolution) |
|
|
|
self.canvas = self.screen.get_canvas() |
|
|
|
|
|
|
|
self.drag = None |
|
|
|
self.containers = [] |
|
|
|
|
|
|
|
def init(self): |
|
|
@ -182,30 +244,123 @@ class UI: |
|
|
|
container.render() |
|
|
|
container.draw(self.canvas) |
|
|
|
|
|
|
|
pygame.display.update() |
|
|
|
|
|
|
|
def add_container(self, container: Container): |
|
|
|
self.containers.append(container) |
|
|
|
|
|
|
|
def exit(self): |
|
|
|
pygame.quit() |
|
|
|
self.is_running = False |
|
|
|
|
|
|
|
def main(self): |
|
|
|
while self.is_running: |
|
|
|
self.clock.tick() |
|
|
|
event = pygame.event.wait() |
|
|
|
|
|
|
|
if event.type == pygame.QUIT: |
|
|
|
self.exit() |
|
|
|
|
|
|
|
for event in pygame.event.get(): |
|
|
|
if event.type == pygame.QUIT: |
|
|
|
elif event.type == pygame.KEYDOWN: |
|
|
|
if event.key == pygame.K_ESCAPE: |
|
|
|
self.exit() |
|
|
|
|
|
|
|
elif event.type == pygame.KEYDOWN: |
|
|
|
if event.key == pygame.K_ESCAPE: |
|
|
|
self.exit() |
|
|
|
elif event.type == pygame.MOUSEBUTTONDOWN: |
|
|
|
self._handle_click(event) |
|
|
|
|
|
|
|
elif event.type == pygame.MOUSEBUTTONUP: |
|
|
|
self._handle_drop(event) |
|
|
|
|
|
|
|
elif event.type == pygame.MOUSEMOTION: |
|
|
|
self._handle_mouse_motion(event) |
|
|
|
|
|
|
|
self.update() |
|
|
|
|
|
|
|
pygame.quit() |
|
|
|
|
|
|
|
def _handle_click(self, event: pygame.event.Event) -> None: |
|
|
|
clickables_clicked = self._get_clickables_clicked(event) |
|
|
|
|
|
|
|
if len(clickables_clicked) == 0: |
|
|
|
return |
|
|
|
|
|
|
|
clickable = clickables_clicked[-1] |
|
|
|
|
|
|
|
if type(clickable) is Dragable: |
|
|
|
self.drag = Drag( |
|
|
|
clickable, |
|
|
|
Position.from_tuple(event.pos) - clickable.position |
|
|
|
) |
|
|
|
|
|
|
|
clickable.click() |
|
|
|
|
|
|
|
pygame.display.update() |
|
|
|
def _handle_drag(self, event: pygame.event.Event): |
|
|
|
position_cursor = Position.from_tuple(event.pos) |
|
|
|
|
|
|
|
position = position_cursor - self.drag.cursor_offset |
|
|
|
|
|
|
|
self.drag.dragable.position = position |
|
|
|
self.drag.dragable.drag() |
|
|
|
|
|
|
|
def _handle_drop(self, event: pygame.event.Event): |
|
|
|
if self.drag is None: |
|
|
|
return |
|
|
|
|
|
|
|
position_cursor = Position.from_tuple(event.pos) |
|
|
|
|
|
|
|
dropables_hovered = self._get_visualizations_hovered(position_cursor, [Dropable]) |
|
|
|
|
|
|
|
if len(dropables_hovered) == 0: |
|
|
|
self.drag.dragable.fail(self.drag) |
|
|
|
self.drag.dragable.position = self.drag.origin |
|
|
|
else: |
|
|
|
dropables_hovered[-1].drop(self.drag.dragable) |
|
|
|
self.drag.dragable.drop(dropables_hovered[-1]) |
|
|
|
|
|
|
|
self.drag = None |
|
|
|
|
|
|
|
def _handle_mouse_motion(self, event: pygame.event.Event): |
|
|
|
if type(self.drag) is Drag: |
|
|
|
self._handle_drag(event) |
|
|
|
|
|
|
|
def _get_visualizations_hovered(self, cursor_position: Position, filter: list = []): |
|
|
|
containers_hovered = [] |
|
|
|
visualizations_hovered = [] |
|
|
|
|
|
|
|
for container in self.containers: |
|
|
|
if not container.is_inside(cursor_position): |
|
|
|
continue |
|
|
|
|
|
|
|
containers_hovered.append(container) |
|
|
|
|
|
|
|
for container_hovered in containers_hovered: |
|
|
|
for visualization in container_hovered.visualizations: |
|
|
|
if not self._is_in_filter(visualization, filter): |
|
|
|
continue |
|
|
|
|
|
|
|
if visualization.is_inside(cursor_position): |
|
|
|
visualizations_hovered.append(visualization) |
|
|
|
|
|
|
|
return visualizations_hovered |
|
|
|
|
|
|
|
def _get_clickables_clicked(self, event: pygame.event.Event): |
|
|
|
return self._get_visualizations_hovered( |
|
|
|
Position.from_tuple(event.pos), |
|
|
|
[Clickable, Dragable] |
|
|
|
) |
|
|
|
|
|
|
|
def _is_in_filter(self, visualization: Visualization, filter: list = []) -> bool: |
|
|
|
if len(filter) == 0: |
|
|
|
return True |
|
|
|
|
|
|
|
for f in filter: |
|
|
|
for parent in visualization.__class__.mro(): |
|
|
|
if parent is f: |
|
|
|
return True |
|
|
|
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
|
|
class IconButton(Clickable): |
|
|
|
def __init__(self, icon: pygame.Surface, icon_hover = None): |
|
|
|
def __init__(self, icon: Image, icon_hover = None): |
|
|
|
self.icon = icon |
|
|
|
self.icon_hover = icon_hover |
|
|
|
|
|
|
@ -218,7 +373,13 @@ class ResourceContainer: |
|
|
|
self.images[name] = Image(url) |
|
|
|
|
|
|
|
def get_image(self, name): |
|
|
|
return self.images[name] |
|
|
|
return self._get_resource(self.images, name) |
|
|
|
|
|
|
|
def _get_resource(self, resources: list, name: str): |
|
|
|
try: |
|
|
|
return resources[name] |
|
|
|
except KeyError: |
|
|
|
raise Exception('Resource "%s" not found!' % (name)) |
|
|
|
|
|
|
|
@staticmethod |
|
|
|
def from_json_file(json_file): |
|
|
|