diff --git a/sinks/dashboard/dashboard.py b/sinks/dashboard/dashboard.py index 6b33986f..0323a930 100644 --- a/sinks/dashboard/dashboard.py +++ b/sinks/dashboard/dashboard.py @@ -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 @@ -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") @@ -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. """ @@ -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. @@ -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") @@ -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 @@ -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): @@ -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) @@ -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) @@ -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) @@ -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 diff --git a/sinks/dashboard/items/dashboard_item.py b/sinks/dashboard/items/dashboard_item.py index 0404db96..ae3ca7a0 100644 --- a/sinks/dashboard/items/dashboard_item.py +++ b/sinks/dashboard/items/dashboard_item.py @@ -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 @@ -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]: @@ -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) @@ -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): """ diff --git a/sinks/dashboard/items/plot_dash_item.py b/sinks/dashboard/items/plot_dash_item.py index a9d8a396..0e6bf8ea 100644 --- a/sinks/dashboard/items/plot_dash_item.py +++ b/sinks/dashboard/items/plot_dash_item.py @@ -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 diff --git a/sinks/dashboard/utils.py b/sinks/dashboard/utils.py index 643eef85..efc21f3d 100644 --- a/sinks/dashboard/utils.py +++ b/sinks/dashboard/utils.py @@ -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): """ @@ -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)