You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
300 lines
9.3 KiB
300 lines
9.3 KiB
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 |
|
}
|
|
|