Browse Source

Change point state to use a singleton to smooth out some weird bugs

tb-init-ui-render
Taylor Bockman 5 years ago
parent
commit
08b46e501c
  1. 41
      clusterview/mode_handlers.py
  2. 53
      clusterview/opengl_widget.py
  3. 52
      clusterview/point_list_widget.py
  4. 7
      clusterview/point_manager.py
  5. 3
      clusterview/points.py
  6. 6
      main_window.py
  7. 14
      tests/test_point.py
  8. 23
      tests/test_point_set.py

41
clusterview/mode_handlers.py

@ -5,10 +5,11 @@ from PyQt5.QtCore import QEvent, Qt
from .exceptions import ExceededWindowBoundsError from .exceptions import ExceededWindowBoundsError
from .mode import Mode from .mode import Mode
from .opengl_widget import (get_bb_bottom_right, get_bb_top_left, from .opengl_widget import (get_bb_bottom_right, get_bb_top_left,
set_current_points, set_drawing_event, set_drawing_event, set_move_bb_top_left,
set_move_bb_top_left, set_move_bb_bottom_right, set_move_bb_bottom_right, reset_move_bbs,
reset_move_bbs, viewport_height, viewport_width) viewport_height, viewport_width)
from .points import PointSet from .points import PointSet
from .point_manager import PointManager
class __ClickFlag: class __ClickFlag:
@ -32,9 +33,10 @@ class __ClickFlag:
# Size of point for drawing # Size of point for drawing
__POINT_SIZE = 8 __POINT_SIZE = 8
# There are a lot of module-global variables being used because of the # PointManager is a class that is filled with static methods
# nature of state management in OpenGL. # designed for managing state.
__point_set = PointSet(__POINT_SIZE, viewport_height(), viewport_width()) PointManager.point_set = PointSet(__POINT_SIZE, viewport_height(),
viewport_width())
# Module level flag for left click events (used to detect a left # Module level flag for left click events (used to detect a left
# click hold drag) # click hold drag)
@ -55,9 +57,11 @@ def __refresh_point_list(ctx):
# it using the current __point_set. # it using the current __point_set.
ctx.point_list_widget.clear() ctx.point_list_widget.clear()
for p in __point_set.points: for p in PointManager.point_set.points:
ctx.point_list_widget.addItem("({}, {})".format(p.x, p.y)) ctx.point_list_widget.addItem("({}, {})".format(p.x, p.y))
ctx.point_list_widget.update()
def __handle_add_point(ctx, event): def __handle_add_point(ctx, event):
""" """
Event handler for the add point mode. Event handler for the add point mode.
@ -69,23 +73,21 @@ def __handle_add_point(ctx, event):
@param ctx A context handle to the main window. @param ctx A context handle to the main window.
@param event The click event. @param event The click event.
""" """
global __point_set
if (event.button() == Qt.LeftButton and if (event.button() == Qt.LeftButton and
event.type() == QEvent.MouseButtonPress): event.type() == QEvent.MouseButtonPress):
# At this point we can be sure resize_gl has been called # At this point we can be sure resize_gl has been called
# at least once, so set the viewport properties of the # at least once, so set the viewport properties of the
# point set so it knows the canvas bounds. # point set so it knows the canvas bounds.
__point_set.viewport_width = viewport_width() PointManager.point_set.viewport_width = viewport_width()
__point_set.viewport_height = viewport_height() PointManager.point_set.viewport_height = viewport_height()
# Clear any existing selections # Clear any existing selections
__point_set.clear_selection() PointManager.point_set.clear_selection()
try: try:
# No attribute at the moment. # No attribute at the moment.
__point_set.add_point(event.x(), event.y()) PointManager.point_set.add_point(event.x(), event.y())
except ExceededWindowBoundsError: except ExceededWindowBoundsError:
# The user tried to place a point whos edges would be # The user tried to place a point whos edges would be
# on the outside of the window. We will just ignore it. # on the outside of the window. We will just ignore it.
@ -95,8 +97,6 @@ def __handle_add_point(ctx, event):
set_drawing_event(event) set_drawing_event(event)
set_current_points(__point_set)
ctx.opengl_widget.update() ctx.opengl_widget.update()
ctx.point_list_widget.update() ctx.point_list_widget.update()
@ -112,9 +112,7 @@ def __handle_edit_point(ctx, event):
# #
# Should move the associated point in the list to the new location if # Should move the associated point in the list to the new location if
# applicable. # applicable.
global __point_set PointManager.point_set.clear_selection()
__point_set.clear_selection()
# Store old x, y from event # Store old x, y from event
set_drawing_event(event) set_drawing_event(event)
@ -143,7 +141,7 @@ def ogl_keypress_handler(ctx, event):
__mouse_start = None __mouse_start = None
__left_click_flag = __ClickFlag.NONE __left_click_flag = __ClickFlag.NONE
__point_set.clear_selection() PointManager.point_set.clear_selection()
reset_move_bbs() reset_move_bbs()
elif ctx.mode is not Mode.OFF: elif ctx.mode is not Mode.OFF:
@ -162,7 +160,6 @@ def __handle_move_points(ctx, event):
global __left_click_flag global __left_click_flag
global __mouse_start global __mouse_start
global __point_set
set_drawing_event(event) set_drawing_event(event)
@ -196,7 +193,7 @@ def __handle_move_points(ctx, event):
dx = __mouse_start[0] - event.x() dx = __mouse_start[0] - event.x()
dy = __mouse_start[1] - event.y() dy = __mouse_start[1] - event.y()
for p in __point_set.points: for p in PointManager.point_set.points:
if p.selected: if p.selected:
# Use the deltas to decide what direction to move. # Use the deltas to decide what direction to move.
# We only want to move in small unit increments. # We only want to move in small unit increments.
@ -239,7 +236,7 @@ def __handle_delete_point(ctx, event):
set_drawing_event(event) set_drawing_event(event)
__point_set.remove_point(event.x(), event.y()) PointManager.point_set.remove_point(event.x(), event.y())
__refresh_point_list(ctx) __refresh_point_list(ctx)

