diff --git a/clusterview/exceptions.py b/clusterview/exceptions.py index 3b66d60..f37a1bb 100644 --- a/clusterview/exceptions.py +++ b/clusterview/exceptions.py @@ -42,9 +42,9 @@ def handle_exceptions(func): def my_qt_func(): raises SomeException """ - def wrapped(self, *args, **kwargs): + def wrapped(*args, **kwargs): try: - return func(self, *args, **kwargs) + return func(*args, **kwargs) except Exception as e: error_dialog = QErrorMessage() error_dialog.showMessage(str(e)) diff --git a/clusterview/mode_handlers.py b/clusterview/mode_handlers.py index 6bdb224..f19aa58 100644 --- a/clusterview/mode_handlers.py +++ b/clusterview/mode_handlers.py @@ -1,5 +1,9 @@ +from PyQt5.QtCore import QEvent, Qt + from .mode import Mode -from .opengl_widget import set_current_points, set_drawing_event +from .opengl_widget import (set_current_points, set_drawing_event, + set_move_bb_top_left, set_move_bb_bottom_right, + reset_move_bbs) from .points import PointSet # Size of point for drawing @@ -9,6 +13,11 @@ __POINT_SIZE = 8 # nature of state management in OpenGL. __point_set = PointSet(__POINT_SIZE) +# Module level flag for left click events (used to detect a left +# click hold drag) +__left_click_flag = False + + def __refresh_point_list(ctx): """ Refreshes the point list display. @@ -37,16 +46,19 @@ def __handle_add_point(ctx, event): """ global __point_set - # No attribute at the moment. - __point_set.add_point(event.x(), event.y()) + if (event.button() == Qt.LeftButton and + event.type() == QEvent.MouseButtonPress): - __refresh_point_list(ctx) + # No attribute at the moment. + __point_set.add_point(event.x(), event.y()) - set_drawing_event(event) - set_current_points(__point_set) + __refresh_point_list(ctx) - ctx.opengl_widget.update() - ctx.point_list_widget.update() + set_drawing_event(event) + set_current_points(__point_set) + + ctx.opengl_widget.update() + ctx.point_list_widget.update() def __handle_edit_point(ctx, event): @@ -67,23 +79,56 @@ def __handle_edit_point(ctx, event): ctx.update() # after this remove the point from the list + def __handle_move_points(ctx, event): - # TODO: Should move the associated points in the list to the new location. - # Store list of old points that are captured + global __left_click_flag set_drawing_event(event) - ctx.update() + + if event.button() == Qt.LeftButton: + __left_click_flag = True + + # This if statement block is used to set the bounding box for + # drawing and call the selection procedure. + if __left_click_flag and event.type() == QEvent.MouseButtonPress: + + set_move_bb_top_left(event.x(), event.y()) + + elif __left_click_flag and event.type() == QEvent.MouseMove: + + set_move_bb_bottom_right(event.x(), event.y()) + + + elif __left_click_flag and event.type() == QEvent.MouseButtonRelease: + + __left_click_flag = False + + # Final bottom right corner point + set_move_bb_bottom_right(event.x(), event.y()) + + # Call the selection procedure + # TODO + + # Satisfy the post condition by resetting the bounding box + reset_move_bbs() + + # Find and move all points from the old list to their new locations + ctx.opengl_widget.update() + def __handle_delete_point(ctx, event): - set_drawing_event(event) + if (event.button() == Qt.LeftButton and + event.type() == QEvent.MouseButtonPress): - __point_set.remove_point(event.x(), event.y()) + set_drawing_event(event) - __refresh_point_list(ctx) + __point_set.remove_point(event.x(), event.y()) - ctx.opengl_widget.update() - ctx.point_list_widget.update() + __refresh_point_list(ctx) + + ctx.opengl_widget.update() + ctx.point_list_widget.update() # Simple dispatcher to make it easy to dispatch the right mode # function when the OpenGL window is clicked. diff --git a/clusterview/opengl_widget.py b/clusterview/opengl_widget.py index 6ca6e08..4241071 100644 --- a/clusterview/opengl_widget.py +++ b/clusterview/opengl_widget.py @@ -8,26 +8,31 @@ To be clear, the actual widget is defined in the UI generated code - `clusterview_ui.py`. The functions here are imported as overrides to the OpenGL functions of that widget. + +It should be split up into a few more separate files eventually... +Probably even into it's own module folder. """ from enum import Enum from OpenGL.GL import (glBegin, glClearColor, glColor4f, glEnable, - glEnd, GL_LIGHT0, GL_LIGHTING, GL_POINTS, + glEnd, GL_LIGHT0, GL_LIGHTING, GL_LINE_LOOP, GL_POINTS, glPointSize, glVertex3f, glViewport) -from .exceptions import InvalidModeError, InvalidStateError +from .exceptions import handle_exceptions, InvalidModeError, InvalidStateError from .mode import Mode from .points import PointSet class Color(Enum): BLUE = 0 + BLACK = 1 # A simple map from Color -> RGBA 4-Tuple # Note: The color values in the tuple are not RGB, but # rather OpenGL percentage values for RGB. COLOR_TO_RGBA = { - Color.BLUE: (0, 0.5, 1.0, 0.0) + Color.BLUE: (0, 0.5, 1.0, 0.0), + Color.BLACK: (0.0, 0.0, 0.0, 0.0) } # Constants set based on the size of the window. @@ -35,6 +40,11 @@ __BOTTOM_LEFT = (0, 0) __WIDTH = None __HEIGHT = None +# State variables for a move selection bounding box. +# There are always reset to None after a selection has been made. +__move_bb_top_left = None +__move_bb_bottom_right = None + # Module-global state variables for our drawing # state machine. # @@ -117,6 +127,38 @@ def set_drawing_event(event): __current_event = event +def set_move_bb_top_left(x, y): + """ + Called to set the move bounding box's top left corner. + + @param x The x-coordinate. + @param y The y-coordinate. + """ + global __move_bb_top_left + + __move_bb_top_left = (x, y) + + +def set_move_bb_bottom_right(x, y): + """ + Called to set the move bounding box's bottom right corner. + + @param x The x-coordinate. + @param y The y-coordinate. + """ + global __move_bb_bottom_right + + __move_bb_bottom_right = (x, y) + + +def reset_move_bbs(): + global __move_bb_top_left + global __move_bb_bottom_right + + __move_bb_top_left = None + __move_bb_bottom_right = None + + def initialize_gl(): """ Initializes the OpenGL context on the Window. @@ -144,6 +186,7 @@ def resize_gl(w, h): __HEIGHT = __current_context.height() +@handle_exceptions def paint_gl(): """ Stock PaintGL function from OpenGL that switches @@ -155,10 +198,9 @@ def paint_gl(): raise InvalidStateError("Event must exist for ADD, EDIT, MOVE, " + "and DELETE") - if (__current_mode in [Mode.ADD, Mode.EDIT, Mode.MOVE, Mode.DELETE] and + if (__current_mode in [Mode.ADD, Mode.EDIT, Mode.DELETE] and __current_points is None): - raise InvalidStateError("Points must exist for ADD, EDIT, MOVE, " + - "and DELETE") + return if __current_mode is Mode.ADD or __current_mode is Mode.DELETE: # Note that drawing the points doesn't require a bounding box or @@ -167,10 +209,29 @@ def paint_gl(): # is the same as adding a point since we just draw what is in # the point set. draw_points(__current_points, Color.BLUE) + elif __current_mode is Mode.EDIT: raise NotImplementedError("Drawing for EDIT not implemented.") + elif __current_mode is Mode.MOVE: - raise NotImplementedError("Drawing for MOVE not implemented.") + # We have to repeatedly draw the points while we are showing the + # move box. + if __current_points is not None: + draw_points(__current_points, Color.BLUE) + + draw_selection_box(Color.BLACK) + + if __move_bb_top_left is None and __move_bb_bottom_right is None: + # Currently this fires all the time - not great. Needs to only fire + # when, additionally, we have a selection chosen based on the box + # calculated in the mode handlers. + + print("FIRE THE MOVE STUFF") + # Once the selection boxes go to None begin the highlight selected + # points procedure. This will store a point list of selected points + # highlight them, etc. + # Once thats done moving points will be difficult but do-able. + elif __current_mode is Mode.DELETE: raise NotImplementedError("Drawing for DELETE not implemented.") @@ -199,6 +260,60 @@ def __clamp_y(y): return y_w +def draw_selection_box(color): + """ + When the move bounding box state is populated and the mode is set + to MODE.Move this function will draw the selection bounding box. + + @param color The color Enum. + """ + global __current_context + + if __current_context is None: + raise InvalidStateError("Drawing context must be set before setting " + + "drawing mode") + + if not isinstance(color, Color): + raise ValueError("Color must exist in the Color enumeration") + + if __move_bb_top_left is None or __move_bb_bottom_right is None: + # Nothing to draw. + return + + ct = COLOR_TO_RGBA[color] + + glViewport(0, 0, __WIDTH, __HEIGHT) + + # Top right corner has the same x as the bottom right + # and same y as the top left. + top_right_corner = (__move_bb_bottom_right[0], __move_bb_top_left[1]) + + # Bottom left corner has the same x as the top left and + # same y as the bottom right. + bottom_left_corner = (__move_bb_top_left[0], __move_bb_bottom_right[1]) + + + glBegin(GL_LINE_LOOP) + glColor4f(ct[0], ct[1], ct[2], ct[3]) + + glVertex3f(__clamp_x(__move_bb_top_left[0]), + __clamp_y(__move_bb_top_left[1]), + 0.0) + + glVertex3f(__clamp_x(top_right_corner[0]), + __clamp_y(top_right_corner[1]), + 0.0) + + glVertex3f(__clamp_x(__move_bb_bottom_right[0]), + __clamp_y(__move_bb_bottom_right[1]), + 0.0) + + glVertex3f(__clamp_x(bottom_left_corner[0]), + __clamp_y(bottom_left_corner[1]), + 0.0) + + glEnd() + def draw_points(point_set, color): """ Simple point drawing function. diff --git a/main_window.py b/main_window.py index 8b0d55d..ee86281 100644 --- a/main_window.py +++ b/main_window.py @@ -32,6 +32,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): # to OpenGL coordinates properly. set_drawing_context(self.opengl_widget) + # Enables mouse tracking on the viewport so mouseMoveEvents are + # tracked and fired properly. + self.opengl_widget.setMouseTracking(True) + #----------------------------------------------- # OpenGL Graphics Handlers are set # here and defined in clusterview.opengl_widget. @@ -52,6 +56,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): # Override handler for mouse press so we can draw points based on # the OpenGL coordinate system inside of the OpenGL Widget. self.opengl_widget.mousePressEvent = self.__ogl_click_dispatcher + self.opengl_widget.mouseMoveEvent = self.__ogl_click_dispatcher + self.opengl_widget.mouseReleaseEvent = self.__ogl_click_dispatcher #----------------------------------------------------------------- # Mode changers - these will be used to signal the action in the @@ -98,12 +104,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): """ Mode dispatcher for click actions on the OpenGL widget. """ - if self.__mode is Mode.OFF: - raise InvalidModeError(Mode.OFF) - - # Map from Mode -> function - # where the function is a handler for the - # OpenGL event. The context passed to these functions allows - # them to modify on screen widgets such as the QOpenGLWidget and - # QListWidget. - MODE_HANDLER_MAP[self.__mode](self, event) + + if self.__mode is not Mode.OFF: + # Map from Mode -> function + # where the function is a handler for the + # OpenGL event. The context passed to these functions allows + # them to modify on screen widgets such as the QOpenGLWidget and + # QListWidget. + MODE_HANDLER_MAP[self.__mode](self, event)