Skip to content

Commit

Permalink
Merge pull request #64 from tangkong/gui_open_page
Browse files Browse the repository at this point in the history
GUI: Open page method, add some icons
  • Loading branch information
tangkong authored Jul 26, 2024
2 parents 4576f44 + 4e2ac9b commit 6528c74
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 22 deletions.
1 change: 1 addition & 0 deletions conda-recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ requirements:
- pcdsutils
- pyqt
- python-dateutil
- qtawesome
- qtpy

test:
Expand Down
23 changes: 23 additions & 0 deletions docs/source/upcoming_release_notes/64-gui_open_page.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
64 gui_open_page
################

API Breaks
----------
- N/A

Features
--------
- adds ``Window.open_page(entry)`` for opening an ``Entry`` in a new tab
- Adds icons to represent various ``Entry`` subclasses

Bugfixes
--------
- N/A

Maintenance
-----------
- N/A

Contributors
------------
- tangkong
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ apischema
pcdsutils
PyQt5
python-dateutil
qtawesome
qtpy
5 changes: 3 additions & 2 deletions superscore/tests/test_window.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from pytestqt.qtbot import QtBot

from superscore.client import Client
from superscore.widgets.window import Window


def test_main_window(qtbot: QtBot):
def test_main_window(qtbot: QtBot, mock_client: Client):
"""Pass if main window opens successfully"""
window = Window()
window = Window(client=mock_client)
qtbot.addWidget(window)
12 changes: 12 additions & 0 deletions superscore/ui/search_page.ui
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,39 @@
<property name="text">
<string>Snapshot</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="collection_checkbox">
<property name="text">
<string>Collection</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="setpoint_checkbox">
<property name="text">
<string>Setpoint</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="readback_checkbox">
<property name="text">
<string>Readback</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
Expand Down
21 changes: 21 additions & 0 deletions superscore/widgets/page/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
def get_page_icon_map():
# Don't pollute the namespace
from superscore.model import Collection, Readback, Setpoint, Snapshot
from superscore.widgets.page.entry import CollectionPage

page_map = {
Collection: CollectionPage
}

# a list of qtawesome icon names
icon_map = {
Collection: 'mdi.file-document-multiple',
Snapshot: 'mdi.camera',
Setpoint: 'mdi.target',
Readback: 'mdi.book-open-variant',
}

return page_map, icon_map


PAGE_MAP, ICON_MAP = get_page_icon_map()
62 changes: 44 additions & 18 deletions superscore/widgets/page/search.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""Search page"""

import logging
from typing import Any, Dict, List, Optional
from typing import Any, Callable, Dict, List, Optional

import qtawesome as qta
from dateutil import tz
from PyQt5.QtCore import QModelIndex
from PyQt5.QtWidgets import QStyleOptionViewItem, QWidget
from qtpy import QtCore, QtWidgets

from superscore.client import Client
from superscore.model import Collection, Entry, Readback, Setpoint, Snapshot
from superscore.widgets.core import Display
from superscore.widgets.page import ICON_MAP

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -48,9 +48,16 @@ class SearchPage(Display, QtWidgets.QWidget):
filter_table_view: QtWidgets.QTableView
results_table_view: QtWidgets.QTableView

def __init__(self, *args, client: Client, **kwargs) -> None:
def __init__(
self,
*args,
client: Client,
open_page_slot: Optional[Callable] = None,
**kwargs
) -> None:
super().__init__(*args, **kwargs)
self.client = client
self.open_page_slot = open_page_slot
self.model: Optional[ResultModel] = None

self.type_checkboxes: List[QtWidgets.QCheckBox] = [
Expand All @@ -65,9 +72,14 @@ def setup_ui(self) -> None:
self.end_dt_edit.setDate(QtCore.QDate.currentDate())
self.apply_filter_button.clicked.connect(self.show_current_filter)

self.collection_checkbox.setIcon(qta.icon(ICON_MAP[Collection]))
self.snapshot_checkbox.setIcon(qta.icon(ICON_MAP[Snapshot]))
self.setpoint_checkbox.setIcon(qta.icon(ICON_MAP[Setpoint]))
self.readback_checkbox.setIcon(qta.icon(ICON_MAP[Readback]))

# set up filter table view
self.model = ResultModel(entries=[])
self.proxy_model = ResultFilterProxyModel()
self.proxy_model = ResultFilterProxyModel(open_page_slot=self.open_page_slot)
self.proxy_model.setSourceModel(self.model)
self.results_table_view.setModel(self.proxy_model)
self.results_table_view.setSortingEnabled(True)
Expand All @@ -77,23 +89,23 @@ def setup_ui(self) -> None:
self.open_delegate = ButtonDelegate(button_text='open me')
del_col = len(ResultModel.headers) - 1
self.results_table_view.setItemDelegateForColumn(del_col, self.open_delegate)
self.open_delegate.clicked.connect(self.proxy_model.open_row)

self.name_subfilter_line_edit.textChanged.connect(self.subfilter_results)

def _gather_search_terms(self) -> Dict[str, Any]:
search_kwargs = {}

# type
entry_type_list = []
entry_type_list = [Snapshot, Collection, Setpoint, Readback]
for checkbox, entry_type in zip(
self.type_checkboxes,
(Snapshot, Collection, Setpoint, Readback)
):
if checkbox.isChecked():
entry_type_list.append(entry_type)
if not checkbox.isChecked():
entry_type_list.remove(entry_type)

if entry_type_list:
search_kwargs["entry_type"] = tuple(entry_type_list)
search_kwargs["entry_type"] = tuple(entry_type_list)

# name
name = self.name_line_edit.text()
Expand Down Expand Up @@ -167,13 +179,13 @@ def data(self, index: QtCore.QModelIndex, role: int) -> Any:
# Special handling for open button delegate
if index.column() == (len(self.headers) - 1):
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
return 'open'
return 'click to open'

if role != QtCore.Qt.DisplayRole:
# table is read only
return QtCore.QVariant()

if index.column() == 0: # name column, no edit permissions
if index.column() == 0: # name column
name_text = getattr(entry, 'title', getattr(entry, 'pv_name', '<N/A>'))
return name_text
elif index.column() == 1: # Type
Expand Down Expand Up @@ -230,7 +242,7 @@ def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlag:


class ButtonDelegate(QtWidgets.QStyledItemDelegate):
clicked = QtCore.Signal(int)
clicked = QtCore.Signal(QtCore.QModelIndex)

def __init__(self, *args, button_text: str = '', **kwargs):
self.button_text = button_text
Expand All @@ -244,15 +256,15 @@ def createEditor(
) -> QtWidgets.QWidget:
button = QtWidgets.QPushButton(self.button_text, parent)
button.clicked.connect(
lambda _, row=index.row(): self.clicked.emit(row)
lambda _, index=index: self.clicked.emit(index)
)
return button

def updateEditorGeometry(
self,
editor: QWidget,
option: QStyleOptionViewItem,
index: QModelIndex
editor: QtWidgets.QWidget,
option: QtWidgets.QStyleOptionViewItem,
index: QtCore.QModelIndex
) -> None:
return editor.setGeometry(option.rect)

Expand All @@ -264,9 +276,16 @@ class ResultFilterProxyModel(QtCore.QSortFilterProxyModel):
"""