53
clusterview/opengl_widget.py

@ -21,7 +21,7 @@ from OpenGL.GL import (glBegin, glClearColor, glColor3f, glEnable,
from .exceptions import handle_exceptions, InvalidModeError, InvalidStateError from .exceptions import handle_exceptions, InvalidModeError, InvalidStateError
from .mode import Mode from .mode import Mode
from .points import PointSet from .point_manager import PointManager
class Color(Enum): class Color(Enum):
BLUE = 0 BLUE = 0
@ -55,10 +55,8 @@ __move_bb_bottom_right = None
# function local. # function local.
__current_mode = None __current_mode = None
__current_event = None __current_event = None
__current_points = None
__current_context = None __current_context = None
def set_drawing_context(ctx): def set_drawing_context(ctx):
""" """
Sets the drawing context so that drawing functions can properly Sets the drawing context so that drawing functions can properly
@ -68,26 +66,6 @@ def set_drawing_context(ctx):
__current_context = ctx __current_context = ctx
def set_current_points(points):
"""
Sets the point state variable that will be passed in to the paint_gl
function, and further to the point painter functions in order to
render the scene.
Each time a point is added, removed, or edited this point set must be
updated.
@param points The PointSet representing the current scene.
"""
global __current_points
if not isinstance(points, PointSet):
raise ValueError("set_current_points must recieve a PointSet as its " +
"argument.")
__current_points = points
def set_drawing_mode(mode): def set_drawing_mode(mode):
""" """
State management function. It is useful to look at the State management function. It is useful to look at the
@ -128,7 +106,6 @@ def set_drawing_event(event):
if event is not None: if event is not None:
__current_event = event __current_event = event
def set_move_bb_top_left(x, y): def set_move_bb_top_left(x, y):
""" """
Called to set the move bounding box's top left corner. Called to set the move bounding box's top left corner.
@ -140,7 +117,6 @@ def set_move_bb_top_left(x, y):
__move_bb_top_left = (x, y) __move_bb_top_left = (x, y)
def set_move_bb_bottom_right(x, y): def set_move_bb_bottom_right(x, y):
""" """
Called to set the move bounding box's bottom right corner. Called to set the move bounding box's bottom right corner.
@ -152,15 +128,12 @@ def set_move_bb_bottom_right(x, y):
__move_bb_bottom_right = (x, y) __move_bb_bottom_right = (x, y)
def get_bb_top_left(): def get_bb_top_left():
return __move_bb_top_left return __move_bb_top_left
def get_bb_bottom_right(): def get_bb_bottom_right():
return __move_bb_bottom_right return __move_bb_bottom_right
def reset_move_bbs(): def reset_move_bbs():
global __move_bb_top_left global __move_bb_top_left
global __move_bb_bottom_right global __move_bb_bottom_right
@ -185,7 +158,6 @@ def resize_gl(w, h):
""" """
global __WIDTH global __WIDTH
global __HEIGHT global __HEIGHT
global __current_points
__WIDTH = __current_context.width() __WIDTH = __current_context.width()
__HEIGHT = __current_context.height() __HEIGHT = __current_context.height()
@ -208,7 +180,7 @@ def paint_gl():
return return
if (__current_mode in [Mode.ADD, Mode.EDIT, Mode.DELETE] and if (__current_mode in [Mode.ADD, Mode.EDIT, Mode.DELETE] and
__current_points is None): PointManager.point_set.empty()):
return return
if __current_mode is Mode.ADD or __current_mode is Mode.DELETE: if __current_mode is Mode.ADD or __current_mode is Mode.DELETE:
@ -217,7 +189,7 @@ def paint_gl():
# the point set, which will be redrawn here. This action # the point set, which will be redrawn here. This action
# is the same as adding a point since we just draw what is in # is the same as adding a point since we just draw what is in
# the point set. # the point set.
draw_points(__current_points, Color.GREY) draw_points(PointManager.point_set, Color.GREY)
elif __current_mode is Mode.EDIT: elif __current_mode is Mode.EDIT:
raise NotImplementedError("Drawing for EDIT not implemented.") raise NotImplementedError("Drawing for EDIT not implemented.")
@ -225,8 +197,8 @@ def paint_gl():
elif __current_mode is Mode.MOVE: elif __current_mode is Mode.MOVE:
# We have to repeatedly draw the points while we are showing the # We have to repeatedly draw the points while we are showing the
# move box. # move box.
if __current_points is not None: if not PointManager.point_set.empty():
draw_points(__current_points, Color.GREY) draw_points(PointManager.point_set, Color.GREY)
draw_selection_box(Color.BLACK) draw_selection_box(Color.BLACK)
@ -236,8 +208,7 @@ def paint_gl():
# Mark points that are selected in the bounding box # Mark points that are selected in the bounding box
# and draw them using the normal function # and draw them using the normal function
highlight_selection() highlight_selection()
draw_points(__current_points, Color.GREY) draw_points(PointManager.point_set, Color.GREY)
def __clamp_x(x): def __clamp_x(x):
""" """
@ -296,7 +267,7 @@ def highlight_selection():
top_left = get_bb_top_left() top_left = get_bb_top_left()
bottom_right = get_bb_bottom_right() bottom_right = get_bb_bottom_right()
for point in __current_points.points: for point in PointManager.point_set.points:
if box_hit(point.x, point.y, top_left[0], top_left[1], if box_hit(point.x, point.y, top_left[0], top_left[1],
bottom_right[0], bottom_right[1]): bottom_right[0], bottom_right[1]):
@ -358,7 +329,6 @@ def draw_selection_box(color):
glEnd() glEnd()
def clear_selection(): def clear_selection():
""" """
A helper designed to be called from the main window A helper designed to be called from the main window
@ -366,10 +336,8 @@ def clear_selection():
and mode files. This way you dont have to do something and mode files. This way you dont have to do something
before the selection clears. before the selection clears.
""" """
global __current_points if not PointManager.point_set.empty():
PointManager.point_set.clear_selection()
if __current_points is not None:
__current_points.clear_selection()
def draw_points(point_set, color): def draw_points(point_set, color):
""" """
@ -393,8 +361,7 @@ def draw_points(point_set, color):
glViewport(0, 0, __WIDTH, __HEIGHT) glViewport(0, 0, __WIDTH, __HEIGHT)
glPointSize(__current_points.point_size) glPointSize(PointManager.point_set.point_size)
glBegin(GL_POINTS) glBegin(GL_POINTS)
for point in point_set.points: for point in point_set.points:

52
clusterview/point_list_widget.py

@ -0,0 +1,52 @@
"""
Similar to the opengl_widget module, this module defines
helper functions for the point_list_widget. It is named
the same for convenience. The actual point_list_widget
is defined in the clusterview_ui.py file.
"""
from .point_manager import PointManager
def __string_point_to_point(str_point):
"""
In the QListWidget points are stored as strings
because of the way Qt has list items defined.
@param str_point The string of the form (x, y) to convert.
"""
# 1. Split
elems = str_point.split(",")
# 2. Take elements "(x" and "y)" and remove their first and
# last characters, respectively. Note that for y this
# function expects there to be a space after the comma.
x = elems[0][1:]
y = elems[1][1:-1]
return (x, y)
def item_click_handler(ctx, item):
"""
Handles an item becoming clicked in the list.
This function is designed to be partially applied with the
main_window context in order to be able to trigger an opengl_widget
refresh.
@param ctx The context.
@param item The clicked item.
"""
point = __string_point_to_point(item.text())
# TODO: Super slow linear search, should write a find_point function
# on the point_set in order to speed this up since PointSet
# is backed by a set anyway.
for p in PointManager.point_set.points:
if p.x == point[0] and p.y == point[1]:
p.select()
else:
p.unselect()
ctx.opengl_widget.update()

7
clusterview/point_manager.py

@ -0,0 +1,7 @@
class PointManager():
"""
A state class that represents the absolute state of the
world in regards to points.
"""
point_set = None

3
clusterview/points.py

@ -211,6 +211,9 @@ class PointSet:
def viewport_width(self, width): def viewport_width(self, width):
self.__viewport_width = width self.__viewport_width = width
def empty(self):
return len(self.__points) == 0
def clear_selection(self): def clear_selection(self):
""" """
Handy helper function to clear all selected points. Handy helper function to clear all selected points.

6
main_window.py

@ -12,6 +12,7 @@ from clusterview.mode_handlers import MODE_HANDLER_MAP, ogl_keypress_handler
from clusterview.opengl_widget import (clear_selection, initialize_gl, from clusterview.opengl_widget import (clear_selection, initialize_gl,
paint_gl, resize_gl, paint_gl, resize_gl,
set_drawing_mode, set_drawing_context) set_drawing_mode, set_drawing_context)
from clusterview.point_list_widget import item_click_handler
from clusterview_ui import Ui_MainWindow from clusterview_ui import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
@ -44,6 +45,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# into the function so that we can modify it as we please. # into the function so that we can modify it as we please.
self.opengl_widget.keyPressEvent = partial(ogl_keypress_handler, self) self.opengl_widget.keyPressEvent = partial(ogl_keypress_handler, self)
# Same story here but this time with the itemClicked event
# so that when an element is clicked on in the point list it will
# highlight.
self.point_list_widget.itemClicked = partial(item_click_handler, self)
#----------------------------------------------- #-----------------------------------------------
# OpenGL Graphics Handlers are set # OpenGL Graphics Handlers are set
# here and defined in clusterview.opengl_widget. # here and defined in clusterview.opengl_widget.

14
tests/test_point.py

@ -13,25 +13,25 @@ def test_move_point():
assert p.x == 5 and p.y == 5 assert p.x == 5 and p.y == 5
def test_move_point_outside_screen_x_positive(): def test_move_point_outside_screen_x_positive():
p = Point(1, 2, 8, 100, 100) p = Point(4, 4, 8, 100, 100)
with pytest.raises(ExceededWindowBoundsError): with pytest.raises(ExceededWindowBoundsError) as exc_info:
p.move(96, 0) p.move(96, 0)
def test_move_point_outside_screen_y_positive(): def test_move_point_outside_screen_y_positive():
p = Point(1, 2, 8, 100, 100) p = Point(4, 4, 8, 100, 100)
with pytest.raises(ExceededWindowBoundsError): with pytest.raises(ExceededWindowBoundsError):
p.move(0, 95) p.move(0, 95)
def test_move_point_outside_screen_x_negative(): def test_move_point_outside_screen_x_negative():
p = Point(1, 2, 8, 100, 100) p = Point(4, 4, 8, 100, 100)
with pytest.raises(ExceededWindowBoundsError): with pytest.raises(ExceededWindowBoundsError):
p.move(-2, 0) p.move(-5, 0)
def test_move_point_outside_screen_y_negative(): def test_move_point_outside_screen_y_negative():
p = Point(1, 2, 8, 100, 100) p = Point(4, 4, 8, 100, 100)
with pytest.raises(ExceededWindowBoundsError): with pytest.raises(ExceededWindowBoundsError):
p.move(0, -3) p.move(0, -5)

23
tests/test_point_set.py

@ -3,6 +3,11 @@ import pytest
from clusterview.points import Attribute, Point, PointSet from clusterview.points import Attribute, Point, PointSet
def test_empty():
l = PointSet(3, 100, 100)
assert l.empty()
def test_add_to_point_set(): def test_add_to_point_set():
l = PointSet(3, 100, 100) l = PointSet(3, 100, 100)
@ -33,10 +38,10 @@ def test_remove_point_exact_click():
attribute = Attribute("thing", 1) attribute = Attribute("thing", 1)
l = PointSet(8, 100, 100) l = PointSet(8, 100, 100)
l.add_point(1, 2, attrs=[attribute]) l.add_point(4, 4, attrs=[attribute])
p = Point(1, 2, 8, 100, 100) p = Point(4, 4, 8, 100, 100)
l.remove_point(1, 2) l.remove_point(4, 4)
points = list(l.points) points = list(l.points)
@ -56,13 +61,13 @@ def test_remove_point_bounding_box():
attribute = Attribute("thing", 1) attribute = Attribute("thing", 1)
l = PointSet(8, 100, 100) l = PointSet(8, 100, 100)
l.add_point(1, 2, attrs=[attribute]) l.add_point(4, 4, attrs=[attribute])
p = Point(1, 2, 8, 100, 100) p = Point(4, 4, 8, 100, 100)
# The click-point (2, 1) will be inside of our point size 8 # The click-point (2, 1) will be inside of our point size 8
# bounding box. # bounding box.
l.remove_point(2, 1) l.remove_point(4, 4)
points = list(l.points) points = list(l.points)
@ -78,12 +83,12 @@ def test_remove_point_bounding_box():
def test_attributes_must_be_array_of_attributes(): def test_attributes_must_be_array_of_attributes():
with pytest.raises(ValueError): with pytest.raises(ValueError):
l = PointSet(8, 100, 100) l = PointSet(8, 100, 100)
l.add_point(1, 2, attrs=[1,2,3,4,5]) l.add_point(4, 4, attrs=[1,2,3,4,5])
def test_clear_all_selected_points(): def test_clear_all_selected_points():
l = PointSet(8, 100, 100) l = PointSet(8, 100, 100)
l.add_point(1, 2) l.add_point(4, 4)
l.add_point(3, 4) l.add_point(5, 5)
for p in l.points: for p in l.points:
p.select() p.select()

Loading…
Cancel
Save