From 36f73287a8522042c44bac075fbd068ee0b497a5 Mon Sep 17 00:00:00 2001 From: Abigail Giles Date: Wed, 23 Nov 2022 15:59:54 -0500 Subject: [PATCH 1/2] Initial work to use the same code from bluesky-widgets --- setup.cfg | 19 ++- src/napari_tiled_browser/models/search.py | 92 ++++++++++++ src/napari_tiled_browser/napari.yaml | 11 +- src/napari_tiled_browser/qt/search.py | 171 ++++++++++++++++++++++ src/napari_tiled_browser/tiled_widget2.py | 28 ++++ 5 files changed, 308 insertions(+), 13 deletions(-) create mode 100644 src/napari_tiled_browser/models/search.py create mode 100644 src/napari_tiled_browser/qt/search.py create mode 100644 src/napari_tiled_browser/tiled_widget2.py diff --git a/setup.cfg b/setup.cfg index c376325..01e4221 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -name = napari-tiled +name = napari_tiled version = 0.0.1 description = A Tiled browser plugin for napari long_description = file: README.md @@ -8,6 +8,7 @@ url = https://github.com/AbbyGi/napari-tiled author = Abby Giles author_email = agiles@bnl.gov license = BSD-3-Clause +license_file = LICENSE license_files = LICENSE classifiers = Development Status :: 2 - Pre-Alpha @@ -31,17 +32,16 @@ project_urls = [options] packages = find: install_requires = - numpy magicgui + napari + numpy qtpy - + tiled python_requires = >=3.8 include_package_data = True package_dir = =src -# add your package requirements here - [options.packages.find] where = src @@ -51,13 +51,12 @@ napari.manifest = [options.extras_require] testing = - tox - pytest # https://docs.pytest.org/en/latest/contents.html - pytest-cov # https://pytest-cov.readthedocs.io/en/latest/ - pytest-qt # https://pytest-qt.readthedocs.io/en/latest/ napari pyqt5 - + pytest + pytest-cov + pytest-qt + tox [options.package_data] * = *.yaml diff --git a/src/napari_tiled_browser/models/search.py b/src/napari_tiled_browser/models/search.py new file mode 100644 index 0000000..4fcd939 --- /dev/null +++ b/src/napari_tiled_browser/models/search.py @@ -0,0 +1,92 @@ +from napari.utils.events import EmitterGroup, Event +from tiled.client import from_uri + +DEFAULT_PAGE_LIMIT = 20 + + +class ResultsPage: + def __init__(self, *, uri=None): + self._uri = None + self._client = None + self._total_length = 0 + self.uri = uri + self._page_offset = 0 + self._page_limit = DEFAULT_PAGE_LIMIT + self.queries = [] + self.results = [] + self.events = EmitterGroup( + source=self, + auto_connect=True, + page_offset=Event, + page_limit=Event, + refreshed=Event, + connected=Event, + uri=Event, + ) + self.events.page_offset.connect(self.refresh) + self.events.page_limit.connect(self.refresh) + self.events.connected.connect(self.refresh) + self.events.uri.connect(self.refresh) + self.events.refreshed() + + @property + def page_offset(self): + return self._page_offset + + @page_offset.setter + def page_offset(self, value): + self._page_offset = value + self.events.page_offset(value=value) + + @property + def page_limit(self): + return self._page_limit + + @page_limit.setter + def page_limit(self, value): + self._page_limit = value + self.events.page_limit(value=value) + + @property + def range(self): + return ( + self.page_offset * self.page_limit, + min((1 + self.page_offset) * self.page_limit, self._total_length), + ) + + @property + def total_length(self): + return self._total_length + + @property + def uri(self): + return self._uri + + @uri.setter + def uri(self, value): + if value == self._uri: + return + self._uri = value + self.events.uri(value=value) + if value is None: + self._client = None + else: + self._client = from_uri(value) + self.events.connected() + + def refresh(self, event): + if self._client is None: + return [] + results = self._client + for query in self.queries: + results = results.search(query) + self._total_length = len(results) + self.results.clear() + self.results.extend( + results.keys()[ + (self.page_offset * self.page_limit) : ( # noqa E203 + (1 + self.page_offset) * self.page_limit + ) + ] + ) + self.events.refreshed() diff --git a/src/napari_tiled_browser/napari.yaml b/src/napari_tiled_browser/napari.yaml index 3545a3c..fb59fe4 100644 --- a/src/napari_tiled_browser/napari.yaml +++ b/src/napari_tiled_browser/napari.yaml @@ -2,9 +2,14 @@ name: napari-tiled display_name: Tiled Browser contributions: commands: + # - id: napari-tiled.make_qwidget + # python_name: napari_tiled_browser.tiled_widget:TiledBrowser + # title: Tiled Browser - id: napari-tiled.make_qwidget - python_name: napari_tiled_browser.tiled_widget:TiledBrowser - title: Tiled Browser + python_name: napari_tiled_browser.tiled_widget2:TiledBrowser2 + title: Tiled Browser 2 widgets: + # - command: napari-tiled.make_qwidget + # display_name: Tiled Browser - command: napari-tiled.make_qwidget - display_name: Tiled Browser + display_name: Tiled Browser 2 diff --git a/src/napari_tiled_browser/qt/search.py b/src/napari_tiled_browser/qt/search.py new file mode 100644 index 0000000..d6c8255 --- /dev/null +++ b/src/napari_tiled_browser/qt/search.py @@ -0,0 +1,171 @@ +from qtpy.QtCore import Qt, Signal +from qtpy.QtWidgets import ( + QComboBox, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QSplitter, + QTableWidget, + QTableWidgetItem, + QVBoxLayout, + QWidget, +) + + +class SearchResults(QWidget): + # your QWidget.__init__ can optionally request the napari viewer instance + # in one of two ways: + # 1. use a parameter called `napari_viewer`, as done here + # 2. use a type annotation of 'napari.viewer.Viewer' for any parameter + def __init__(self, model): + super().__init__() + self.model = model + + # 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) + + # Navigation elements + self.rows_per_page_label = QLabel("Rows per page: ") + self.rows_per_page_selector = QComboBox() + # TODO: use model._page_limit as default option here? + self.rows_per_page_selector.addItems(["10", "20", "50"]) + self.rows_per_page_selector.setCurrentIndex(1) + + self.current_location_label = QLabel() + self.previous_page = ClickableQLabel("<") + self.next_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.previous_page) + navigation_layout.addWidget(self.next_page) + self.navigation_widget.setLayout(navigation_layout) + + # Catalog table elements + self.catalog_table = QTableWidget(0, 1) + self.catalog_table.setHorizontalHeaderLabels(["ID"]) + self._create_table_rows() + self.catalog_table_widget = QWidget() + + # Catalog table layout + catalog_table_layout = QVBoxLayout() + catalog_table_layout.addWidget(self.catalog_table) + catalog_table_layout.addWidget(self.navigation_widget) + self.catalog_table_widget.setLayout(catalog_table_layout) + self.catalog_table_widget.setVisible(False) + + self.splitter = QSplitter(self) + self.splitter.setOrientation(Qt.Orientation.Vertical) + + self.splitter.addWidget(self.connection_widget) + self.splitter.addWidget(self.catalog_table_widget) + + self.splitter.setStretchFactor(1, 2) + + layout = QVBoxLayout() + layout.addWidget(self.splitter) + self.setLayout(layout) + + 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.rows_per_page_selector.currentTextChanged.connect( + self._on_rows_per_page_changed + ) + + self.model.events.connected.connect(self._on_connected) + self.model.events.refreshed.connect(self._on_refreshed) + + # def _on_connect_clicked(self): + # url = self.url_entry.displayText() + # if not url: + # show_info("Please specify a url.") + # return + # try: + # self.catalog = from_uri(url) + # except Exception: + # show_info("Could not connect. Please check the url.") + # return + # self.connection_label.setText(f"Connected to {url}") + # self.catalog_table_widget.setVisible(True) + # self._set_current_location_label() + # self._populate_table() + + def _on_connect_clicked(self): + url = self.url_entry.displayText() + self.model.uri = url + + # print(f"{self.catalog = }") + self.connection_label.setText(f"Connected to {url}") + + def _on_connected(self, event): + self.catalog_table_widget.setVisible(True) + + def _on_refreshed(self, event): + self._set_current_location_label() + self._create_table_rows() + self._populate_table() + + def _on_rows_per_page_changed(self, value): + self.model.page_limit = int(value) + + def _create_table_rows(self): + target_length = len(self.model.results) + while self.catalog_table.rowCount() > target_length: + self.catalog_table.removeRow(0) + while self.catalog_table.rowCount() < target_length: + last_row_position = self.catalog_table.rowCount() + self.catalog_table.insertRow(last_row_position) + + def _populate_table(self): + for row_index, node in enumerate(self.model.results): + self.catalog_table.setItem(row_index, 0, QTableWidgetItem(node)) + + def _on_prev_page_clicked(self): + if self.model.page_offset != 0: + self.model.page_offset -= 1 + + def _on_next_page_clicked(self): + if ( + self.model.page_limit * (self.model.page_offset + 1) + < self.model.total_length + ): + self.model.page_offset += 1 + + def _set_current_location_label(self): + start, end = self.model.range + current_location_text = ( + f"{1 + start}-{end} of {self.model.total_length}" + ) + self.current_location_label.setText(current_location_text) + + +class ClickableQLabel(QLabel): + clicked = Signal() + + def mousePressEvent(self, event): + self.clicked.emit() + + +# TODO: handle changing the location label/current_page when on last page and +# increasing rows per page diff --git a/src/napari_tiled_browser/tiled_widget2.py b/src/napari_tiled_browser/tiled_widget2.py new file mode 100644 index 0000000..5ac3527 --- /dev/null +++ b/src/napari_tiled_browser/tiled_widget2.py @@ -0,0 +1,28 @@ +""" +This module is an example of a barebones QWidget plugin for napari + +It implements the Widget specification. +see: https://napari.org/plugins/guides.html?#widgets + +Replace code below according to your needs. +""" + +from qtpy.QtWidgets import QVBoxLayout, QWidget + +from .models.search import ResultsPage +from .qt.search import SearchResults + + +class TiledBrowser2(QWidget): + # your QWidget.__init__ can optionally request the napari viewer instance + # in one of two ways: + # 1. use a parameter called `napari_viewer`, as done here + # 2. use a type annotation of 'napari.viewer.Viewer' for any parameter + def __init__(self, napari_viewer): + super().__init__() + self.viewer = napari_viewer + self.search = ResultsPage(uri=None) + widget = SearchResults(self.search) + layout = QVBoxLayout() + layout.addWidget(widget) + self.setLayout(layout) From 784887eecdfba9347b15e9c3aa6e8294ef4d887f Mon Sep 17 00:00:00 2001 From: Abigail Giles Date: Tue, 29 Nov 2022 15:41:28 -0500 Subject: [PATCH 2/2] Get pagination working correctly --- src/napari_tiled_browser/models/search.py | 32 ++++++++--------- src/napari_tiled_browser/qt/search.py | 43 ++++++++++------------- 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/src/napari_tiled_browser/models/search.py b/src/napari_tiled_browser/models/search.py index 4fcd939..0a43f5a 100644 --- a/src/napari_tiled_browser/models/search.py +++ b/src/napari_tiled_browser/models/search.py @@ -9,34 +9,32 @@ def __init__(self, *, uri=None): self._uri = None self._client = None self._total_length = 0 - self.uri = uri - self._page_offset = 0 + self._page_number = 0 self._page_limit = DEFAULT_PAGE_LIMIT self.queries = [] self.results = [] self.events = EmitterGroup( source=self, auto_connect=True, - page_offset=Event, + page_number=Event, page_limit=Event, refreshed=Event, connected=Event, uri=Event, ) - self.events.page_offset.connect(self.refresh) + self.events.page_number.connect(self.refresh) self.events.page_limit.connect(self.refresh) self.events.connected.connect(self.refresh) - self.events.uri.connect(self.refresh) - self.events.refreshed() + self.uri = uri @property - def page_offset(self): - return self._page_offset + def page_number(self): + return self._page_number - @page_offset.setter - def page_offset(self, value): - self._page_offset = value - self.events.page_offset(value=value) + @page_number.setter + def page_number(self, value): + self._page_number = value + self.events.page_number(value=value) @property def page_limit(self): @@ -50,8 +48,8 @@ def page_limit(self, value): @property def range(self): return ( - self.page_offset * self.page_limit, - min((1 + self.page_offset) * self.page_limit, self._total_length), + self.page_number * self.page_limit, + min((1 + self.page_number) * self.page_limit, self._total_length), ) @property @@ -68,7 +66,7 @@ def uri(self, value): return self._uri = value self.events.uri(value=value) - if value is None: + if value == "": self._client = None else: self._client = from_uri(value) @@ -84,8 +82,8 @@ def refresh(self, event): self.results.clear() self.results.extend( results.keys()[ - (self.page_offset * self.page_limit) : ( # noqa E203 - (1 + self.page_offset) * self.page_limit + (self.page_number * self.page_limit) : ( # noqa E203 + (1 + self.page_number) * self.page_limit ) ] ) diff --git a/src/napari_tiled_browser/qt/search.py b/src/napari_tiled_browser/qt/search.py index d6c8255..c908ed0 100644 --- a/src/napari_tiled_browser/qt/search.py +++ b/src/napari_tiled_browser/qt/search.py @@ -1,3 +1,5 @@ +from math import floor + from qtpy.QtCore import Qt, Signal from qtpy.QtWidgets import ( QComboBox, @@ -96,26 +98,18 @@ def __init__(self, model): self.model.events.connected.connect(self._on_connected) self.model.events.refreshed.connect(self._on_refreshed) - # def _on_connect_clicked(self): - # url = self.url_entry.displayText() - # if not url: - # show_info("Please specify a url.") - # return - # try: - # self.catalog = from_uri(url) - # except Exception: - # show_info("Could not connect. Please check the url.") - # return - # self.connection_label.setText(f"Connected to {url}") - # self.catalog_table_widget.setVisible(True) - # self._set_current_location_label() - # self._populate_table() + if self.model.uri != "": + self.url_entry.setText(self.model.uri) + self.connection_label.setText(f"Connected to {self.model.uri}") + # Create the table if we already have a uri + self._set_current_location_label() + self._create_table_rows() + self._populate_table() + self.catalog_table_widget.setVisible(True) def _on_connect_clicked(self): url = self.url_entry.displayText() self.model.uri = url - - # print(f"{self.catalog = }") self.connection_label.setText(f"Connected to {url}") def _on_connected(self, event): @@ -127,7 +121,12 @@ def _on_refreshed(self, event): self._populate_table() def _on_rows_per_page_changed(self, value): + lower_bound, _ = self.model.range self.model.page_limit = int(value) + self.model.page_number = min( + self.model.page_number, + floor(self.model.total_length / self.model.page_limit), + ) def _create_table_rows(self): target_length = len(self.model.results) @@ -142,15 +141,15 @@ def _populate_table(self): self.catalog_table.setItem(row_index, 0, QTableWidgetItem(node)) def _on_prev_page_clicked(self): - if self.model.page_offset != 0: - self.model.page_offset -= 1 + if self.model.page_number != 0: + self.model.page_number -= 1 def _on_next_page_clicked(self): if ( - self.model.page_limit * (self.model.page_offset + 1) + self.model.page_limit * (self.model.page_number + 1) < self.model.total_length ): - self.model.page_offset += 1 + self.model.page_number += 1 def _set_current_location_label(self): start, end = self.model.range @@ -165,7 +164,3 @@ class ClickableQLabel(QLabel): def mousePressEvent(self, event): self.clicked.emit() - - -# TODO: handle changing the location label/current_page when on last page and -# increasing rows per page