diff --git a/xicam/gui/bluesky/central.py b/xicam/gui/bluesky/central.py index 4b8f0a2..ce43078 100644 --- a/xicam/gui/bluesky/central.py +++ b/xicam/gui/bluesky/central.py @@ -38,6 +38,7 @@ def __init__(self, *args, left_pane.addWidget(self.summary_widget) layout = QHBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) splitter = QSplitter(Qt.Horizontal) layout.addWidget(splitter) splitter.addWidget(left_pane) diff --git a/xicam/gui/bluesky/databroker_catalog_plugin.py b/xicam/gui/bluesky/databroker_catalog_plugin.py index 8c23f74..49e5ee8 100644 --- a/xicam/gui/bluesky/databroker_catalog_plugin.py +++ b/xicam/gui/bluesky/databroker_catalog_plugin.py @@ -52,7 +52,7 @@ def emit_opened_catalogs(name, my_list): # connect the open_entries in the search model to sigOpen self.centralWidget.search_model.open_entries.connect(emit_opened_catalogs) - self.centralWidget.summary_widget.open.connect(emit_opened_catalogs) + self.centralWidget.search_widget.search_results_widget.sigOpen.connect(emit_opened_catalogs) layout.addWidget(self.centralWidget) diff --git a/xicam/gui/bluesky/search.py b/xicam/gui/bluesky/search.py index b9d8807..c9d25d5 100644 --- a/xicam/gui/bluesky/search.py +++ b/xicam/gui/bluesky/search.py @@ -25,15 +25,18 @@ QLabel, QLineEdit, QVBoxLayout, + QFormLayout, QWidget, QTableView, QMenu, + QAction ) from xicam.core import msg from .utils import ConfigurableQObject from .top_utils import load_config, Callable +from xicam.gui.widgets.collapsiblewidget import CollapsiblePanel MAX_SEARCH_RESULTS = 100 # TODO Use fetchMore instead of a hard limit. @@ -335,25 +338,30 @@ def __init__(self, *args, **kwargs): mongo_query_help_button.setText('?') search_bar_layout.addWidget(mongo_query_help_button) mongo_query_help_button.clicked.connect(self.show_mongo_query_help) + self.query_widget = CollapsiblePanel(title='Query', layout=search_bar_layout) + + layout = QFormLayout() + layout.setContentsMargins(0, 0, 0, 0) + date_collapse = CollapsiblePanel(title='Dates', layout=layout) self.since_widget = QDateTimeEdit() self.since_widget.setCalendarPopup(True) self.since_widget.setDisplayFormat('yyyy-MM-dd HH:mm') - since_layout = QHBoxLayout() - since_layout.addWidget(QLabel('Since:')) - since_layout.addWidget(self.since_widget) self.until_widget = QDateTimeEdit() self.until_widget.setCalendarPopup(True) self.until_widget.setDisplayFormat('yyyy-MM-dd HH:mm') - until_layout = QHBoxLayout() - until_layout.addWidget(QLabel('Until:')) - until_layout.addWidget(self.until_widget) + + date_collapse.addRow('Since:', self.since_widget) + date_collapse.addRow('Until:', self.until_widget) + # Show the Dates widget initially uncollapsed + date_collapse.toggleButton.setChecked(True) layout = QVBoxLayout() - layout.addLayout(since_layout) - layout.addLayout(until_layout) - layout.addLayout(search_bar_layout) + # layout.addLayout(since_layout) + # layout.addLayout(until_layout) + layout.addWidget(date_collapse) + layout.addWidget(self.query_widget) self.setLayout(layout) def mark_custom_query(self, valid): @@ -405,6 +413,8 @@ class SearchResultsWidget(QTableView): """ Table of search results """ + sigOpen = Signal(str, list) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setEditTriggers(QAbstractItemView.NoEditTriggers) @@ -416,6 +426,24 @@ def __init__(self, *args, **kwargs): self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.setAlternatingRowColors(True) + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.menuRequested) + self.menu = QMenu() + standardActions = [ + QAction("Open", self), + ] + self.menu.addActions(standardActions) + standardActions[0].triggered.connect(self.open) + + def menuRequested(self, position): + self.menu.exec_(self.viewport().mapToGlobal(position)) + + def open(self): + uids = [self.model().itemFromIndex(index).data(Qt.DisplayRole) for index in + self.selectionModel().selectedRows()] + runs = [self.model().search_state._results_catalog[uid] for uid in uids] + self.sigOpen.emit("blah?", runs) + def hide_hidden_columns(self): hidden_columns = QSettings().value("catalog.columns.hidden") or set() header = self.horizontalHeader() diff --git a/xicam/gui/bluesky/summary.py b/xicam/gui/bluesky/summary.py index a27751d..ac766b2 100644 --- a/xicam/gui/bluesky/summary.py +++ b/xicam/gui/bluesky/summary.py @@ -11,14 +11,11 @@ class SummaryWidget(QWidget): - open = Signal([str, list]) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.uid_label = QLabel() - self.open_individually_button = QPushButton('Open') - self.open_individually_button.setEnabled(False) - self.open_individually_button.clicked.connect(self._open_individually) + self.copy_uid_button = QPushButton('Copy UID to Clipboard') self.copy_uid_button.setEnabled(False) self.copy_uid_button.clicked.connect(self._copy_uid) @@ -29,7 +26,6 @@ def __init__(self, *args, **kwargs): uid_layout.addWidget(self.uid_label) uid_layout.addWidget(self.copy_uid_button) layout = QVBoxLayout() - layout.addWidget(self.open_individually_button) layout.addLayout(uid_layout) layout.addWidget(self.streams) self.setLayout(layout) @@ -42,10 +38,6 @@ def cache_tab_titles(self, titles): def _copy_uid(self): QApplication.clipboard().setText(self.uid) - def _open_individually(self): - for entry in self.entries: - self.open.emit(None, [entry]) - def set_entries(self, entries): self.entries.clear() @@ -54,15 +46,12 @@ def set_entries(self, entries): self.uid_label.setText('') self.streams.setText('') self.copy_uid_button.setEnabled(False) - self.open_individually_button.setEnabled(False) elif len(entries) == 1: entry, = entries run = entry() self.uid = run.metadata['start']['uid'] self.uid_label.setText(self.uid[:8]) self.copy_uid_button.setEnabled(True) - self.open_individually_button.setEnabled(True) - self.open_individually_button.setText('Open') num_events = (run.metadata['stop'] or {}).get('num_events') if num_events: self.streams.setText( @@ -76,6 +65,4 @@ def set_entries(self, entries): else: self.uid_label.setText('(Multiple Selected)') self.streams.setText('') - self.copy_uid_button.setEnabled(False) - self.open_individually_button.setText('Open individually') - self.open_individually_button.setEnabled(True) + self.copy_uid_button.setEnabled(False) \ No newline at end of file diff --git a/xicam/gui/widgets/collapsiblewidget.py b/xicam/gui/widgets/collapsiblewidget.py index 33a8357..ade712b 100644 --- a/xicam/gui/widgets/collapsiblewidget.py +++ b/xicam/gui/widgets/collapsiblewidget.py @@ -1,5 +1,6 @@ -from qtpy.QtCore import Qt, Signal -from qtpy.QtWidgets import QGridLayout, QSplitter, QToolBar, QWidget +from qtpy.QtCore import Signal, QPropertyAnimation, Qt, QParallelAnimationGroup, QAbstractAnimation +from qtpy.QtWidgets import QSplitter, QToolBar, QWidget, QFrame, QScrollArea, QToolButton, QGridLayout, QSizePolicy, \ + QLabel, QVBoxLayout, QFormLayout # TODO: this could be more generic, defining a collapsible interface/mixin type class @@ -83,3 +84,95 @@ def collapse(self, collapsed): else: sizes = [self.widgetSize, self.splitter.sizes()[1] - self.widgetSize] self.splitter.setSizes(sizes) + + +class CollapsiblePanel(QWidget): + def __init__(self, parent=None, title='', layout=None, animationDuration=100): + """ + References: + # Adapted from c++ version + http://stackoverflow.com/questions/32476006/how-to-make-an-expandable-collapsable-section-widget-in-qt + """ + super(CollapsiblePanel, self).__init__(parent=parent) + + self.animationDuration = animationDuration + self.toggleAnimation = QParallelAnimationGroup() + self.contentArea = QWidget() + self.headerLine = QFrame() + self.toggleButton = QToolButton() + self.mainLayout = QGridLayout() + + toggleButton = self.toggleButton + toggleButton.setStyleSheet("QToolButton { border: none; }") + toggleButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + toggleButton.setArrowType(Qt.RightArrow) + toggleButton.setText(str(title)) + toggleButton.setCheckable(True) + toggleButton.setChecked(False) + + headerLine = self.headerLine + headerLine.setFrameShape(QFrame.HLine) + headerLine.setFrameShadow(QFrame.Sunken) + headerLine.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) + + # self.contentArea.setStyleSheet("QScrollArea { background-color: white; border: none; }") + self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) + self.contentArea.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) + # start out collapsed + self.contentArea.setMaximumHeight(0) + self.contentArea.setMinimumHeight(0) + # let the entire widget grow and shrink with its content + toggleAnimation = self.toggleAnimation + toggleAnimation.addAnimation(QPropertyAnimation(self, b"minimumHeight")) + toggleAnimation.addAnimation(QPropertyAnimation(self, b"maximumHeight")) + toggleAnimation.addAnimation(QPropertyAnimation(self.contentArea, b"minimumHeight")) + toggleAnimation.addAnimation(QPropertyAnimation(self.contentArea, b"maximumHeight")) + # don't waste space + mainLayout = self.mainLayout + mainLayout.setVerticalSpacing(0) + mainLayout.setContentsMargins(0, 0, 0, 0) + mainLayout.addWidget(self.toggleButton, 0, 0, 1, 1, Qt.AlignLeft) + mainLayout.addWidget(self.headerLine, 0, 2, 1, 1, Qt.AlignVCenter) + mainLayout.addWidget(self.contentArea, 1, 0, 1, 3) + self.setLayout(self.mainLayout) + + # Initialize contentLayout + self.content_layout = None + self.setContentLayout(layout if layout is not None else QVBoxLayout()) + + def start_animation(checked): + collapsedHeight = self.sizeHint().height() - self.contentArea.maximumHeight() + contentHeight = self.content_layout.sizeHint().height() + for i in [0, 1]: + spoilerAnimation = self.toggleAnimation.animationAt(i) + spoilerAnimation.setDuration(self.animationDuration) + spoilerAnimation.setStartValue(collapsedHeight) + spoilerAnimation.setEndValue(collapsedHeight + contentHeight) + for i in [2, 3]: + contentAnimation = self.toggleAnimation.animationAt(i) + contentAnimation.setDuration(self.animationDuration) + contentAnimation.setStartValue(0) + contentAnimation.setEndValue(contentHeight) + + arrow_type = Qt.DownArrow if checked else Qt.RightArrow + direction = QAbstractAnimation.Forward if checked else QAbstractAnimation.Backward + toggleButton.setArrowType(arrow_type) + self.toggleAnimation.setDirection(direction) + self.toggleAnimation.start() + + self.toggleButton.toggled.connect(start_animation) + + def setContentLayout(self, contentLayout): + # Not sure if this is equivalent to self.contentArea.destroy() + # self.contentArea.destroy() + self.content_layout = contentLayout + self.contentArea.setLayout(contentLayout) + + def addWidget(self, widget): + self.content_layout.addWidget(widget) + + def addRow(self, *args, **kwargs): + if not isinstance(self.contentArea.layout(), QFormLayout): + raise AttributeError('The assigned layout is not QFormLayout, however .addRow was used') + else: + self.content_layout.addRow(*args, **kwargs)