Skip to content

Commit

Permalink
Toggle Mouse Resizing (#274)
Browse files Browse the repository at this point in the history
* Added mouse resize locking

* Fixed corners and cursor when resizing is disabled

* Added prompt on remove all

* Disable mouse resizing by default

---------

Co-authored-by: Patrick Gu <[email protected]>
  • Loading branch information
Darth-Raazi and patrick-gu authored Jul 27, 2024
1 parent f309ce0 commit 9c26f76
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 76 deletions.
127 changes: 64 additions & 63 deletions sinks/dashboard/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def mouseDoubleClickEvent(self, event):
#else close the property panel
self.dashboard.open_property_panel(None)
super().mouseDoubleClickEvent(event) # Call the superclass implementation

# we define a function for zooming since keyboard zooming needs a function
def zoom(self, angle: int):
zoomFactor = 1 + angle*0.001 # create adjusted zoom factor
Expand Down Expand Up @@ -132,6 +132,9 @@ def __init__(self, callback):
# Keep track of if editing is allowed
self.locked = False

# Keep track of whether mouse resizing is allowed
self.mouse_resize = False

# Determine the specific directory you want to always open
script_dir = os.path.dirname(os.path.abspath(__file__))
self.save_directory = os.path.join(script_dir, "..", "..", "sinks", "dashboard", "saved-files")
Expand Down Expand Up @@ -204,27 +207,15 @@ def return_fun():
new_action.triggered.connect(create_registry_trigger(i))
self.lockableActions.append(new_action)

# adding a button to the dashboard that removes all dashitems on the screen
remove_dashitems = menubar.addMenu("Clear")
remove_dashitems_action = remove_dashitems.addAction("Remove all the dashitems")
remove_dashitems_action.triggered.connect(self.remove_all)
self.lockableActions.append(remove_dashitems_action)

# adding a button to switch instances of parsley
self.can_selector = menubar.addMenu("Parsley")

# Add an action to the menu bar to lock/unlock
# the dashboard
add_lock_menu = menubar.addMenu("Lock")
lock_action = add_lock_menu.addAction("Lock Dashboard (^l)")
lock_action.triggered.connect(self.lock)
self.lockableActions.append(lock_action)
unlock_action = add_lock_menu.addAction("Unlock Dashboard (^l)")
unlock_action.triggered.connect(self.unlock)
lock_selected = add_lock_menu.addAction("Lock Selected (l)")
editing_menu = menubar.addMenu("Editing")
self.lock_action = editing_menu.addAction("Lock Dashboard (^l)")
self.lock_action.triggered.connect(self.toggle_lock)
lock_selected = editing_menu.addAction("Lock Selected (l)")
lock_selected.triggered.connect(self.lock_selected)
self.lockableActions.append(lock_selected)
self.unlock_items_menu = add_lock_menu.addMenu("Unlock Items")
self.unlock_items_menu = editing_menu.addMenu("Unlock Items")
"""Menu containing actions to unlock items that are locked, in the order
in which they were locked.
"""
Expand All @@ -233,13 +224,21 @@ def return_fun():
were locked.
Includes the rect item and the unlock action."""
self.mouse_resize_action = editing_menu.addAction("Mouse Resizing (^m)")
self.mouse_resize_action.setCheckable(True)
self.mouse_resize_action.setChecked(False)
self.mouse_resize_action.triggered.connect(self.toggle_mouse)
self.lockableActions.append(self.mouse_resize_action)

# An action to the to the menu bar to duplicate
# the selected item
duplicate_item_menu = menubar.addMenu("Duplicate")
duplicate_action = duplicate_item_menu.addAction("Duplicate Item (^d)")
items_menu = menubar.addMenu("Items")
duplicate_action = items_menu.addAction("Duplicate Item (^d)")
duplicate_action.triggered.connect(self.on_duplicate)
self.lockableActions.append(duplicate_action)
remove_action = items_menu.addAction("Remove All (^r)")
remove_action.triggered.connect(self.remove_all)
self.lockableActions.append(remove_action)

# We have a menu in the top to allow users to change the stacking order
# of the selected items.
Expand All @@ -257,6 +256,9 @@ def return_fun():
send_backward_action.triggered.connect(self.send_backward)
self.lockableActions.append(send_backward_action)

# Add a button to switch instances of parsley
self.can_selector = menubar.addMenu("Parsley")

# Add an action to the menu bar to display a
# help box
add_help_menu = menubar.addMenu("Help")
Expand Down Expand Up @@ -303,6 +305,8 @@ def return_fun():
self.key_press_signals.send_backward.connect(self.send_backward)
self.key_press_signals.send_to_front.connect(self.send_to_front)
self.key_press_signals.send_to_back.connect(self.send_to_back)
self.key_press_signals.remove_all.connect(self.remove_all)
self.key_press_signals.mouse_resize.connect(self.toggle_mouse)
self.installEventFilter(self.key_press_signals)

# Data used to check unsaved changes and indicate on the window title
Expand All @@ -329,10 +333,13 @@ def check_for_changes(self):
return False

def change_detector(self):
if self.check_for_changes():
self.setWindowTitle("Omnibus Dashboard ⏺")
else:
self.setWindowTitle("Omnibus Dashboard")
title = self.windowTitle()
unsaved_symbol = "⏺"
changed = self.check_for_changes()
if changed and unsaved_symbol not in title:
self.setWindowTitle(f"{title} {unsaved_symbol}")
elif not changed and unsaved_symbol in title:
self.setWindowTitle(title[:-2])

def every_second(self, payload, stream):
def on_select(string):
Expand Down Expand Up @@ -485,7 +492,18 @@ def remove(self, item):
dashitem.on_delete()

# Method to remove all widgets
def remove_all(self):
def remove_all(self, hide_confirm=False):
if len(self.widgets) == 0:
return

# Make sure the user actually wants to do this
if not hide_confirm:
confirm = ConfirmDialog("Remove All", "Are you sure you want to remove all widgets?")
confirm.exec()
# 0 = rejected, 1 = accepted
if confirm.result() == 0:
return

for item in self.widgets:
self.remove(item)

Expand Down Expand Up @@ -584,50 +602,29 @@ def open(self):
self.file_location = filename
self.load()

# Method to lock dashboard
def lock(self):
self.locked = True
self.setWindowTitle("Omnibus Dashboard - LOCKED")

# Disable menu actions
for menu_item in self.lockableActions:
menu_item.setEnabled(False)

for _rect, action in self.locked_widgets:
action.setEnabled(False)

# Disable selecting and moving plots
for rect in self.widgets:
rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, enabled=False)
rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, enabled=False)

self.scene.clearSelection()

def toggle_lock(self):
"""Toggle lock/unlock state of the dashboard"""
if self.locked:
self.unlock()
else:
self.lock()

# Method to unlock dashboard
def unlock(self):
self.locked = False
self.setWindowTitle("Omnibus Dashboard")
self.locked = not self.locked
title = "Omnibus Dashboard - LOCKED" if self.locked else "Omnibus Dashboard"
self.setWindowTitle(title)

# Enable menu actions
for menu_item in self.lockableActions:
menu_item.setEnabled(True)

menu_item.setEnabled(not self.locked)
for _rect, action in self.locked_widgets:
action.setEnabled(True)
action.setEnabled(not self.locked)

# Enable selecting and moving plots
for rect in self.widgets:
individually_locked = any(rect == pair[0] for pair in self.locked_widgets)
rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, enabled=not individually_locked)
rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, enabled=not individually_locked)

if self.locked:
rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, enabled=False)
rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, enabled=False)
self.lock_action.setText("Unlock Dashboard (^l)")
self.scene.clearSelection()
else:
individually_locked = any(rect == pair[0] for pair in self.locked_widgets)
rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, enabled=not individually_locked)
rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, enabled=not individually_locked)
self.lock_action.setText("Lock Dashboard (^l)")

def lock_widget(self, rect: QGraphicsRectItem):
"""Mark a widget rect as locked."""
rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, enabled=False)
Expand Down Expand Up @@ -665,7 +662,7 @@ def closeEvent(self, event):
new_data = self.get_data()
# Automatically exit if user has clicked "Dont ask again checkbox" or no new changes are made.
if not self.should_show_save_popup or new_data["widgets"] == old_data["widgets"]:
self.remove_all()
self.remove_all(True)
else:
# Execute save popup dialog.
self.show_save_popup(old_data, event)
Expand Down Expand Up @@ -894,6 +891,10 @@ def send_backward(self):
for item in items:
self.scene.addItem(item)

def toggle_mouse(self):
self.mouse_resize = not self.mouse_resize
self.mouse_resize_action.setChecked(self.mouse_resize)

# Function to launch the dashboard
def dashboard_driver(callback):
# quit applicaiton from terminal
Expand Down
28 changes: 18 additions & 10 deletions sinks/dashboard/items/dashboard_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ class DashboardItem(QWidget):

def __init__(self, dashboard, params=None):
super().__init__()
self.dashboard = dashboard
self.setMouseTracking(True)
self.corner_grabbed = False
self.corner_in = False
self.corner_size = self.dynamic_corner_size()
self.corner_index = 3 # 0: left up, 1: right up, 2: left down, 3: right down
self.temp_pos = None # Used to store the temp position of the widget when resizing
self.dashboard = dashboard
self.resize_callback = dashboard.on_item_resize
"""
We use pyqtgraph's ParameterTree functionality to make an easy interface for setting
Expand Down Expand Up @@ -111,14 +111,17 @@ def on_delete(self):
# The following functions are used to make the widget resizable by dragging the bottom right corner.

def dynamic_corner_size(self)-> int | float :
return min(100, max(min(self.width(), self.height())/10,1))
if self.dashboard.mouse_resize:
return min(100, max(min(self.width(), self.height())/10,1))
else:
return 0

def mousePressEvent(self, event):
""" Starts resizing the widget when the mouse is pressed in the bottom right corner.
Notes:
if the mouse is not in the corner, the event is passed to the base class method for normal processing.
"""
if self.corner_hit(event.pos()) and not self.dashboard.locked:
if self.dashboard.mouse_resize and not self.dashboard.locked and self.corner_hit(event.pos()):
# Check item itself isn't locked
for rect, pair in self.dashboard.widgets.items():
if pair[1] == self and rect in [widget[0] for widget in self.dashboard.locked_widgets]:
Expand All @@ -135,8 +138,8 @@ def mouseMoveEvent(self, event): # This function is called when the mouse is mov
"""
When the mouse is move in the corner, the cursor shape is changed to indicate that the widget can be resized
"""
if self.corner_hit(event.pos()):
self.setCursor(Qt.SizeFDiagCursor)
if self.dashboard.mouse_resize and not self.dashboard.locked and self.corner_hit(event.pos()):
self.setCursor(Qt.SizeAllCursor)
self.corner_in = True
else:
self.setCursor(Qt.ArrowCursor)
Expand All @@ -158,17 +161,22 @@ def mouseMoveEvent(self, event): # This function is called when the mouse is mov
self.setGeometry(self.temp_pos.x() + delta.x(), self.pos().y(), new_width, new_height)
elif self.corner_index == 3:
self.setGeometry(self.pos().x(), self.pos().y(), new_width, new_height)
else:
super().mouseMoveEvent(event)

def mouseReleaseEvent(self, event):
"""
Stops resizing the widget when the mouse is released.
"""
# Reset all the states
self.corner_grabbed = False
self.corner_in = False
self.temp_pos = None
# corner_size is updated to be proportional to the widget size
self.corner_size = self.dynamic_corner_size()
if self.dashboard.mouse_resize and not self.dashboard.locked:
self.corner_grabbed = False
self.corner_in = False
self.temp_pos = None
# corner_size is updated to be proportional to the widget size
self.corner_size = self.dynamic_corner_size()
else:
super().mouseReleaseEvent(event)

def corner_hit(self, pos):
"""
Expand Down
3 changes: 0 additions & 3 deletions sinks/dashboard/items/plot_dash_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,3 @@ def get_name():

def on_delete(self):
publisher.unsubscribe_from_all(self.on_data_update)

def dynamic_corner_size(self)-> int | float :
return 0
6 changes: 6 additions & 0 deletions sinks/dashboard/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class EventTracker(QObject):
send_backward = Signal()
send_to_front = Signal()
send_to_back = Signal()
remove_all = Signal()
mouse_resize = Signal()

def eventFilter(self, widget, event):
"""
Expand Down Expand Up @@ -76,6 +78,10 @@ def eventFilter(self, widget, event):
self.send_to_back.emit()
case KeyEvent(Qt.Key_BracketLeft):
self.send_backward.emit()
case KeyEvent(Qt.Key_R, Qt.ControlModifier):
self.remove_all.emit()
case KeyEvent(Qt.Key_M, Qt.ControlModifier):
self.mouse_resize.emit()
return super().eventFilter(widget, event)


Expand Down

0 comments on commit 9c26f76

Please sign in to comment.