name_regexp: QtCore.QRegularExpression
sourceModel: ResultModel

def __init__(self, *args, **kwargs) -> None:
def __init__(
self,
*args,
open_page_slot: Optional[Callable] = None,
**kwargs
) -> None:
super().__init__(*args, **kwargs)
self.open_page_slot = open_page_slot
self.name_regexp = QtCore.QRegularExpression()

def filterAcceptsRow(
Expand All @@ -281,3 +300,10 @@ def filterAcceptsRow(
name_ok = self.name_regexp.match(name).hasMatch()

return name_ok

def open_row(self, proxy_index: QtCore.QModelIndex) -> None:
"""opens page for entry data at ``row`` (in proxy model)"""
if self.open_page_slot is not None:
source_row = self.mapToSource(proxy_index)
logger.debug(f'Open page button for row: {proxy_index.row()}')
self.open_page_slot(self.sourceModel().entries[source_row.row()])
63 changes: 61 additions & 2 deletions superscore/widgets/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@
"""
from __future__ import annotations

from qtpy import QtWidgets
import logging
from typing import Optional

import qtawesome as qta
from qtpy import QtCore, QtWidgets

from superscore.client import Client
from superscore.model import Entry
from superscore.widgets.core import Display
from superscore.widgets.page import ICON_MAP, PAGE_MAP
from superscore.widgets.page.search import SearchPage

logger = logging.getLogger(__name__)


class Window(Display, QtWidgets.QMainWindow):
Expand All @@ -16,5 +26,54 @@ class Window(Display, QtWidgets.QMainWindow):
tree_view: QtWidgets.QTreeView
tab_widget: QtWidgets.QTabWidget

def __init__(self, *args, **kwargs):
def __init__(self, *args, client: Optional[Client] = None, **kwargs):
super().__init__(*args, **kwargs)
if client:
self.client = client
else:
self.client = Client.from_config()

self.setup_ui()
self.open_search_page()

def setup_ui(self) -> None:
tab_bar = self.tab_widget.tabBar()
# always use scroll area and never truncate file names
tab_bar.setUsesScrollButtons(True)
tab_bar.setElideMode(QtCore.Qt.ElideNone)
self.tab_widget.tabCloseRequested.connect(self.tab_widget.removeTab)

def open_page(self, entry: Entry) -> None:
"""
Open a page for ``entry`` in a new tab.
Parameters
----------
entry : Entry
Entry subclass to open a new page for
"""
logger.debug(f'attempting to open {entry}')
if not isinstance(entry, Entry):
logger.debug('Could not open page for non-Entry dataclass')
return

if type(entry) not in PAGE_MAP:
logger.debug(f'No page corresponding to {type(entry).__name__}')

try:
page = PAGE_MAP[type(entry)]
except KeyError:
logger.debug(f'No page widget for {type(entry)}, cannot open in tab')
return

page_widget = page(data=entry)
icon = qta.icon(ICON_MAP[type(entry)])
tab_name = getattr(
entry, 'title', getattr(entry, 'pv_name', f'<{type(entry).__name__}>')
)
idx = self.tab_widget.addTab(page_widget, icon, tab_name)
self.tab_widget.setCurrentIndex(idx)

def open_search_page(self) -> None:
page = SearchPage(client=self.client, open_page_slot=self.open_page)
self.tab_widget.addTab(page, 'search')

0 comments on commit 6528c74

Please sign in to comment.