Skip to content

Commit

Permalink
added in a pv search widget along with drag and drop to the var_table…
Browse files Browse the repository at this point in the history
… and obj_table.

Added a error message if the search button is clicked before the env is set.
  • Loading branch information
YektaY committed Mar 6, 2025
1 parent 132fc41 commit ed057b0
Show file tree
Hide file tree
Showing 7 changed files with 1,084 additions and 2 deletions.
10 changes: 10 additions & 0 deletions src/badger/built_in_plugins/environments/sphere_2d/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from badger import environment
from PyQt5.QtNetwork import QNetworkRequest
from PyQt5.QtCore import QUrl


class Environment(environment.Environment):
Expand Down Expand Up @@ -33,3 +35,11 @@ def set_variables(self, variable_inputs: dict[str, float]):

def get_observables(self, observable_names):
return {k: self._observations[k] for k in observable_names}

def search(self, keyword, archive_url):
url_string = (
f"{archive_url}" f"retrieval/bpl/searchForPVsRegex?regex=.*{keyword}.*"
)
request = QNetworkRequest(QUrl(url_string))

return request
3 changes: 3 additions & 0 deletions src/badger/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ def set_variable(self, variable_name, variable_value):
def get_bound(self, variable_name):
return self.get_bounds([variable_name])[variable_name]

def search(self, keyword=None, archive_url=None):
return None

############################################################
# Expert level of customization
############################################################
Expand Down
232 changes: 232 additions & 0 deletions src/badger/gui/acr/components/archive_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import logging
from typing import List
from PyQt5.QtGui import QDrag, QKeyEvent
from PyQt5.QtCore import (
QAbstractTableModel,
QMimeData,
QModelIndex,
QObject,
Qt,
QVariant,
pyqtSignal,
)
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
from PyQt5.QtWidgets import (
QAbstractItemView,
QHBoxLayout,
QHeaderView,
QLabel,
QLineEdit,
QPushButton,
QTableView,
QVBoxLayout,
QWidget,
)

logger = logging.getLogger(__name__)


class ArchiveResultsTableModel(QAbstractTableModel):
"""This table model holds the results of an archiver appliance PV search. This search is for names matching
the input search words, and the results are a list of PV names that match that search.
Parameters
----------
parent : QObject, optional
The parent item of this table
"""

def __init__(self, parent: QObject = None) -> None:
super().__init__(parent=parent)

self.results_list = []
self.column_names = ("PV",)

def rowCount(self, parent: QObject) -> int:
"""Return the row count of the table"""
if parent is not None and parent.isValid():
return 0
return len(self.results_list)

def columnCount(self, parent: QObject) -> int:
"""Return the column count of the table"""
if parent is not None and parent.isValid():
return 0
return len(self.column_names)

def data(self, index: QModelIndex, role: int) -> QVariant:
"""Return the data for the associated role. Currently only supporting DisplayRole."""
if not index.isValid():
return QVariant()

if role != Qt.DisplayRole:
return QVariant()

return self.results_list[index.row()]

def headerData(self, section, orientation, role=Qt.DisplayRole) -> QVariant:
"""Return data associated with the header"""
if role != Qt.DisplayRole:
return super().headerData(section, orientation, role)

return str(self.column_names[section])

def flags(self, index: QModelIndex) -> Qt.ItemFlags:
"""Return flags that determine how users can interact with the items in the table"""
if index.isValid():
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled

def append(self, pv: str) -> None:
"""Appends a row to this table given the PV name as input"""
self.beginInsertRows(
QModelIndex(), len(self.results_list), len(self.results_list)
)
self.results_list.append(pv)
self.endInsertRows()
self.layoutChanged.emit()

def replace_rows(self, pvs: List[str]) -> None:
"""Overwrites any existing rows in the table with the input list of PV names"""
self.beginInsertRows(QModelIndex(), 0, len(pvs) - 1)
self.results_list = pvs
self.endInsertRows()
self.layoutChanged.emit()

def clear(self) -> None:
"""Clear out all data stored in this table"""
self.beginRemoveRows(QModelIndex(), 0, len(self.results_list))
self.results_list = []
self.endRemoveRows()
self.layoutChanged.emit()

def sort(self, col: int, order=Qt.AscendingOrder) -> None:
"""Sort the table by PV name"""
self.results_list.sort(reverse=order == Qt.DescendingOrder)
self.layoutChanged.emit()


class ArchiveSearchWidget(QWidget):
"""
The ArchiveSearchWidget is a display widget for showing the results of a PV search using an instance of the
EPICS archiver appliance. Currently the only type of search supported is for PV names matching an input search
string, though this can be extended in the future.
Parameters
----------
parent : QObject, optional
The parent item of this widget
"""

append_PVs_requested = pyqtSignal(str)

def __init__(self, environment, parent: QObject = None) -> None:
super().__init__(parent=parent)
self.env = environment
self.network_manager = QNetworkAccessManager()
self.network_manager.finished.connect(self.populate_results_list)

self.resize(400, 800)
self.layout = QVBoxLayout()

self.archive_title_label = QLabel("Archive URL:")
self.archive_url_textedit = QLineEdit("http://lcls-archapp.slac.stanford.edu/")
self.archive_url_textedit.setFixedWidth(250)
self.archive_url_textedit.setFixedHeight(25)

self.search_label = QLabel("Pattern:")
self.search_box = QLineEdit()
self.search_button = QPushButton("Search")
self.search_button.setDefault(True)
self.search_button.clicked.connect(self.request_archiver_info)

