Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ install_requires =
matplotlib
napari
numpy
tinycss2
python_requires = >=3.8
include_package_data = True
package_dir =
Expand Down Expand Up @@ -57,6 +58,7 @@ testing =
pyqt6
pytest
pytest-cov
pytest-mock
pytest-mpl
pytest-qt
tox
Expand Down
12 changes: 10 additions & 2 deletions src/napari_matplotlib/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QVBoxLayout, QWidget

from .util import Interval
from .util import Interval, from_css_get_size_of

# Icons modified from
# https://github.com/matplotlib/matplotlib/tree/main/lib/matplotlib/mpl-data/images
Expand Down Expand Up @@ -52,7 +52,9 @@ def __init__(self, napari_viewer: napari.viewer.Viewer):

self.canvas.figure.patch.set_facecolor("none")
self.canvas.figure.set_layout_engine("constrained")
self.toolbar = NapariNavigationToolbar(self.canvas, self)
self.toolbar = NapariNavigationToolbar(
self.canvas, self
) # type: ignore[no-untyped-call]
self._replace_toolbar_icons()

self.setLayout(QVBoxLayout())
Expand Down Expand Up @@ -189,6 +191,12 @@ def _replace_toolbar_icons(self) -> None:
class NapariNavigationToolbar(NavigationToolbar2QT):
"""Custom Toolbar style for Napari."""

def __init__(self, *args, **kwargs): # type: ignore[no-untyped-def]
super().__init__(*args, **kwargs)
self.setIconSize(
from_css_get_size_of("QtViewerPushButton", fallback=(28, 28))
)

def _update_buttons_checked(self) -> None:
"""Update toggle tool icons when selected/unselected."""
super()._update_buttons_checked()
Expand Down
32 changes: 31 additions & 1 deletion src/napari_matplotlib/tests/test_util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from qtpy.QtCore import QSize

from napari_matplotlib.util import Interval
from napari_matplotlib.util import Interval, from_css_get_size_of


def test_interval():
Expand All @@ -13,3 +14,32 @@ def test_interval():

with pytest.raises(ValueError, match="must be an integer"):
"string" in interval # type: ignore


def test_get_size_from_css(mocker):
"""Test getting the max-width and max-height from something in css"""
test_css = """
Flibble {
min-width : 0;
max-width : 123px;
min-height : 0px;
max-height : 456px;
padding: 0px;
}
"""
mocker.patch("napari.qt.get_current_stylesheet").return_value = test_css
assert from_css_get_size_of("Flibble", (1, 1)) == QSize(123, 456)


def test_fallback_if_missing_dimensions(mocker):
"""Test fallback if given something that doesn't have dimensions"""
test_css = " Flobble { background-color: rgb(0, 97, 163); } "
mocker.patch("napari.qt.get_current_stylesheet").return_value = test_css
assert from_css_get_size_of("Flobble", (1, 1)) == QSize(1, 1)


def test_fallback_if_prelude_not_in_css():
"""Test fallback if given something not in the css"""
assert from_css_get_size_of("AQButtonThatDoesntExist", (1, 1)) == QSize(
1, 1
)
64 changes: 63 additions & 1 deletion src/napari_matplotlib/util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from typing import Optional
from typing import List, Optional, Tuple, Union

import napari.qt
import tinycss2
from qtpy.QtCore import QSize


class Interval:
Expand Down Expand Up @@ -34,3 +38,61 @@ def __contains__(self, val: int) -> bool:
if self.upper is not None and val > self.upper:
return False
return True


def _has_id(nodes: List[tinycss2.ast.Node], id_name: str) -> bool:
"""
Is `id_name` in IdentTokens in the list of CSS `nodes`?
"""
return any(
[node.type == "ident" and node.value == id_name for node in nodes]
)


def _get_dimension(
nodes: List[tinycss2.ast.Node], id_name: str
) -> Union[int, None]:
"""
Get the value of the DimensionToken for the IdentToken `id_name`.

Returns
-------
None if no IdentToken is found.
"""
cleaned_nodes = [node for node in nodes if node.type != "whitespace"]
for name, _, value, _ in zip(*(iter(cleaned_nodes),) * 4):
if (
name.type == "ident"
and value.type == "dimension"
and name.value == id_name
):
return value.int_value
return None


def from_css_get_size_of(
qt_element_name: str, fallback: Tuple[int, int]
) -> QSize:
"""
Get the size of `qt_element_name` from napari's current stylesheet.

TODO: Confirm that the napari.qt.get_current_stylesheet changes with napari
theme (docs seem to indicate it should)

Returns
-------
QSize of the element if it's found, the `fallback` if it's not found..
"""
rules = tinycss2.parse_stylesheet(
napari.qt.get_current_stylesheet(),
skip_comments=True,
skip_whitespace=True,
)
w, h = None, None
for rule in rules:
if _has_id(rule.prelude, qt_element_name):
w = _get_dimension(rule.content, "max-width")
h = _get_dimension(rule.content, "max-height")
if w and h:
return QSize(w, h)
return QSize(*fallback)