from enum import Enum from PyQt5.QtCore import QEvent, Qt from PyQt5.QtGui import QCursor from .exceptions import ExceededWindowBoundsError from .mode import Mode from .opengl_widget import (get_bb_bottom_right, get_bb_top_left, set_drawing_event, set_move_bb_top_left, set_move_bb_bottom_right, reset_move_bbs, viewport_height, viewport_width) from .points import PointSet from .point_manager import PointManager class __ClickFlag: # This is the first stage. On mouse release it goes to # SELECTION_MOVE. NONE = 0 # We are now in selection box mode. SELECTION_BOX = 1 # Second stage - we have selected a number of points # and now we are going to track the left mouse button # to translate those points. After a left click # this moves to SELECTED_MOVED. SELECTION_MOVE = 2 # Any subsequent click in this mode will send it back # to NONE - we are done. SELECTED_MOVED = 3 # Size of point for drawing __POINT_SIZE = 8 # Canvas pixel border - empirical, not sure where this is stored officially __CANVAS_BORDER = 1 # PointManager is a class that is filled with static methods # designed for managing state. PointManager.point_set = PointSet(__POINT_SIZE, viewport_height(), viewport_width()) # Module level flag for left click events (used to detect a left # click hold drag) __left_click_flag = __ClickFlag.NONE # Variable to track the mouse state during selection movement __last_mouse_pos = None # Used to implement mouse dragging when clicked __left_click_down = False def refresh_point_list(ctx): """ Refreshes the point list display. @param ctx A handle to the window context. """ # In order to make some guarantees and avoid duplicate # data we will clear the point list widget and re-populate # it using the current __point_set. ctx.point_list_widget.clear() for p in PointManager.point_set.points: ctx.point_list_widget.addItem("({}, {})".format(p.x, p.y)) ctx.point_list_widget.update() def __handle_add_point(ctx, event): """ Event handler for the add point mode. Sets the drawing mode for the OpenGL Widget using `set_drawing_mode`, converts a point to our point representation, and adds it to the list. @param ctx A context handle to the main window. @param event The click event. """ # Update information as needed __handle_info_updates(ctx, event) if (event.button() == Qt.LeftButton and event.type() == QEvent.MouseButtonPress): # At this point we can be sure resize_gl has been called # at least once, so set the viewport properties of the # point set so it knows the canvas bounds. PointManager.point_set.viewport_width = viewport_width() PointManager.point_set.viewport_height = viewport_height() # Clear any existing selections PointManager.point_set.clear_selection() try: # No attribute at the moment. PointManager.point_set.add_point(event.x(), event.y()) except ExceededWindowBoundsError: # The user tried to place a point whos edges would be # on the outside of the window. We will just ignore it. return refresh_point_list(ctx) set_drawing_event(event) ctx.opengl_widget.update() ctx.point_list_widget.update() def __handle_edit_point(ctx, event): # TODO: This function and delete definitely need to make sure they are # on a point we have. # # Since points are unique consider a hashmap of points to make O(1) # lookups for addition and deletion. This list can be maintained here # in this module. It should be a dictionary - from point to # attributes in the case of algorithms that require points to have # weights or something. # # Should move the associated point in the list to the new location if # applicable. __handle_info_updates(ctx, event) PointManager.point_set.clear_selection() # Store old x, y from event set_drawing_event(event) ctx.update() # after this remove the point from the list def ogl_keypress_handler(ctx, event): """ A keypress handler attached to the OpenGL widget. It primarily exists to allow the user to cancel selection. Also allows users to escape from modes. @param ctx A handle to the window context. @param event The event associated with this handler. """ global __left_click_flag global __last_mouse_pos if event.key() == Qt.Key_Escape: if ctx.mode is Mode.MOVE: if __left_click_flag is not __ClickFlag.NONE: __last_mouse_pos = None __left_click_flag = __ClickFlag.NONE PointManager.point_set.clear_selection() reset_move_bbs() refresh_point_list(ctx) elif ctx.mode is not Mode.OFF: ctx.mode = Mode.OFF # Also change the mouse back to normal ctx.opengl_widget.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) ctx.status_bar.showMessage("") ctx.opengl_widget.update() def __handle_move_points(ctx, event): """ A relatively complicated state machine that handles the process of selection, clicking, and dragging. @param ctx The context to the window. @param event The event. """ global __left_click_flag global __left_mouse_down global __last_mouse_pos set_drawing_event(event) __handle_info_updates(ctx, event) # If we release the mouse, we want to quickly alert drag mode. if (event.button() == Qt.LeftButton and event.type() == QEvent.MouseButtonRelease): __left_mouse_down = False # This if statement block is used to set the bounding box for # drawing and call the selection procedure. if (event.button() == Qt.LeftButton and event.type() == QEvent.MouseButtonPress): __left_mouse_down = True if __left_click_flag is __ClickFlag.NONE: __left_click_flag = __ClickFlag.SELECTION_BOX set_move_bb_top_left(event.x(), event.y()) elif (__left_click_flag is __ClickFlag.SELECTION_BOX and __left_mouse_down): # We are now in the click-and-hold to signal move # tracking and translation __left_click_flag = __ClickFlag.SELECTION_MOVE __last_mouse_pos = (event.x(), event.y()) # Post-selection handlers if (__left_click_flag is __ClickFlag.SELECTION_BOX and event.type() == QEvent.MouseMove): set_move_bb_bottom_right(event.x(), event.y()) elif (__left_click_flag is __ClickFlag.SELECTION_MOVE and __last_mouse_pos is not None and __left_mouse_down and event.type() == QEvent.MouseMove): dx = abs(__last_mouse_pos[0] - event.x()) dy = abs(__last_mouse_pos[1] - event.y()) for p in PointManager.point_set.points: if p.selected: # Use the deltas to decide what direction to move. # We only want to move in small unit increments. # If we used the deltas directly the points would # fly off screen quickly as we got farther from our # start. try: if event.x() < __last_mouse_pos[0]: p.move(-dx, 0) if event.y() < __last_mouse_pos[1]: p.move(0, -dy) if event.x() > __last_mouse_pos[0]: p.move(dx, 0) if event.y() > __last_mouse_pos[1]: p.move(0, dy) except ExceededWindowBoundsError: # This point has indicated a move would exceed # it's bounds, so we'll just go to the next # point. continue __last_mouse_pos = (event.x(), event.y()) elif (__left_click_flag is not __ClickFlag.NONE and event.type() == QEvent.MouseButtonRelease): if __left_click_flag is __ClickFlag.SELECTION_BOX: set_move_bb_bottom_right(event.x(), event.y()) # Satisfy the post condition by resetting the bounding box reset_move_bbs() ctx.opengl_widget.update() def __handle_delete_point(ctx, event): __handle_info_updates(ctx, event) if (event.button() == Qt.LeftButton and event.type() == QEvent.MouseButtonPress): set_drawing_event(event) PointManager.point_set.remove_point(event.x(), event.y()) refresh_point_list(ctx) ctx.opengl_widget.update() ctx.point_list_widget.update() def __handle_info_updates(ctx, event): """ Updates data under the "information" header. @param ctx The context to the main window. @param event The event. """ if event.type() == QEvent.MouseMove: ctx.mouse_position_label.setText(f"{event.x(), event.y()}") # Simple dispatcher to make it easy to dispatch the right mode # function when the OpenGL window is acted on. MODE_HANDLER_MAP = { Mode.OFF: __handle_info_updates, Mode.LOADED: __handle_info_updates, Mode.ADD: __handle_add_point, Mode.EDIT: __handle_edit_point, Mode.MOVE: __handle_move_points, Mode.DELETE: __handle_delete_point }