diff --git a/PyMca5/PyMcaCore/QTiledDataSource.py b/PyMca5/PyMcaCore/QTiledDataSource.py new file mode 100644 index 000000000..8177d196d --- /dev/null +++ b/PyMca5/PyMcaCore/QTiledDataSource.py @@ -0,0 +1,158 @@ +import numpy as np +import time +import sys + +from PyMca5.PyMcaCore import DataObject +from PyMca5.PyMcaGui.io.TiledDataChannelTable import TiledDataChannelTable +from PyMca5.PyMcaGui.io.QTiledWidget import TiledBrowser +from PyMca5.PyMcaGui.pymca import QSource + + +SOURCE_TYPE = 'Tiled' + +class QTiledDataSource(QSource.QSource): + """ + Creates instance of a Tiled Data Source class. This is neccesary + to create a Tiled Tab in the QDispatcher, which houses the Tiled + Browser. + + This is largely based on the NexusDataSource class, but all Data + Source tabs (Spec, EDF, SPS) have an analogous class. + + See QDataSource.py + """ + + def __init__(self, nameInput): + print("-------- QTiledDataSource init") + super().__init__() + if isinstance(nameInput, list): + nameList = nameInput + else: + nameList = [nameInput] + self.sourceName = nameList + self.sourceType = SOURCE_TYPE + self.__sourceNameList = self.sourceName + self.refresh() + + def refresh(self): + pass + + def getDataObject(self, key_list, selection=None, poll=False): + if type(key_list) not in [type([])]: + nolist = True + key_list = [key_list] + else: + output = [] + nolist = False + data = self.get_data_object(key_list, selection=selection) + + return data + + def _set_key(self, selection=None): + """Sets key once a scan has been selected in Tiled Browser.""" + + key = { + "scan": selection.metadata['start']['uid'], + "scan_id": selection.metadata['start']['scan_id'], + "streams": list(selection), + "selection": selection, + } + + return key + + def _set_data_channel_selection(self): + """Retrieve Data Channel Selections from Tiled Data Channel Table.""" + print("-------- QTiledDataSource _set_data_channel_selection") + channel_sel = TiledDataChannelTable.getChannelSelection() + self.chan_sel = { + 'x': channel_sel['x'], + 'y': channel_sel['y'], + 'm': channel_sel['m'], + 'Channel List': channel_sel['Data Channel List'], + } + + def _get_key_info(self, selection): + """Retrives key info.""" + + key = self._set_key(selection=selection) + key_info = { + "SourceType": SOURCE_TYPE, + "selection": selection, + "key": key, + } + + return key_info + + def get_data_object(self, key, selection=None): + """Generate a dataObject that will be used to plot scan data.""" + print("-------- QTiledDataSource get_data_object") + dataObject = DataObject.DataObject() + dataObject.info = self._get_key_info(selection) + dataObject.data = key['selection'] + + chan_sel = self.chan_sel + + # What data.info attributes to add? + dataObject.info['selection'] = selection + + # data = [key['selection']][datachannel][data] + # If data channel selected in x axis go to data and time + dataObject.data.x = dataObject.data.chan_sel['x']['data']['time'] + # If data channel selected in y axis plot everything + dataObject.data.y = dataObject.data.chan_sel['y']['data'] + # If data selected in m divide y by m and plot + dataObject.data.m = dataObject.data.chan_sel['m']['data'] + + return dataObject + + def isUpdated(self,key): + pass + + +def _is_Tiled_Source(filename): + try: + if hasattr(filename, self.node_path): + return True + except Exception: + return False + +source_types = {SOURCE_TYPE: QTiledDataSource} + +def DataSource(name="", source_type=SOURCE_TYPE): + try: + sourceClass = source_types[source_type] + except KeyError: + #ERROR invalid source type + raise TypeError("Invalid Source Type, source type should be one of %s" %\ + source_types.keys()) + return sourceClass(name) + + +if __name__ == "__main__": + try: + sourcename = sys.argv[1] + key = sys.argv[2] + except Exception: + print("Usage: QTiledDataSource ") + sys.exit() + #one can use this: + obj = QTiledDataSource(sourcename) + #or this: + obj = DataSource(sourcename) + #data = obj.getData(key,selection={'pos':(10,10),'size':(40,40)}) + #data = obj.getDataObject(key,selection={'pos':None,'size':None}) + t0 = time.time() + data = obj.getDataObject(key,selection=None) + print("elapsed = ",time.time() - t0) + print("info = ",data.info) + if data.data is not None: + print("data shape = ",data.data.shape) + print(np.ravel(data.data)[0:10]) + else: + print(data.y[0].shape) + print(np.ravel(data.y[0])[0:10]) + data = obj.getDataObject('1.1',selection=None) + r = int(key.split('.')[-1]) + print(" data[%d,0:10] = " % (r-1),data.data[r-1 ,0:10]) + print(" data[0:10,%d] = " % (r-1),data.data[0:10, r-1]) + \ No newline at end of file diff --git a/PyMca5/PyMcaGui/io/QSourceSelector.py b/PyMca5/PyMcaGui/io/QSourceSelector.py index 0f834b06b..3e7a4d726 100644 --- a/PyMca5/PyMcaGui/io/QSourceSelector.py +++ b/PyMca5/PyMcaGui/io/QSourceSelector.py @@ -86,6 +86,7 @@ def __init__(self, parent=None, filetypelist=None, pluginsIcon=False): self.specIcon = qt.QIcon(qt.QPixmap(icons.IconDict["bliss"])) else: self.specIcon = qt.QIcon(qt.QPixmap(icons.IconDict["spec"])) + self.tiledIcon = qt.QIcon(qt.QPixmap(icons.IconDict["bluesky"])) openButton.setIcon(self.openIcon) openButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Minimum)) @@ -106,8 +107,13 @@ def __init__(self, parent=None, filetypelist=None, pluginsIcon=False): else: specButton.setToolTip("Open new shared memory source") + tiledButton = qt.QPushButton(self.fileWidget) + tiledButton.setIcon(self.tiledIcon) + tiledButton.setToolTip("Open tiled data source") + closeButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Minimum)) specButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Minimum)) + tiledButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Minimum)) refreshButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Minimum)) openButton.clicked.connect(self._openFileSlot) @@ -121,10 +127,13 @@ def __init__(self, parent=None, filetypelist=None, pluginsIcon=False): _logger.debug("Using deprecated signal") self.fileCombo.activated[str].connect(self._fileSelection) + tiledButton.clicked.connect(self.tiledConnection) + fileWidgetLayout.addWidget(self.fileCombo) fileWidgetLayout.addWidget(openButton) fileWidgetLayout.addWidget(closeButton) fileWidgetLayout.addWidget(specButton) + fileWidgetLayout.addWidget(tiledButton) if sys.platform == "win32":specButton.hide() fileWidgetLayout.addWidget(refreshButton) self.specButton = specButton @@ -331,6 +340,17 @@ def openSpec(self): lambda i=spec:self.openFile(i, specsession=True)) menu.exec(self.cursor().pos()) + def tiledConnection(self): + """When 'bluesky' icon clicked it opens the TiledBrowser in the Tiled Tab""" + # ddict = {"event": "Open Tiled Tab"} + url = "https://tiled-demo.blueskyproject.io/api" + ddict = { + "event": "NewSourceSelected", + "sourcelist": url, + } + self.sigSourceSelectorSignal.emit(ddict) + + # Potentially add a authorization window when clicked def _fileSelection(self, qstring): _logger.debug("file selected %s", qstring) diff --git a/PyMca5/PyMcaGui/io/QTiledWidget.py b/PyMca5/PyMcaGui/io/QTiledWidget.py new file mode 100644 index 000000000..e3ea2aafe --- /dev/null +++ b/PyMca5/PyMcaGui/io/QTiledWidget.py @@ -0,0 +1,759 @@ +""" +This module is an example of a barebones QWidget plugin for PyMca + +Replace code below according to your needs. +""" +import collections +from datetime import date, datetime +import functools +import json + +from qtpy.QtCore import Qt, Signal +from qtpy.QtGui import QIcon, QPixmap +from qtpy.QtWidgets import ( + QAbstractItemView, + QComboBox, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QSplitter, + QStyle, + QTableWidget, + QTableWidgetItem, + QTextEdit, + QVBoxLayout, + QWidget, +) +from PyMca5.PyMcaGui import PyMcaQt as qt +from PyMca5.PyMcaGui.io import TiledDataChannelTable, QSourceSelector + +from tiled.client import from_uri +from tiled.client.array import DaskArrayClient +from tiled.client.container import Container +from tiled.structures.core import StructureFamily + +def json_decode(obj): + if isinstance(obj, (datetime, date)): + return obj.isoformat() + return str(obj) + + +class DummyClient: + "Placeholder for a structure family we cannot (yet) handle" + + def __init__(self, *args, item, **kwargs): + self.item = item + + +STRUCTURE_CLIENTS = collections.defaultdict(lambda: DummyClient) +STRUCTURE_CLIENTS.update({"array": DaskArrayClient, "container": Container}) + + +class TiledBrowser(qt.QMainWindow): + NODE_ID_MAXLEN = 8 + SUPPORTED_TYPES = (StructureFamily.array, StructureFamily.container) + + # Added to have 'Tiled' tab appear in QDispatcher.py + sigAddSelection = qt.pyqtSignal(object) + sigRemoveSelection = qt.pyqtSignal(object) + sigReplaceSelection = qt.pyqtSignal(object) + sigOtherSignals = qt.pyqtSignal(object) + + def __init__(self, parent=None): + super().__init__() + + self.set_root(None) + + # Connection elements + self.url_entry = QLineEdit() + self.url_entry.setPlaceholderText("Enter a url") + self.connect_button = QPushButton("Connect") + self.connection_label = QLabel("No url connected") + self.connection_widget = QWidget() + + # Connection layout + connection_layout = QVBoxLayout() + connection_layout.addWidget(self.url_entry) + connection_layout.addWidget(self.connect_button) + connection_layout.addWidget(self.connection_label) + connection_layout.addStretch() + self.connection_widget.setLayout(connection_layout) + + # Search By elements + searchBy_tuple = ('Plan Name', 'Plan Type', 'Scan ID', 'uid') + + self.search_dropdown = QComboBox() + self.search_dropdown.addItems(searchBy_tuple) + self.search_dropdown.currentTextChanged.connect(self._set_search_by) + self.search_label = QLabel("Search By") + self.search_entry = QLineEdit() + self.search_entry.setPlaceholderText("Enter Search") + self.search_entry.textChanged.connect(self._search_text_changed) + self.search_drop_widget = QWidget() + self.search_widget = QWidget() + + # Search Drop Down + search_dropdown_layout = QHBoxLayout() + search_dropdown_layout.addWidget(self.search_dropdown) + search_dropdown_layout.addWidget(self.search_label) + search_dropdown_layout.setContentsMargins(0, 0, 0, 0) + search_dropdown_layout.setSpacing(5) + self.search_drop_widget.setLayout(search_dropdown_layout) + + # Larger Search Widget + search_layout = QVBoxLayout() + search_layout.addWidget(self.search_drop_widget) + search_layout.addWidget(self.search_entry) + search_layout.setContentsMargins(0, 0, 0, 0) + search_layout.setSpacing(10) + self.search_widget.setLayout(search_layout) + self.search_widget.setVisible(False) + + # Navigation elements + self.rows_per_page_label = QLabel("Rows per page: ") + self.rows_per_page_selector = QComboBox() + self.rows_per_page_selector.addItems(["5", "10", "25"]) + self.rows_per_page_selector.setCurrentIndex(0) + + self.current_location_label = QLabel() + self.first_page = ClickableQLabel("<<") + self.previous_page = ClickableQLabel("<") + self.next_page = ClickableQLabel(">") + self.last_page = ClickableQLabel(">>") + self.navigation_widget = QWidget() + + self._rows_per_page = int( + self.rows_per_page_selector.currentText() + ) + + # Navigation layout + navigation_layout = QHBoxLayout() + navigation_layout.addWidget(self.rows_per_page_label) + navigation_layout.addWidget(self.rows_per_page_selector) + navigation_layout.addWidget(self.current_location_label) + navigation_layout.addWidget(self.first_page) + navigation_layout.addWidget(self.previous_page) + navigation_layout.addWidget(self.next_page) + navigation_layout.addWidget(self.last_page) + self.navigation_widget.setLayout(navigation_layout) + + # Current path layout + self.current_path_layout = QHBoxLayout() + self.current_path_layout.setSpacing(10) + self.current_path_layout.setAlignment(Qt.AlignLeft) + self._rebuild_current_path_layout() + + # Catalog table elements + self.catalog_table = QTableWidget(0, 1) + self.catalog_table.horizontalHeader().setStretchLastSection(True) + self.catalog_table.setEditTriggers( + QTableWidget.EditTrigger.NoEditTriggers + ) # disable editing + self.catalog_table.horizontalHeader().hide() # remove header + self.catalog_table.setSelectionMode( + QAbstractItemView.SelectionMode.SingleSelection + ) # disable multi-select + # disabled due to bad colour palette: + # self.catalog_table.setAlternatingRowColors(True) + self.catalog_table.itemDoubleClicked.connect( + self._on_item_double_click + ) + self.catalog_table.itemSelectionChanged.connect(self._on_item_selected) + self.catalog_table_widget = QWidget() + self.catalog_breadcrumbs = None + + # Info layout + self.info_box = QTextEdit() + self.info_box.setReadOnly(True) + self.load_button = QPushButton("Open") + self.load_button.setEnabled(False) + self.load_button.clicked.connect(self._on_load) + catalog_info_layout = QHBoxLayout() + catalog_info_layout.addWidget(self.catalog_table) + load_layout = QVBoxLayout() + load_layout.addWidget(self.info_box) + load_layout.addWidget(self.load_button) + catalog_info_layout.addLayout(load_layout) + + # Catalog table layout + catalog_table_layout = QVBoxLayout() + catalog_table_layout.addLayout( self.current_path_layout ) + catalog_table_layout.addLayout(catalog_info_layout) + catalog_table_layout.addWidget(self.navigation_widget) + catalog_table_layout.addStretch(1) + self.catalog_table_widget.setLayout(catalog_table_layout) + self.catalog_table_widget.setVisible(False) + + # Data Channels Table + self.data_channels_table = TiledDataChannelTable.TiledDataChannelTable() + self.data_channels_table.setVisible(False) + + # Command Button Elements + self.buttonWidget = qt.QWidget(self) + self.buttonWidget.setSizePolicy(qt.QSizePolicy.Minimum, + qt.QSizePolicy.Minimum) + addButton = qt.QPushButton("ADD", self.buttonWidget) + removeButton = qt.QPushButton("REMOVE", self.buttonWidget) + replaceButton = qt.QPushButton("REPLACE", self.buttonWidget) + + # Command Buttons Layout + buttonLayout = qt.QHBoxLayout(self.buttonWidget) + buttonLayout.addWidget(addButton) + buttonLayout.addWidget(removeButton) + buttonLayout.addWidget(replaceButton) + buttonLayout.setContentsMargins(5, 5, 5, 5) + self.buttonWidget.setVisible(False) + + # Command Buttons Connections + addButton.clicked.connect(self._addClicked) + replaceButton.clicked.connect(self._replaceClicked) + removeButton.clicked.connect(self._removeClicked) + + self.splitter = QSplitter(self) + self.splitter.setOrientation(Qt.Orientation.Vertical) + + self.splitter.addWidget(self.connection_widget) + self.splitter.addWidget(self.search_widget) + self.splitter.addWidget(self.catalog_table_widget) + self.splitter.addWidget(self.data_channels_table) + self.splitter.addWidget(self.buttonWidget) + + # Set stretch factors for widgets + self.splitter.setStretchFactor(0, 1) # Strech factor for Connection Widget + self.splitter.setStretchFactor(1, 1) # Strech factor for Search Widget + self.splitter.setStretchFactor(2, 3) # Strech factor for Catalog Table + self.splitter.setStretchFactor(3, 4) # Strech factor for Data Channel Table + self.splitter.setStretchFactor(4, 1) # Strech factor for the Command Buttons + + browser_layout = QVBoxLayout() + browser_layout.addWidget(self.splitter) + + layout = QHBoxLayout() + layout.addLayout( browser_layout ) + + centralWidget = qt.QWidget(self) + centralWidget.setLayout(layout) + self.setCentralWidget(centralWidget) + + self.connect_button.clicked.connect(self._on_connect_clicked) + self.previous_page.clicked.connect(self._on_prev_page_clicked) + self.next_page.clicked.connect(self._on_next_page_clicked) + self.first_page.clicked.connect(self._on_first_page_clicked) + self.last_page.clicked.connect(self._on_last_page_clicked) + + self.rows_per_page_selector.currentTextChanged.connect( + self._on_rows_per_page_changed + ) + + # Default values + self.searchBy_selection = 'uid' + self.data = None + self.previous_search_text = '' + self.key_to_uid = {} + + self.selection = None + + def _on_connect_clicked(self): + url = self.url_entry.displayText().strip() + # url = "https://tiled-demo.blueskyproject.io/api" + if not url: + print("Please specify a url.") + return + try: + root = from_uri(url, STRUCTURE_CLIENTS) + if isinstance(root, DummyClient): + print("Unsupported tiled type detected") + except Exception: + print("Could not connect. Please check the url.") + else: + self.connection_label.setText(f"Connected to {url}") + self.set_root(root) + # sigSourceSelectorSignal.emit( + # { + # "event": "NewSourceSelected", + # "sourcelist": url, + # } + # ) + + def setDataSource(self, data): + self.data = data + # self.data.sigUpdated.connect(self._update) + selection = self.set_data_source_key() + + if selection is not None: + dataObject = self._getDataObject(selection=selection) + # self.graphWidget.setImageData(dataObject.data) + self.lastDataObject = dataObject + + def _update(self, ddict): + # targetwidgetid = ddict.get('targetwidgetid', None) + # if targetwidgetid not in [None, id(self)]: + # return + # dataObject = self._getDataObject(ddict['Key'], + # selection=None) + # if dataObject is not None: + # self.graphWidget.setImageData(dataObject.data) + # self.lastDataObject = dataObject + ... + + def _getDataObject(self, key=None, selection=None): + if key is None: + # key = self.info['Key'] + print('deal with later') + dataObject = self.data.getDataObject(key, + selection=None, + poll=False) + # if dataObject is not None: + # dataObject.info['legend'] = self.info['Key'] + # dataObject.info['imageselection'] = False + # dataObject.info['scanselection'] = False + # dataObject.info['targetwidgetid'] = id(self) + # self.data.addToPoller(dataObject) + return dataObject + + def setData(self, node): + self.data = node + self.refreshData() + + def refreshData(self): + pass + + def clearData(self): + self.data = None + + def set_data_source_key(self): + if 'raw' in self.node_path and 'raw' != self.node_path[-1]: + self.selection = self.root[self.node_path] + return self.selection + + def set_root(self, root): + self.root = root + self.node_path = () + self._current_page = 0 + if root is not None: + self.search_widget.setVisible(True) + self.catalog_table_widget.setVisible(True) + self.data_channels_table.setVisible(True) + self.buttonWidget.setVisible(True) + self._rebuild() + + def get_current_node(self): + return self.get_node(self.node_path) + + @functools.lru_cache(maxsize=1) + def get_node(self, node_path): + if node_path: + return self.root[node_path] + + return self.root + + def enter_node(self, node_id): + print(f"{self.node_path = }") + print(f"{node_id = }") + self.node_path += (node_id,) + self._current_page = 0 + self._rebuild() + # avoid populating data channels table if not in a scan + if 'raw' in self.node_path and self.node_path[-1] != 'raw': + rawDataChannels = tuple(self.get_current_node().items()) + dataChannels = [channel[0] for channel in rawDataChannels] + self.data_channels_table.build_table(dataChannels) + + def exit_node(self): + self.node_path = self.node_path[:-1] + self._current_page = 0 + self._rebuild() + + def open_node(self, node_id): + # This allows another scan to be selected after one is already clicked on. + if 'raw' in self.node_path and 'raw' != self.node_path[-1]: + self.node_path = self.node_path[:-1] + + node = self.get_current_node()[node_id] + family = node.item["attributes"]["structure_family"] + if isinstance(node, DummyClient): + print(f"Cannot open type: '{family}'") + return + if family == StructureFamily.array: + # TODO: find a way set data to self.data + self.setData(node) + elif family == StructureFamily.container: + self.enter_node(node_id) + else: + print(f"Type not supported:'{family}") + + def _on_load(self): + selected = self.catalog_table.selectedItems() + if not selected: + return + item = selected[0] + if item is self.catalog_breadcrumbs: + return + self.open_node(item.text()) + + def _on_rows_per_page_changed(self, value): + # If scan already selected + # if 'raw' in self.node_path and 'raw' != self.node_path[-1]: + # self.node_path = self.node_path[:-1] + + self._rows_per_page = int(value) + self._current_page = 0 + self._rebuild_table() + self._set_current_location_label() + + def _on_item_double_click(self, item): + if item is self.catalog_breadcrumbs: + self.exit_node() + return + + try: + self.open_node(item.text()) + + except KeyError: + uid = str(self.key_to_uid.get(item.text())) + self.open_node(uid) + + def _on_item_selected(self): + selected = self.catalog_table.selectedItems() + if not selected or (item := selected[0]) is self.catalog_breadcrumbs: + self._clear_metadata() + return + + name = item.text() + + try: + node_path = self.node_path + (name,) + node = self.get_node(node_path) + + except KeyError: + try: + uid = str(self.key_to_uid.get(name)) + node_path = self.node_path + (uid,) + node = self.get_node(node_path) + + except KeyError: + try: + node_path = self._replace_last_node(name) + node = self.get_node(node_path) + + except KeyError: + uid = str(self.key_to_uid.get(name)) + node_path = self._replace_last_node(uid) + node = self.get_node(node_path) + + attrs = node.item["attributes"] + family = attrs["structure_family"] + metadata = json.dumps(attrs["metadata"], indent=2, default=json_decode) + + info = f"type: {family}
" + if family == StructureFamily.array: + shape = attrs["structure"]["shape"] + info += f"shape: {tuple(shape)}
" + info += f"metadata: {metadata}" + self.info_box.setText(info) + + if family in self.SUPPORTED_TYPES: + self.load_button.setEnabled(True) + else: + self.load_button.setEnabled(False) + + def _clear_metadata(self): + self.info_box.setText("") + self.load_button.setEnabled(False) + + def _on_breadcrumb_clicked(self, node): + # If root is selected. + if node == "root": + self.node_path = () + self._rebuild() + + # For any node other than root. + else: + try: + index = self.node_path.index(node) + self.node_path = self.node_path[:index + 1] + self._rebuild() + + # If node ID has been truncated. + except ValueError: + for i, node_id in enumerate(self.node_path): + if node == node_id[:self.NODE_ID_MAXLEN - 3] + "...": + index = i + break + + self.node_path = self.node_path[:index + 1] + self._rebuild() + + def _clear_current_path_layout(self): + for i in reversed(range(self.current_path_layout.count())): + widget = self.current_path_layout.itemAt(i).widget() + self.current_path_layout.removeWidget(widget) + widget.deleteLater() + + def _rebuild_current_path_layout(self): + # Add root to widget list. + root = ClickableQLabel("root") + root.clicked_with_text.connect(self._on_breadcrumb_clicked) + widgets = [root] + + # Appropriately shorten node_id. + for node_id in self.node_path: + if len(node_id) > self.NODE_ID_MAXLEN: + node_id = node_id[: self.NODE_ID_MAXLEN - 3] + "..." + + # Convert node_id into a ClickableQWidget and add to widget list. + clickable_label = ClickableQLabel(node_id) + clickable_label.clicked_with_text.connect(self._on_breadcrumb_clicked) + widgets.append(clickable_label) + + # Add nodes to node path. + if len(self.current_path_layout) < len(widgets): + for widget in widgets: + widget = widgets[-1] + self.current_path_layout.addWidget(widget) + + # Remove nodes from node path after they are left. + elif len(self.current_path_layout) > len(widgets): + self._clear_current_path_layout() + while len(self.current_path_layout) < len(widgets): + for widget in widgets: + self.current_path_layout.addWidget(widget) + + def _rebuild_table(self): + prev_block = self.catalog_table.blockSignals(True) + # Remove all rows first + while self.catalog_table.rowCount() > 0: + self.catalog_table.removeRow(0) + + if self.node_path: + # add breadcrumbs + self.catalog_breadcrumbs = QTableWidgetItem("..") + self.catalog_table.insertRow(0) + self.catalog_table.setItem(0, 0, self.catalog_breadcrumbs) + + # All key value pairs for current node + all_items = self.get_current_node().items() + + # Changed keys: + changed_keys_values = [] + + # Change key metadata if applicable + for key, value in all_items: + if self.searchBy_selection != 'uid' and 'raw' in self.node_path: + searchBy_key = self._get_search_by() + metadata_path = value.item["attributes"]["metadata"]["start"][searchBy_key] + uid = value.item["attributes"]["metadata"]["start"]["uid"] + key = str(metadata_path) + self.key_to_uid[key] = uid + + user_entry = self.search_entry.text() + # If there is an entry in the search bar + if user_entry: + if self._filter_row_in_table(key): + changed_keys_values.append((key, value)) + else: + pass + # If there is no entry in the search bar + else: + changed_keys_values.append((key, value)) + + # If there is no Search By selection + else: + user_entry = self.search_entry.text() + # If there is an entry in the search bar + if user_entry and 'raw' in self.node_path: + if self._filter_row_in_table(key): + changed_keys_values.append((key, value)) + else: + pass + # If there is no entry in the search bar + else: + changed_keys_values.append((key, value)) + + self.nrows_catalog_table = len(changed_keys_values) + self._set_current_location_label() + + node_offset = self._rows_per_page * self._current_page + # Fetch a page of keys. + items = changed_keys_values[ + node_offset : node_offset + self._rows_per_page + ] + + # Loop over rows, filling in keys until we run out of keys. + start = 1 if self.node_path else 0 + row_index = start + for key, value in items: + family = value.item["attributes"]["structure_family"] + if family == StructureFamily.container: + icon = self.style().standardIcon(QStyle.SP_DirHomeIcon) + elif family == StructureFamily.array: + icon = self.style().standardIcon( + QStyle.SP_FileIcon + ) + else: + icon = self.style().standardIcon( + QStyle.SP_TitleBarContextHelpButton + ) + + self.catalog_table.insertRow(row_index) + self.catalog_table.setItem(row_index, 0, QTableWidgetItem(icon, key)) + row_index += 1 + + # remove extra rows + while row_index < self.catalog_table.rowCount(): + self.catalog_table.removeRow(self.catalog_table.rowCount() - 1) + + headers = [ + str(x + 1) + for x in range( + node_offset, node_offset + self.catalog_table.rowCount() + ) + ] + if self.node_path: + headers = [""] + headers + + self.catalog_table.setVerticalHeaderLabels(headers) + self._clear_metadata() + self.catalog_table.blockSignals(prev_block) + + def _rebuild(self): + self._rebuild_table() + self._rebuild_current_path_layout() + self._set_current_location_label() + self.data_channels_table.clear_table() + + def _set_search_by(self, selection=None): + """ + Converts user selection in the Search DropDown to value that can be + referenced in the metadata. + """ + if selection is not None: + if 'raw' in self.node_path and 'raw' != self.node_path[-1]: + self.searchBy_selection = selection.lower().replace(' ', '_') + self.node_path = self.node_path[:-1] + else: + self.searchBy_selection = selection.lower().replace(' ', '_') + self.node_path = self.node_path + self._rebuild() + return self.searchBy_selection, self.node_path + + def _get_search_by(self): + """"Retrieves user selected search by method in form that can be searched in metadata.""" + return self.searchBy_selection + + def _filter_row_in_table(self, scan): + """This filters out scans that do not match the text in the search bar.""" + user_entry = str(self.search_entry.text()) + value = user_entry in scan + return value + + def _search_text_changed(self): + """Updates Catalog Table if in 'Raw' directory and search entry is changed.""" + current_text = self.search_entry.text() + if current_text != self.previous_search_text: + if 'raw' in self.node_path: + if 'raw' != self.node_path[-1]: + self.node_path = self.node_path[:-1] + self._rebuild_table() + else: + self._rebuild_table() + + def _replace_last_node(self, new_node_id): + """Replaces the last node in the current path with new selected node.""" + if self.node_path: + self.node_path = self.node_path[:-1] + (new_node_id,) + return self.node_path + + def _on_prev_page_clicked(self): + if self._current_page != 0: + self._current_page -= 1 + self._rebuild() + + def _on_next_page_clicked(self): + if ( + self._current_page * self._rows_per_page + ) + self._rows_per_page < len(self.get_current_node()): + self._current_page += 1 + self._rebuild() + + def _on_first_page_clicked(self): + if self._current_page != 0: + self._current_page = 0 + self._rebuild() + + def _on_last_page_clicked(self): + while True: + if ( + self._current_page * self._rows_per_page + ) + self._rows_per_page < len(self.get_current_node()): + self._current_page += 1 + else: + self._rebuild() + break + + def _set_current_location_label(self): + if not self.node_path: + self.nrows_catalog_table = self.nrows_catalog_table + + starting_index = self._current_page * self._rows_per_page + 1 + ending_index = min( + self._rows_per_page * (self._current_page + 1), + self.nrows_catalog_table, + ) + current_location_text = f"{starting_index}-{ending_index} of {self.nrows_catalog_table}" + self.current_location_label.setText(current_location_text) + + def _addClicked(self, emit=True): + """Plots scan to the scan window after it is selected and the add button is clicked.""" + + sel_list = [] + channel_sel = self.data_channels_table.getChannelSelection() + if len(channel_sel['Data Channel List']): + if len(channel_sel['y']): + # TODO: find was to give self.data a SourceName method. + sel = { + 'SourceName': self.data.sourceName, + 'SourceType': self.data.sourceType, + 'selection': {'x': channel_sel['x'], + 'y': channel_sel['y'], + 'm': channel_sel['m'], + 'Channel List': channel_sel['Data Channel List']}, + 'scanselection': True, + } + sel_list.append(sel) + + if emit: + if len(sel_list): + self.sigAddSelection.emit(sel_list) + else: + return sel_list + + def _replaceClicked(self): + pass + + def _removeClicked(self): + pass + + + +class ClickableQLabel(QLabel): + clicked = Signal() + clicked_with_text = Signal(str) + + def mousePressEvent(self, event): + self.clicked.emit() + self.clicked_with_text.emit(self.text()) + + +def main(): + app = qt.QApplication([]) + w = TiledBrowser() + w.show() + app.exec() + +if __name__ == "__main__": + main() + +# TODO: handle changing the location label/current_page when on last page and +# increasing rows per page \ No newline at end of file diff --git a/PyMca5/PyMcaGui/io/TiledDataChannelTable.py b/PyMca5/PyMcaGui/io/TiledDataChannelTable.py new file mode 100644 index 000000000..d9280c118 --- /dev/null +++ b/PyMca5/PyMcaGui/io/TiledDataChannelTable.py @@ -0,0 +1,195 @@ +from PyQt5 import QtWidgets + +from PyMca5.PyMcaGui import PyMcaQt as qt +from PyMca5.PyMcaGui.io.SpecFileCntTable import CheckBoxItem + +class TiledDataChannelTable(qt.QTableWidget): + """ + Creates the data channel table (second table) inside the QTiledWidget. + The selections for the x and y set the axes in the scan window. + """ + sigTiledDataChannelTableSignal = qt.pyqtSignal(object) + + def __init__(self, parent=None): + qt.QTableWidget.__init__(self, parent) + self.dataChannelList = [] + self.xSelection = [] + self.ySelection = [] + self.monSelection = [] + + def format_table(self): + """Sets the column headers and the size of the columns for the table.""" + + # Column Labels + labels = ['Data Channel', 'x', 'y', 'Mon'] + self.setColumnCount(len(labels)) + for i in range(len(labels)): + item = self.horizontalHeaderItem(i) + if item is None: + item = qt.QTableWidgetItem(labels[i]) + item.setText(labels[i]) + self.setHorizontalHeaderItem(i, item) + + # Strech Columns to fill table + self.horizontalHeader().setStretchLastSection(True) + for column in range(self.columnCount()): + self.horizontalHeader().setSectionResizeMode(column, QtWidgets.QHeaderView.Stretch) + + def clear_table(self): + """Clears the table if a different scan is selected.""" + # Clear contents of the table + self.setRowCount(0) + self.setColumnCount(0) + + # Reset internal state + self.dataChannelList = [] + self.xSelection = [] + self.ySelection = [] + self.monSelection = [] + + self.format_table() + + def build_table(self, channelList): + """ + Builds the table in QTiledWidget based on the datachannels in selected + scan file. + """ + self.dataChannelList = channelList + n = len(channelList) + self.setRowCount(n) + if n > 0: + for (i, channelLabel) in enumerate(channelList): + self._addLine(i, channelLabel) + + def _addLine(self, i, channelLabel): + """ + Adds individual line to Data Channel Table based on the label of the + Data Channel in the scan file. + """ + # Data Channel name + item = self.item(i, 0) + if item is None: + item = qt.QTableWidgetItem(channelLabel) + item.setTextAlignment(qt.Qt.AlignHCenter | qt.Qt.AlignVCenter) + self.setItem(i, 0, item) + else: + item.setText(channelLabel) + + # Data Channel name is not selectable + item.setFlags(qt.Qt.ItemIsEnabled) + + # Checkboxes + for j in range(1, self.columnCount()): + widget = self.cellWidget(i, j) + if widget is None: + widget = CheckBoxItem(self, i, j) + self.setCellWidget(i, j, widget) + widget.sigCheckBoxItemSignal.connect(self._mySlot) + + # Resize columns to fit contents + self.resizeColumnsToContents() + + # Stretch Columns to fill the table + column_count = self.columnCount() + for column in range(column_count): + self.horizontalHeader().setStretchLastSection(True) + self.horizontalHeader().setSectionResizeMode(column, QtWidgets.QHeaderView.Stretch) + + def _mySlot(self, ddict): + row = ddict["row"] + col = ddict["col"] + + # 'x' column + if col == 1: + if ddict["state"]: + if row not in self.xSelection: + self.xSelection.append(row) + else: + if row in self.xSelection: + del self.xSelection[self.xSelection.index(row)] + # 'y' column + if col == 2: + if ddict["state"]: + if row not in self.ySelection: + self.ySelection.append(row) + else: + if row in self.ySelection: + del self.ySelection[self.ySelection.index(row)] + + # 'Mon' column + if col == 3: + if ddict["state"]: + if row not in self.monSelection: + self.monSelection.append(row) + else: + if row in self.monSelection: + del self.monSelection[self.monSelection.index(row)] + + self._update() + + def _update(self): + for i in range(self.rowCount()): + + # 'x' column + j = 1 + widget = self.cellWidget(i, j) + if i in self.xSelection: + if not widget.isChecked(): + widget.setChecked(True) + else: + if widget.isChecked(): + widget.setChecked(False) + + # 'y' column + j = 2 + widget = self.cellWidget(i, j) + if i in self.ySelection: + if not widget.isChecked(): + widget.setChecked(True) + else: + if widget.isChecked(): + widget.setChecked(False) + + # 'Mon' column + j = 3 + widget = self.cellWidget(i, j) + if i in self.monSelection: + if not widget.isChecked(): + widget.setChecked(True) + + else: + if widget.isChecked(): + widget.setChecked(False) + + ddict = {"event": "updated"} + self.sigTiledDataChannelTableSignal.emit(ddict) + + def getChannelSelection(self): + ddict = { + "Data Channel List": self.dataChannelList[:], + 'x': self.xSelection[:], + 'y' : self.ySelection[:], + 'm' : self.monSelection[:], + } + + return ddict + + def setChannelSelection(self, ddict): + dataChannelList = ddict.get("DataChannel List", self.dataChannelList[:]) + + self.xSelection = [item for item in ddict.get('x', []) if item < len(dataChannelList)] + self.ySelection = [item for item in ddict.get('y', []) if item < len(dataChannelList)] + self.monSelection = [item for item in ddict.get('m', []) if item < len(dataChannelList)] + + self._update() + +def main(): + app = qt.QApplication([]) + table = TiledDataChannelTable() + table.build_table(['Ch1', 'Ch2', 'Ch3']) + table.setChannelSelection({'x': [1,2], 'y':[4], 'ChList': ['dummy', 'Ch0', 'Ch1', 'Ch2', 'Ch3']}) + table.show() + app.exec() + +if __name__ == "__main__": + main() diff --git a/PyMca5/PyMcaGui/plotting/PlotWindow.py b/PyMca5/PyMcaGui/plotting/PlotWindow.py index bfe7a6abe..de3197a8b 100644 --- a/PyMca5/PyMcaGui/plotting/PlotWindow.py +++ b/PyMca5/PyMcaGui/plotting/PlotWindow.py @@ -42,12 +42,12 @@ import numpy from numpy import argsort, nonzero, take -from . import LegendSelector -from .ObjectPrintConfigurationDialog import ObjectPrintConfigurationDialog -from . import McaROIWidget -from . import PlotWidget -from . import MaskImageTools -from . import RenameCurveDialog +from PyMca5.PyMcaGui.plotting import LegendSelector +from PyMca5.PyMcaGui.plotting.ObjectPrintConfigurationDialog import ObjectPrintConfigurationDialog +from PyMca5.PyMcaGui.plotting import McaROIWidget +from PyMca5.PyMcaGui.plotting import PlotWidget +from PyMca5.PyMcaGui.plotting import MaskImageTools +from PyMca5.PyMcaGui.plotting import RenameCurveDialog try: from . import ColormapDialog @@ -55,7 +55,7 @@ except Exception: COLORMAP_DIALOG = False -from .PyMca_Icons import IconDict +from PyMca5.PyMcaGui.plotting.PyMca_Icons import IconDict from PyMca5.PyMcaGui import PyMcaQt as qt from PyMca5.PyMcaGui.io import PyMcaFileDialogs diff --git a/PyMca5/PyMcaGui/plotting/PyMca_Icons.py b/PyMca5/PyMcaGui/plotting/PyMca_Icons.py index b13251826..f68abe789 100644 --- a/PyMca5/PyMcaGui/plotting/PyMca_Icons.py +++ b/PyMca5/PyMcaGui/plotting/PyMca_Icons.py @@ -3585,6 +3585,44 @@ "..##################..", "......................"] +bluesky = [ +"32 32 2 1 ", +" c None", +"B c #87CEEB", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" BBBBBBB ", +" BBBBBBB ", +" BBBBBBBBBBBBBB ", +" BBBBBBBBBBBBBB ", +" BBBBBBBBBBBBBBBBBB ", +" BBBBBBBBBBBBBBBBBBB BBB ", +" BBBBBBBBBBBBBBBBBBB BBB ", +" BBBBBBBBBBBBBBBBBBBB BBBBBBBB ", +" BBBBBBBBBBBBBBBBBBBB BBBBBBBB ", +" BBBBBBBBBBBBBBBBBBBB BBBB BB", +" BBBBBBBBBBBBBBBBBBBB BBBB BB", +" BBBBBBBBBBBBBBBBBBBB BBBBBB BB", +" BBBBBBBBBBBBBBBBBBBB BBBBBB BB", +" BBBBBBBBBBBBBBBBBBBB BBBB BB ", +" BBBBBBBBBBBBBBBBBBBB BBBB BB ", +" BBBBBBBBBBBBBBBBBBBBBBBBB ", +" BBBBBBBBBBBBBBBBBBBBBBBBB ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +] + IconDict0 = { @@ -3597,6 +3635,7 @@ "fileprint": image_print_data, "spec": spec, "bliss": bliss, + "bluesky": bluesky, "normal": normal, "normalize16": normalize16, "reload": reload_, @@ -3826,7 +3865,6 @@ def __setitem__(self, key, item): IconDict = _PatchedIconDict(IconDict0) - def change_icons(plot): """Replace some of the silx icons with PyMca icons. @@ -3838,7 +3876,6 @@ def change_icons(plot): if hasattr(plot, "printPreview"): plot.printPreview.setIcon(qt.QIcon(qt.QPixmap(IconDict["fileprint"]))) - def showIcons(): w = qt.QWidget() g = qt.QGridLayout(w) diff --git a/PyMca5/PyMcaGui/pymca/McaWindow.py b/PyMca5/PyMcaGui/pymca/McaWindow.py index ce5f2d2a4..092bac8df 100644 --- a/PyMca5/PyMcaGui/pymca/McaWindow.py +++ b/PyMca5/PyMcaGui/pymca/McaWindow.py @@ -47,13 +47,13 @@ from PyMca5.PyMcaGui.io import PyMcaFileDialogs from PyMca5.PyMcaGui.plotting.PyMca_Icons import IconDict -from .ScanWindow import ScanWindow -from . import McaCalibrationControlGUI +from PyMca5.PyMcaGui.pymca import McaCalibrationControlGUI +from PyMca5.PyMcaGui.pymca.ScanWindow import ScanWindow from PyMca5.PyMcaIO import ConfigDict from PyMca5.PyMcaGui.physics.xrf import McaAdvancedFit from PyMca5.PyMcaGui.physics.xrf import McaCalWidget from PyMca5.PyMcaCore import DataObject -from . import McaSimpleFit +from PyMca5.PyMcaGui.pymca import McaSimpleFit from PyMca5.PyMcaMath.fitting import Specfit from PyMca5.PyMcaMath.fitting import SpecfitFuns from PyMca5.PyMcaGui.plotting import PyMcaPrintPreview @@ -75,14 +75,15 @@ class McaWindow(ScanWindow): - def __init__(self, parent=None, name="Mca Window", specfit=None, backend=None, + def __init__(self, parent=None, title="Mca Window", specfit=None, backend=None, plugins=True, newplot=False, roi=True, fit=True, **kw): - ScanWindow.__init__(self, parent, - name=name, - newplot=newplot, - plugins=plugins, + super().__init__(parent=parent, + name=title, + specfit=specfit, backend=backend, + plugins=plugins, + newplot=newplot, roi=roi, fit=fit, **kw) diff --git a/PyMca5/PyMcaGui/pymca/QDataSource.py b/PyMca5/PyMcaGui/pymca/QDataSource.py index 55e3c6570..000eb5388 100644 --- a/PyMca5/PyMcaGui/pymca/QDataSource.py +++ b/PyMca5/PyMcaGui/pymca/QDataSource.py @@ -38,9 +38,11 @@ from PyMca5.PyMcaCore import SpecFileDataSource from PyMca5.PyMcaCore import EdfFileDataSource +from PyMca5.PyMcaCore import QTiledDataSource from PyMca5.PyMcaIO import BlissSpecFile from PyMca5.PyMcaGui.io import QEdfFileWidget from PyMca5.PyMcaGui.io import QSpecFileWidget +from PyMca5.PyMcaGui.io import QTiledWidget if sys.platform == "win32": source_types = { SpecFileDataSource.SOURCE_TYPE: SpecFileDataSource.SpecFileDataSource, @@ -55,11 +57,13 @@ from PyMca5.PyMcaGui.io import QSpsWidget source_types = { SpecFileDataSource.SOURCE_TYPE: SpecFileDataSource.SpecFileDataSource, EdfFileDataSource.SOURCE_TYPE: EdfFileDataSource.EdfFileDataSource, - QSpsDataSource.SOURCE_TYPE: QSpsDataSource.QSpsDataSource} + QSpsDataSource.SOURCE_TYPE: QSpsDataSource.QSpsDataSource, + QTiledDataSource.SOURCE_TYPE: QTiledDataSource.QTiledDataSource} source_widgets = { SpecFileDataSource.SOURCE_TYPE: QSpecFileWidget.QSpecFileWidget, EdfFileDataSource.SOURCE_TYPE: QEdfFileWidget.QEdfFileWidget, - QSpsDataSource.SOURCE_TYPE: QSpsWidget.QSpsWidget} + QSpsDataSource.SOURCE_TYPE: QSpsWidget.QSpsWidget, + QTiledDataSource.SOURCE_TYPE: QTiledWidget.TiledBrowser} NEXUS = True try: @@ -85,6 +89,9 @@ def getSourceType(sourceName0): # wrapped as SpecFile return SpecFileDataSource.SOURCE_TYPE + if QTiledDataSource._is_Tiled_Source(sourceName): + return QTiledDataSource.SOURCE_TYPE + if sps is not None: if sourceName in sps.getspeclist(): return QSpsDataSource.SOURCE_TYPE @@ -154,6 +161,11 @@ def getSourceType(sourceName0): except Exception: pass return SpecFileDataSource.SOURCE_TYPE + elif sourceName.startswith("tiled:") or \ + sourceName.startswith(r"http:/") or \ + sourceName.startswith(r"https:/"): + # only chance is to use silx via an h5py-like API + return QTiledDataSource.SOURCE_TYPE else: return QSpsDataSource.SOURCE_TYPE diff --git a/PyMca5/PyMcaGui/pymca/QDispatcher.py b/PyMca5/PyMcaGui/pymca/QDispatcher.py index 856a408f7..752f62483 100644 --- a/PyMca5/PyMcaGui/pymca/QDispatcher.py +++ b/PyMca5/PyMcaGui/pymca/QDispatcher.py @@ -33,7 +33,7 @@ from PyMca5.PyMcaGui import PyMcaQt as qt QTVERSION = qt.qVersion() from PyMca5.PyMcaGui.io import QSourceSelector -from . import QDataSource +from PyMca5.PyMcaGui.pymca import QDataSource #import weakref _logger = logging.getLogger(__name__) @@ -260,6 +260,9 @@ def _sourceSelectorSlot(self, ddict): _logger.debug("connecting source of type %s" % sourceType) source.sigUpdated.connect(self._selectionUpdatedSlot) + elif ddict["event"] == "Open Tiled Tab": + self.tabWidget.setCurrentWidget(self.selectorWidget["Tiled"]) + elif (ddict["event"] == "SourceSelected") or \ (ddict["event"] == "SourceReloaded"): found = 0 diff --git a/PyMca5/PyMcaGui/pymca/ScanWindow.py b/PyMca5/PyMcaGui/pymca/ScanWindow.py index ac981dc07..a43cb8971 100644 --- a/PyMca5/PyMcaGui/pymca/ScanWindow.py +++ b/PyMca5/PyMcaGui/pymca/ScanWindow.py @@ -45,7 +45,7 @@ from PyMca5.PyMcaGui.io import PyMcaFileDialogs from PyMca5.PyMcaGui.plotting import PlotWindow -from . import ScanFit +from PyMca5.PyMcaGui.pymca import ScanFit from PyMca5.PyMcaMath import SimpleMath from PyMca5.PyMcaCore import DataObject import copy diff --git a/PyMca5/PyMcaIO/TiledFile.py b/PyMca5/PyMcaIO/TiledFile.py new file mode 100644 index 000000000..edcc6cefb --- /dev/null +++ b/PyMca5/PyMcaIO/TiledFile.py @@ -0,0 +1,3 @@ +class TiledFile(object): + def __init__(self): + self.header = {} \ No newline at end of file diff --git a/PyMca5/__init__.py b/PyMca5/__init__.py index eb68f9530..deb9ad27a 100644 --- a/PyMca5/__init__.py +++ b/PyMca5/__init__.py @@ -47,9 +47,9 @@ import ctypes from ctypes.wintypes import MAX_PATH -if os.path.exists(os.path.join(\ - os.path.dirname(os.path.dirname(__file__)), 'bootstrap.py')): - raise ImportError('PyMca cannot be imported from source directory') +#if os.path.exists(os.path.join(\ +# os.path.dirname(os.path.dirname(__file__)), 'bootstrap.py')): +# raise ImportError('PyMca cannot be imported from source directory') def version(): return __version__