From ea3ddb1ed22dd5e468eb32d82d2f1e5b4f2f2370 Mon Sep 17 00:00:00 2001 From: SardorSharipov <56306755+SardorSharipov@users.noreply.github.com> Date: Tue, 28 May 2024 13:31:14 +0300 Subject: [PATCH] View pivot table (#56) --- .github/workflows/ci.yml | 2 +- .gitignore | 2 + src/internal/controller/impl/controller.py | 27 ++ src/internal/controller/interfaces.py | 13 + src/internal/objects/impl/card.py | 14 + src/internal/objects/impl/test_card.py | 7 +- src/internal/objects/interfaces.py | 10 + src/internal/objects/test_builder.py | 1 + .../repositories/impl/test_repository.py | 1 + src/internal/view/modules/card/card_view.py | 53 +++ .../view/modules/pivot_table/__init__.py | 21 ++ .../modules/pivot_table/pivot_table_view.py | 315 ++++++++++++++++++ .../modules/pivot_table/states/__init__.py | 0 .../pivot_table/states/add_attribute_state.py | 71 ++++ .../pivot_table/states/show_axis_state.py | 160 +++++++++ .../view/modules/pivot_table/toplevel.py | 35 ++ .../view/state_machine/impl/state_machine.py | 4 + src/internal/view/view.py | 1 + 18 files changed, 735 insertions(+), 2 deletions(-) create mode 100644 src/internal/view/modules/pivot_table/__init__.py create mode 100644 src/internal/view/modules/pivot_table/pivot_table_view.py create mode 100644 src/internal/view/modules/pivot_table/states/__init__.py create mode 100644 src/internal/view/modules/pivot_table/states/add_attribute_state.py create mode 100644 src/internal/view/modules/pivot_table/states/show_axis_state.py create mode 100644 src/internal/view/modules/pivot_table/toplevel.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f22175a..b1eceee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: pip install -r requirements.txt - name: Test with pytest run: | - pip install pytest pytest pytest-asyncio + pip install pytest pytest-asyncio pytest ./src - name: Lint with Ruff run: | diff --git a/.gitignore b/.gitignore index 4f5953c..cd407fc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ boards.txt server/mongo_data .vscode venv +boards.txt +======= .coverage diff --git a/src/internal/controller/impl/controller.py b/src/internal/controller/impl/controller.py index bd03398..8c28e8c 100644 --- a/src/internal/controller/impl/controller.py +++ b/src/internal/controller/impl/controller.py @@ -371,6 +371,33 @@ def edit_linked_objects( action.do() self._undo_redo_manager.store_action(action) + def add_attribute( + self, attr_name: str, value: str + ): + for obj in self._repo.get_all(): + if isinstance(obj, internal.objects.interfaces.IBoardObjectCard): + attribute = obj.attribute + attribute[attr_name] = value + obj.attribute = attribute.copy() + logging.debug('added new attribute=%s to all cards with value=%s', attr_name, value) + + def edit_attribute( + self, obj_id: internal.objects.interfaces.ObjectId, attr_name: str, value: str + ): + obj: typing.Optional[internal.objects.interfaces.IBoardObjectCard] = self._repo.get(obj_id) + if obj: + + attributes = obj.attribute + attributes[attr_name] = value + obj.attribute = attributes.copy() + logging.debug( + 'editing attribute of an object old value=%s with new value=%s', + obj.attribute[attr_name], value + ) + self._on_feature_finish() + return + logging.debug('no object id=%s found to edit with attribute=%s', obj_id, attr_name) + def undo_last_action(self): logging.debug('controller was asked to undo last action') self._undo_redo_manager.undo() diff --git a/src/internal/controller/interfaces.py b/src/internal/controller/interfaces.py index 7f0b9a6..fb8d827 100644 --- a/src/internal/controller/interfaces.py +++ b/src/internal/controller/interfaces.py @@ -77,14 +77,27 @@ def edit_connector_type( def edit_stroke_style(self, obj_id: internal.objects.interfaces.ObjectId, stroke_style: str): pass + @abc.abstractmethod def edit_table(self, obj_id: internal.objects.interfaces.ObjectId, list_col, list_row): pass + @abc.abstractmethod def edit_linked_objects( self, obj_id: internal.objects.interfaces.ObjectId, linked_obj: typing.Dict[str, list[int]] ): pass + @abc.abstractmethod + def add_attribute(self, attr_name: str, value: str): + pass + + @abc.abstractmethod + def edit_attribute( + self, obj_id: internal.objects.interfaces.ObjectId, attr_name: str, value: str + ): + pass + + @abc.abstractmethod def undo_last_action(self): pass diff --git a/src/internal/objects/impl/card.py b/src/internal/objects/impl/card.py index d0f081a..95b64e4 100644 --- a/src/internal/objects/impl/card.py +++ b/src/internal/objects/impl/card.py @@ -15,9 +15,11 @@ _COLOR_FIELD = 'color' _WIDTH_FIELD = 'width' _HEIGHT_FIELD = 'height' +_ATTRIBUTED_FIELD = 'attribute' class BoardObjectCard(interfaces.IBoardObjectCard, BoardObjectWithFont): + def __init__( self, id: interfaces.ObjectId, @@ -29,11 +31,13 @@ def __init__( color: str = 'light yellow', width: int = 150, height: int = 150, + attribute: dict[str, str] = dict(), # noqa ): super().__init__(id, types.BoardObjectType.CARD, create_dttm, position, pub_sub_broker, text, font) self.color = color self.width = width self.height = height + self.attribute = attribute @property def color(self) -> str: @@ -62,11 +66,20 @@ def height(self, height: int) -> None: self._height = height self._publish(events.EventObjectChangedSize(self.id)) + @property + def attribute(self) -> dict[str, str]: + return self._attribute + + @attribute.setter + def attribute(self, attribute: dict[str, str]) -> None: + self._attribute = attribute + def serialize(self) -> dict: serialized = super().serialize() serialized[_COLOR_FIELD] = self.color serialized[_WIDTH_FIELD] = self.width serialized[_HEIGHT_FIELD] = self.height + serialized[_ATTRIBUTED_FIELD] = self.attribute return serialized @staticmethod @@ -85,4 +98,5 @@ def from_serialized( data[_COLOR_FIELD], data[_WIDTH_FIELD], data[_HEIGHT_FIELD], + data[_ATTRIBUTED_FIELD], ) diff --git a/src/internal/objects/impl/test_card.py b/src/internal/objects/impl/test_card.py index 42e0775..383c3e0 100644 --- a/src/internal/objects/impl/test_card.py +++ b/src/internal/objects/impl/test_card.py @@ -17,9 +17,10 @@ def test_board_object_card_serialization(): color = 'light blue' width = 100 height = 150 + attribute = {'age': 10} broker = internal.pub_sub.mocks.MockPubSubBroker() - card = BoardObjectCard(id, create_dttm, position, broker, text, font, color, width, height) + card = BoardObjectCard(id, create_dttm, position, broker, text, font, color, width, height, attribute) assert card.serialize() == { 'id': id, 'create_dttm': create_dttm.strftime('%Y-%m-%dT%H-%M-%SZ'), @@ -30,6 +31,7 @@ def test_board_object_card_serialization(): 'color': color, 'width': width, 'height': height, + 'attribute': attribute, } @@ -43,6 +45,7 @@ def test_board_object_card_deserialization(): color = 'light blue' width = 100 height = 150 + attribute = {'age': '10'} serialized = { 'id': id, 'create_dttm': create_dttm.strftime('%Y-%m-%dT%H-%M-%SZ'), @@ -53,6 +56,7 @@ def test_board_object_card_deserialization(): 'color': color, 'width': width, 'height': height, + 'attribute': attribute, } broker = internal.pub_sub.mocks.MockPubSubBroker() @@ -67,3 +71,4 @@ def test_board_object_card_deserialization(): assert card.color == color assert card.width == width assert card.height == height + assert card.attribute == attribute diff --git a/src/internal/objects/interfaces.py b/src/internal/objects/interfaces.py index 259eb5f..6e3427d 100644 --- a/src/internal/objects/interfaces.py +++ b/src/internal/objects/interfaces.py @@ -112,6 +112,16 @@ def height(self) -> int: def height(self, height: int) -> None: pass + @property + @abc.abstractmethod + def attribute(self) -> dict[str, str]: + pass + + @attribute.setter + @abc.abstractmethod + def attribute(self, attribute: dict[str, str]) -> None: + pass + class IBoardObjectPen(IBoardObject): DEFAULT_WIDTH = 2 diff --git a/src/internal/objects/test_builder.py b/src/internal/objects/test_builder.py index 2f75092..8bb1160 100644 --- a/src/internal/objects/test_builder.py +++ b/src/internal/objects/test_builder.py @@ -49,6 +49,7 @@ def test_card_building(): 'color': 'light yellow', 'width': 100, 'height': 150, + 'attribute': {'age': '10'} } broker = internal.pub_sub.mocks.MockPubSubBroker() diff --git a/src/internal/repositories/impl/test_repository.py b/src/internal/repositories/impl/test_repository.py index a61b56e..6cdac0f 100644 --- a/src/internal/repositories/impl/test_repository.py +++ b/src/internal/repositories/impl/test_repository.py @@ -33,6 +33,7 @@ def _impl(): 'color': 'light yellow', 'width': 100, 'height': 150, + 'attribute': {'age': '10'}, } return _impl diff --git a/src/internal/view/modules/card/card_view.py b/src/internal/view/modules/card/card_view.py index e5b528e..a0bfaf6 100644 --- a/src/internal/view/modules/card/card_view.py +++ b/src/internal/view/modules/card/card_view.py @@ -144,6 +144,19 @@ def widgets( label, combobox = func(dependencies) _widgets.append(label) _widgets.append(combobox) + card: internal.objects.interfaces.IBoardObjectCard = dependencies.repo.get(self.id) + + for attr, value in card.attribute.items(): + label, entry = self._attribute_widget( + dependencies, + self.id, + attr, + self._get_attribute, + self._set_attribute + ) + _widgets.append(label) + _widgets.append(entry) + return _widgets def _widgets_func(self) -> List[Callable]: @@ -221,6 +234,30 @@ def _base_widget( string_var.trace('w', lambda *_: setter(dependencies, string_var.get())) return label, combobox + def _attribute_widget(self, + dependencies: internal.view.dependencies.Dependencies, + obj_id: internal.objects.interfaces.ObjectId, + description: str, + getter: Callable, + setter: Callable + ) -> List[tkinter.ttk.Widget]: + string_var = tkinter.StringVar(value=getter(dependencies, obj_id, description)) + label = tkinter.ttk.Label( + dependencies.property_bar, + text=description, + justify='left', + anchor='w' + ) + entry = tkinter.ttk.Entry( + dependencies.property_bar, + textvariable=string_var, + ) + string_var.trace('w', lambda *_: setter( + dependencies, obj_id, description, string_var.get() + )) + + return label, entry + def move_to(self, dependencies: internal.view.dependencies.Dependencies, x: int, y: int): self._update_coord_from_repo(dependencies) @@ -324,6 +361,22 @@ def _set_card_size(self, dependencies: internal.view.dependencies.Dependencies, else: dependencies.controller.edit_size(self.id, _DEFAULT_LARGE_SIZE, _DEFAULT_LARGE_SIZE) + def _get_attribute(self, + dependencies: internal.view.dependencies.Dependencies, + obj_id: internal.objects.interfaces.ObjectId, + attr_name: str + ): + card: internal.objects.interfaces.IBoardObjectCard = dependencies.repo.get(obj_id) + return card.attribute[attr_name] + + def _set_attribute(self, + dependencies: internal.view.dependencies.Dependencies, + obj_id: internal.objects.interfaces.ObjectId, + attr_name: str, + value: str + ): + dependencies.controller.edit_attribute(obj_id, attr_name, value) + def destroy(self, dependencies: internal.view.dependencies.Dependencies): self._unsubscribe_from_repo_object_events(dependencies) ViewObject.destroy(self, dependencies) diff --git a/src/internal/view/modules/pivot_table/__init__.py b/src/internal/view/modules/pivot_table/__init__.py new file mode 100644 index 0000000..66ff4b5 --- /dev/null +++ b/src/internal/view/modules/pivot_table/__init__.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import internal.view.dependencies +from .states import add_attribute_state, show_axis_state +from .pivot_table_view import open_window as open_window + + +def create_states(dependencies: internal.view.dependencies): + dependencies.state_machine.add_state(add_attribute_state.create_state(dependencies.state_machine)) + dependencies.state_machine.add_state(show_axis_state.create_state(dependencies.state_machine)) + + +def register_module_menu(dependencies: internal.view.dependencies): + dependencies.menu.add_command_to_menu(add_attribute_state.ADD_ATTR_MENU_ENTRY_NAME) + dependencies.menu.add_command_to_menu(show_axis_state.SHOW_TABLE_MENU_ENTRY_NAME) + + +@internal.view.modules.modules.register_module('pivot_table') +def init_module(dependencies: internal.view.dependencies): + create_states(dependencies) + register_module_menu(dependencies) diff --git a/src/internal/view/modules/pivot_table/pivot_table_view.py b/src/internal/view/modules/pivot_table/pivot_table_view.py new file mode 100644 index 0000000..88a00b0 --- /dev/null +++ b/src/internal/view/modules/pivot_table/pivot_table_view.py @@ -0,0 +1,315 @@ +import tkinter +from tkinter import ttk + +import internal.objects.interfaces +import internal.view.dependencies +from internal.models import Position +from .toplevel import Window + +NAME = 'name' + + +def open_window( + dependencies: internal.view.dependencies.Dependencies +) -> (tkinter.Toplevel, ttk.Entry): + window = Window(dependencies) + window.title('New attribute') + + label = ttk.Label(window, text='Name') + label.grid(row=0, column=0, padx=5, pady=5, sticky='nsew') + entry = window.add_entry(NAME) + entry.grid(row=0, column=1, padx=5, pady=5, sticky='nsew') + + def dummy(): + window.saved = False + dependencies.canvas.event_generate('') + window.destroy() + + window.protocol('WM_DELETE_WINDOW', dummy) + + def after_add(): + window.saved = True + dependencies.canvas.event_generate('') + window.destroy() + + bt = ttk.Button(window, text='Save', command=after_add) + bt.grid(row=4, column=1, padx=5, pady=5, sticky='nsew') + return window + + +def show_axis( + dependencies: internal.view.dependencies.Dependencies +): + # create new window for table view + # window = tkinter.Toplevel(dependencies.canvas, width=1000, height=500) + window = Window(dependencies) + window.title('Chose axis') + + columns = all_attrs(dependencies) + + for i, col in enumerate(columns): + label = ttk.Label(window, text=col) + label.grid(row=i, column=0, padx=5, pady=5, sticky='nsew') + + vals = ['None', 'X', 'Y'] + entry = window.add_combobox(col, vals) + entry.grid(row=i, column=1, padx=5, pady=5, sticky='nsew') + + def dummy(): + window.saved = False + dependencies.canvas.event_generate('') + window.destroy() + + window.protocol('WM_DELETE_WINDOW', dummy) + + def show_table(): + window.saved = True + dependencies.canvas.event_generate('') + window.destroy() + + button = ttk.Button(window, text='Show Table view', command=show_table) + button.grid(row=len(columns), column=1, padx=5, pady=5, sticky='nsew') + return window + + +def all_attrs( + dependencies: internal.view.dependencies.Dependencies +): + attributes = set() + for obj in dependencies.repo.get_all(): + if not isinstance(obj, internal.objects.interfaces.IBoardObjectCard): + continue + card: internal.objects.interfaces.IBoardObjectCard = obj + attributes = attributes.union(set(list(card.attribute.keys()))) + return list(attributes) + + +def get_options( + dependencies: internal.view.dependencies.Dependencies, + name: str + +): + options = {''} + for obj in dependencies.repo.get_all(): + if not isinstance(obj, internal.objects.interfaces.IBoardObjectCard): + continue + card: internal.objects.interfaces.IBoardObjectCard = obj + options.add(card.attribute.get(name, '')) + return list(options) + + +def draw_table( + dependencies: internal.view.dependencies.Dependencies, + val_dic: dict +): + w = Window(dependencies, canvas=True) + w.title('Pivot table') + + def dummy(): + w.saved = False + dependencies.canvas.event_generate('') + w.destroy() + + w.protocol('WM_DELETE_WINDOW', dummy) + + x_list = [] + y_list = [] + for name, axis in val_dic.items(): + if axis == 'X': + x_list.append(name) + elif axis == 'Y': + y_list.append(name) + + x_options = dict() + col_num = 1 + y_options = dict() + row_num = 1 + for attr in x_list: + x_options[attr] = get_options(dependencies, attr) + col_num *= len(x_options[attr]) + pure_col_n = col_num + + for attr in y_list: + y_options[attr] = get_options(dependencies, attr) + row_num *= len(y_options[attr]) + pure_row_n = row_num + + col_num += len(y_list) + row_num += len(x_list) + height = (500 + 50) / row_num + width = (1000 + 50) / col_num + + start_x = 25 + start_y = 25 + + [ + w.canvas.create_line( + start_x + i * width, start_y, + start_x + i * width, + start_y + height * row_num + ) for i in range(0, col_num + 1) + ] + [ + w.canvas.create_line( + start_x, + start_y + i * height, + start_x + width * col_num, + start_y + i * height + ) for i in range(0, row_num + 1) + ] + reps_x = 1 + for i, atr in enumerate(x_list): + opt_n = len(x_options[atr]) + interval = pure_col_n / (opt_n * reps_x) + for j, opt in enumerate(x_options[atr]): + [w.canvas.create_text( + start_x + (interval * (opt_n * m + j) + len(y_list) + 1 / 2) * width, + start_y + (i + 1 / 2) * height, + text=atr + '.' + opt) for m in range(reps_x)] + reps_x *= opt_n + + reps_y = 1 + + for i, atr in enumerate(y_list): + opt_n = len(y_options[atr]) + interval = pure_row_n / (opt_n * reps_y) + for j, opt in enumerate(y_options[atr]): + [w.canvas.create_text(start_x + (i + 1 / 2) * width, + start_y + (interval * (opt_n * m + j) + len( + x_list) + 1 / 2) * height, + text=atr + '.' + opt) for m in range(reps_y)] + reps_y *= opt_n + + obj_coords_x = dict() + obj_coords_y = dict() + for obj in dependencies.repo.get_all(): + if not isinstance(obj, internal.objects.interfaces.IBoardObjectCard): + continue + card: internal.objects.interfaces.IBoardObjectCard = obj + obj_coords_x[card.id] = 0 + reps_x = 1 + for x in x_list: + val = obj.attribute.get(x, '') + obj_coords_x[card.id] += x_options[x].index(val) * reps_x + reps_x *= len(x_options[x]) + 1 + + obj_coords_y[card.id] = 0 + reps_y = 1 + for y in y_list: + val = obj.attribute.get(y, '') + obj_coords_y[card.id] += y_options[y].index(val) * reps_y + reps_y *= len(y_options[y]) + 1 + + create_card_object( + w.canvas, card, + ((obj_coords_x[card.id] + len(y_list) + .5) * width + start_x, + (obj_coords_y[card.id] + len(x_list) + 0.5) * height + start_y) + ) + + return w, x_list, x_options, y_list, y_options, width, height + + +def get_attr_from_position( + position: Position, + x_list, + x_options, + y_list, + y_options, + width, + height +): + start_x = 25 + start_y = 25 + + attributes = dict() + + col = (position.x - start_x) / width - len(y_list) + for attr in reversed(x_list): + opt = col % len(x_options[attr]) + attributes[attr] = x_options[attr][int(opt)] + + col /= len(x_options[attr]) + row = (position.y - start_y) / height - len(x_list) + for attr in reversed(y_list): + opt = row % len(y_options[attr]) + attributes[attr] = y_options[attr][int(opt)] + + row /= len(y_options[attr]) + + return attributes + + +CARD_TEXT_PREFIX = 'card_text' +CARD_NOTE_PREFIX = 'card_note' + +_WIDTH = 50 + + +def create_card_object( + canvas: tkinter.Canvas, + obj: internal.objects.interfaces.IBoardObjectCard, + coordinates: (int, int) +) -> int: + text_tag = f'{CARD_TEXT_PREFIX}{obj.id}' + note_tag = f'{CARD_NOTE_PREFIX}{obj.id}' + canvas.create_text( + coordinates[0], + coordinates[1], + text=obj.text, + fill=obj.font.color, + tags=[obj.id, text_tag], + width=_WIDTH, + font=(obj.font.family, int(obj.font.size), obj.font.weight, obj.font.slant) + ) + + arr = create_note_coord(canvas, obj.id, _WIDTH) + canvas.create_rectangle( + arr, + fill=obj.color, + tags=[obj.id, note_tag], + ) + canvas.tag_lower(note_tag, text_tag) + adjust_font(canvas, obj) + + +def create_note_coord( + canvas: tkinter.Canvas, + obj_id: internal.objects.interfaces.ObjectId, + width: int +): + args = canvas.bbox(obj_id) + arr = [args[i] for i in range(len(args))] + arr[0] = (arr[2] + arr[0] - width) / 2 + arr[1] = (arr[1] + arr[3] - width) / 2 + arr[2] = arr[0] + width + arr[3] = arr[1] + width + return arr + + +def _get_font( + obj: internal.objects.interfaces.IBoardObjectCard, +) -> tuple: + return obj.font.family, obj.font.size, obj.font.weight, obj.font.slant + + +def adjust_font( + canvas: tkinter.Canvas, + obj: internal.objects.interfaces.IBoardObjectCard, + larger=True +): + text_tag = f'{CARD_TEXT_PREFIX}{obj.id}' + width = int(canvas.itemcget(text_tag, 'width')) + _, y1, _, y2 = canvas.bbox(text_tag) + floated_size = float(obj.font.size) + if larger: + while abs(y1 - y2) > width: + floated_size /= 1.05 + canvas.itemconfig(text_tag, font=_get_font(obj)) + _, y1, _, y2 = canvas.bbox(text_tag) + else: + while abs(y1 - y2) < width * 0.7: + floated_size *= 1.05 + canvas.itemconfig(text_tag, font=_get_font(obj)) + _, y1, _, y2 = canvas.bbox(text_tag) + y1 = canvas.canvasx(y1) + y2 = canvas.canvasy(y2) diff --git a/src/internal/view/modules/pivot_table/states/__init__.py b/src/internal/view/modules/pivot_table/states/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/internal/view/modules/pivot_table/states/add_attribute_state.py b/src/internal/view/modules/pivot_table/states/add_attribute_state.py new file mode 100644 index 0000000..67161f0 --- /dev/null +++ b/src/internal/view/modules/pivot_table/states/add_attribute_state.py @@ -0,0 +1,71 @@ +from __future__ import annotations +from typing import Dict +import tkinter + +import internal.objects +import internal.models.position +from internal.view.state_machine.impl import State +import internal.view.state_machine.interfaces +import internal.view.dependencies +from ..toplevel import Window +from ..pivot_table_view import open_window, NAME + +ADD_ATTR_MENU_ENTRY_NAME = 'add attribute' +ADD_ATTRIBUTE_STATE_NAME = 'ADD_ATTRIBUTE' +_WINDOW = 'toplevel_window' + + +def _predicate_from_root_to_add_attribute( + global_dependencies: internal.view.dependencies.Dependencies, event: tkinter.Event +) -> bool: + if global_dependencies.menu.current_state != ADD_ATTR_MENU_ENTRY_NAME: + return False + + return True + + +def _on_enter( + global_dependencies: internal.view.dependencies.Dependencies, + state_ctx: Dict, + event: tkinter.Event +): + state_ctx[_WINDOW] = open_window(global_dependencies) + + +def _on_leave( + global_dependencies: internal.view.dependencies.Dependencies, + state_ctx: Dict, + event: tkinter.Event +): + name: Window = state_ctx[_WINDOW] + if name.saved: + global_dependencies.controller.add_attribute(name.get_vals()[NAME], '') + global_dependencies.menu.set_selected_state() + + +def _predicate_from_add_attribute_to_root( + global_dependencies: internal.view.dependencies.Dependencies, event: tkinter.Event +) -> bool: + if event.type != tkinter.EventType.Deactivate: + return False + + return True + + +def create_state( + state_machine: internal.view.state_machine.interfaces.IStateMachine +) -> State: + state = State(ADD_ATTRIBUTE_STATE_NAME) + state.set_on_enter(_on_enter) + state.set_on_leave(_on_leave) + state_machine.add_transition( + internal.view.state_machine.interfaces.ROOT_STATE_NAME, + ADD_ATTRIBUTE_STATE_NAME, + _predicate_from_root_to_add_attribute + ) + state_machine.add_transition( + ADD_ATTRIBUTE_STATE_NAME, + internal.view.state_machine.interfaces.ROOT_STATE_NAME, + _predicate_from_add_attribute_to_root + ) + return state diff --git a/src/internal/view/modules/pivot_table/states/show_axis_state.py b/src/internal/view/modules/pivot_table/states/show_axis_state.py new file mode 100644 index 0000000..ec53bb8 --- /dev/null +++ b/src/internal/view/modules/pivot_table/states/show_axis_state.py @@ -0,0 +1,160 @@ +from __future__ import annotations +from typing import Dict +import tkinter + +import internal.objects +import internal.models.position +from internal.view.state_machine.impl import State +import internal.view.state_machine.interfaces +import internal.view.dependencies +from internal.models import Position +from ..toplevel import Window +from ..pivot_table_view import show_axis, draw_table, get_attr_from_position + +SHOW_TABLE_MENU_ENTRY_NAME = 'pivot table' +CHOSE_AXIS_STATE_NAME = 'SHOW_TABLE' +_WINDOW = 'window_add' +_TABLE = 'table_window' +_NAME = 'name' +_INITIAL_POSITION = 'obj_position' +_LAST_DRAG_EVENT_X = 'last_drag_event_x' +_LAST_DRAG_EVENT_Y = 'last_drag_event_y' +_FIRST_DRAG_EVENT_X = 'first_drag_event_x' +_FIRST_DRAG_EVENT_Y = 'first_drag_event_y' +_OBJ_ID = 'obj_id' +_MOVE_STARTED = 'moving' +_X_LIST = 'x_list' +_Y_LIST = 'y_list' +_X_OPTIONS = 'x_options' +_Y_OPTIONS = 'y_options' +_WIDTH = 'width' +_HEIGHT = 'height' + + +def _predicate_from_root_to_add_attribute( + global_dependencies: internal.view.dependencies.Dependencies, event: tkinter.Event +) -> bool: + return global_dependencies.menu.current_state == SHOW_TABLE_MENU_ENTRY_NAME + + +def _on_enter( + global_dependencies: internal.view.dependencies.Dependencies, + state_ctx: Dict, + event: tkinter.Event +): + state_ctx[_WINDOW] = show_axis(global_dependencies) + state_ctx[_MOVE_STARTED] = False + global_dependencies.menu.set_selected_state() + + +def _handle_event( + global_dependencies: internal.view.dependencies.Dependencies, + state_ctx: Dict, + event: tkinter.Event +): + window: Window = state_ctx[_WINDOW] + if event.type != tkinter.EventType.Deactivate or not window or not window.saved: + return + table, x_list, x_options, y_list, y_options, width, height = ( + draw_table(global_dependencies, window.get_vals()) + ) + table.canvas.bind( + '', lambda e: move_obj_start(state_ctx, e, state_ctx[_TABLE]) + ) + table.canvas.bind( + '', lambda e: moving_stop( + global_dependencies, state_ctx, e, state_ctx[_TABLE] + ) + ) + state_ctx[_TABLE] = table + state_ctx[_X_LIST] = x_list + state_ctx[_Y_LIST] = y_list + state_ctx[_X_OPTIONS] = x_options + state_ctx[_Y_OPTIONS] = y_options + state_ctx[_WIDTH] = width + state_ctx[_HEIGHT] = height + + +def move_obj_start( + state_ctx: Dict, + event: tkinter.Event, + window: Window +): + if state_ctx[_MOVE_STARTED]: + x = int(window.canvas.canvasx(event.x)) + y = int(window.canvas.canvasy(event.y)) + window.canvas.move( + state_ctx[_OBJ_ID], + x - state_ctx[_LAST_DRAG_EVENT_X], + y - state_ctx[_LAST_DRAG_EVENT_Y] + ) + state_ctx[_LAST_DRAG_EVENT_X] = x + state_ctx[_LAST_DRAG_EVENT_Y] = y + return + + tags = window.canvas.gettags('current') + if not tags: + return + state_ctx[_MOVE_STARTED] = True + window.canvas.scan_mark(event.x, event.y) + + x = int(window.canvas.canvasx(event.x)) + y = int(window.canvas.canvasy(event.y)) + + state_ctx[_LAST_DRAG_EVENT_X] = x + state_ctx[_LAST_DRAG_EVENT_Y] = y + + state_ctx[_FIRST_DRAG_EVENT_X] = x + state_ctx[_FIRST_DRAG_EVENT_Y] = y + state_ctx[_INITIAL_POSITION] = Position(x, y, 1) + state_ctx[_OBJ_ID] = tags[0] + + +def moving_stop( + global_dependencies: internal.view.dependencies.Dependencies, + state_ctx: Dict, + _: tkinter.Event, + window: Window +): + if not state_ctx[_MOVE_STARTED]: + return + state_ctx[_MOVE_STARTED] = False + diff: Position = Position( + state_ctx[_LAST_DRAG_EVENT_X] - state_ctx[_FIRST_DRAG_EVENT_X], + state_ctx[_LAST_DRAG_EVENT_Y] - state_ctx[_FIRST_DRAG_EVENT_Y], + 0 + ) + position = state_ctx[_INITIAL_POSITION] + diff + + attributes = get_attr_from_position(position, state_ctx[_X_LIST], state_ctx[_X_OPTIONS], + state_ctx[_Y_LIST], + state_ctx[_Y_OPTIONS], state_ctx[_WIDTH], + state_ctx[_HEIGHT]) + for name, value in attributes.items(): + global_dependencies.controller.edit_attribute(state_ctx[_OBJ_ID], name, value) + window.canvas.configure(background='white') + + +def _predicate_from_add_attribute_to_root( + global_dependencies: internal.view.dependencies.Dependencies, event: tkinter.Event +) -> bool: + return event.type == tkinter.EventType.Property + + +def create_state( + state_machine: internal.view.state_machine.interfaces.IStateMachine +) -> State: + state = State(CHOSE_AXIS_STATE_NAME) + state.set_on_enter(_on_enter) + state.set_event_handler(_handle_event) + state_machine.add_transition( + internal.view.state_machine.interfaces.ROOT_STATE_NAME, + CHOSE_AXIS_STATE_NAME, + _predicate_from_root_to_add_attribute + ) + state_machine.add_transition( + CHOSE_AXIS_STATE_NAME, + internal.view.state_machine.interfaces.ROOT_STATE_NAME, + _predicate_from_add_attribute_to_root + ) + return state diff --git a/src/internal/view/modules/pivot_table/toplevel.py b/src/internal/view/modules/pivot_table/toplevel.py new file mode 100644 index 0000000..5fc9265 --- /dev/null +++ b/src/internal/view/modules/pivot_table/toplevel.py @@ -0,0 +1,35 @@ +import tkinter +from tkinter import ttk + +import internal.view.dependencies + + +class Window(tkinter.Toplevel): + def __init__(self, + dependencies: internal.view.dependencies.Dependencies, + canvas: bool = False + ): + super(Window, self).__init__(dependencies.canvas) + self.dependencies = dependencies + if canvas: + self.canvas = tkinter.Canvas(self, width=1200, height=600, bg='white') + self.canvas.pack(expand=False) + + self.entries = dict() + self.saved = False + + def add_entry(self, name): + self.entries[name] = ttk.Entry(self) + return self.entries[name] + + def add_combobox(self, name, vals): + self.entries[name] = ttk.Combobox(self, values=vals, state='readonly') + return self.entries[name] + + def get_vals(self): + if self.saved: + tmp = dict() + for name, entry in self.entries.items(): + tmp[name] = entry.get() + return tmp + return None diff --git a/src/internal/view/state_machine/impl/state_machine.py b/src/internal/view/state_machine/impl/state_machine.py index 8d0b2e5..eade055 100644 --- a/src/internal/view/state_machine/impl/state_machine.py +++ b/src/internal/view/state_machine/impl/state_machine.py @@ -61,6 +61,10 @@ def _start_listening(self): self._global_dependencies.canvas.bind('', self.handle_event) # combination of control+left-button-mouse self._global_dependencies.canvas.bind('', self.handle_event) + # combination of control+left-button-mouse + self._global_dependencies.canvas.bind('', self.handle_event) + # combination of control+left-button-mouse + self._global_dependencies.canvas.bind('', self.handle_event) # menu bind self._global_dependencies.menu.bind(self.handle_event) diff --git a/src/internal/view/view.py b/src/internal/view/view.py index d898f6e..5c45367 100644 --- a/src/internal/view/view.py +++ b/src/internal/view/view.py @@ -21,6 +21,7 @@ import internal.view.modules.pen import internal.view.modules.submenu import internal.view.modules.table +import internal.view.modules.pivot_table import internal.view.modules.text import internal.view.modules.undo_redo import internal.view.objects.impl.object_storage