self.loading_label = QLabel("Loading...")
self.loading_label.hide()

self.results_table_model = ArchiveResultsTableModel()
self.results_view = QTableView(self)
self.results_view.setModel(self.results_table_model)
self.results_view.setProperty("showDropIndicator", False)
self.results_view.setDragDropOverwriteMode(False)
self.results_view.setDragEnabled(True)
self.results_view.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.results_view.setSelectionBehavior(QAbstractItemView.SelectRows)
self.results_view.setDropIndicatorShown(True)
self.results_view.setCornerButtonEnabled(False)
self.results_view.setSortingEnabled(True)
self.results_view.verticalHeader().setVisible(False)
self.results_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.results_view.startDrag = self.startDragAction

self.archive_url_layout = QHBoxLayout()
self.archive_url_layout.addWidget(self.archive_title_label)
self.archive_url_layout.addWidget(self.archive_url_textedit)
self.layout.addLayout(self.archive_url_layout)
self.search_layout = QHBoxLayout()
self.search_layout.addWidget(self.search_label)
self.search_layout.addWidget(self.search_box)
self.search_layout.addWidget(self.search_button)
self.layout.addLayout(self.search_layout)
self.layout.addWidget(self.loading_label)
self.layout.addWidget(self.results_view)
# self.insert_button = QPushButton("Add PVs")
# self.insert_button.clicked.connect(
# lambda: self.append_PVs_requested.emit(self.selectedPVs())
# )
self.results_view.doubleClicked.connect(
lambda: self.append_PVs_requested.emit(self.selectedPVs())
)
# self.layout.addWidget(self.insert_button)
self.setLayout(self.layout)

def selectedPVs(self) -> str:
"""Figure out based on which indexes were selected, the list of PVs (by string name)
The user was hoping to insert into the table. Concatenate them into string form i.e.
<pv1>, <pv2>, <pv3>"""
indices = self.results_view.selectedIndexes()
pv_list = ""
for index in indices:
pv_name = self.results_table_model.results_list[index.row()]
pv_list += pv_name + ", "
return pv_list[:-2]

def startDragAction(self, supported_actions) -> None:
"""
The method to be called when a user initiates a drag action for one of the results in the table. The current
reason for this functionality is the ability to drag a PV name onto a plot to automatically start drawing
data for that PV
"""
drag = QDrag(self)
mime_data = QMimeData()
mime_data.setText(self.selectedPVs())
drag.setMimeData(mime_data)
drag.exec_()

def keyPressEvent(self, e: QKeyEvent) -> None:
"""Special key press tracker, just so that if enter or return is pressed the formula dialog attempts to submit the formula"""
if e.key() == Qt.Key_Return or e.key() == Qt.Key_Enter:
self.request_archiver_info()
return super().keyPressEvent(e)

def request_archiver_info(self) -> None:
""" """
search_text = self.search_box.text()
search_text = search_text.replace("?", ".")
request = self.env.search(search_text, self.archive_url_textedit.text())

if request is None:
request = ""

self.network_manager.get(request)
self.loading_label.show()

def populate_results_list(self, reply: QNetworkReply) -> None:
"""Slot called when the archiver appliance returns search results. Will populate the table with the results"""
self.loading_label.hide()
if reply.error() == QNetworkReply.NoError:
self.results_table_model.clear()
bytes_str = reply.readAll()
pv_list = str(bytes_str, "utf-8").split()
self.results_table_model.replace_rows(pv_list)
else:
logger.error(f"Could not retrieve archiver results due to: {reply.error()}")
reply.deleteLater()
12 changes: 10 additions & 2 deletions src/badger/gui/acr/components/env_cbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
)
from PyQt5.QtCore import QRegExp, QPropertyAnimation

from badger.gui.acr.components.archive_search import ArchiveSearchWidget
from badger.gui.default.components.collapsible_box import CollapsibleBox
from badger.gui.default.components.var_table import VariableTable
from badger.gui.default.components.obj_table import ObjectiveTable
from badger.gui.acr.components.var_table import VariableTable
from badger.gui.acr.components.obj_table import ObjectiveTable
from badger.gui.default.components.data_table import init_data_table
from badger.settings import init_settings
from badger.gui.default.utils import (
Expand Down Expand Up @@ -114,6 +115,8 @@ def init_ui(self):
btn_env_play.setFixedSize(128, 24)
if not strtobool(config_singleton.read_value("BADGER_ENABLE_ADVANCED")):
btn_env_play.hide()
self.btn_pv = btn_pv = QPushButton("PV Search")
btn_pv.setFixedSize(128, 24)
self.btn_docs = btn_docs = QPushButton("Open Docs")
btn_docs.setFixedSize(96, 24)
self.btn_params = btn_params = QPushButton("Parameters")
Expand All @@ -125,6 +128,7 @@ def init_ui(self):
hbox_name.addWidget(cb, 1)
hbox_name.addWidget(btn_env_play)
hbox_name.addWidget(btn_params)
hbox_name.addWidget(btn_pv)
hbox_name.addWidget(btn_docs)
vbox.addWidget(name)

Expand Down Expand Up @@ -508,3 +512,7 @@ def update_stylesheets(self, environment=""):
else:
stylesheet = ""
self.setStyleSheet(stylesheet)

def archiveSearchMenu(self):
self.archive_search = ArchiveSearchWidget()
self.archive_search.show()
Loading

0 comments on commit ed057b0

Please sign in to comment.