Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
6873d6c
remove 2d black margins screenshot
melonora Mar 8, 2024
5948051
add param to viewer.screenshot
melonora Mar 8, 2024
7824f69
scale length of scale bar to canvas size
melonora Mar 22, 2024
cf45903
add docstring
melonora Mar 23, 2024
998d0b7
scale length of scale bar to canvas size
melonora Mar 22, 2024
607f116
add docstring
melonora Mar 23, 2024
181e1b2
adjust font size
melonora Apr 28, 2024
22c5319
adjust bottom offset to canvas size
melonora May 1, 2024
a1e8738
adjust bottom offset to canvas size
melonora May 4, 2024
5fdf661
adjust width based on canvas height
melonora May 4, 2024
3123367
remove scalebar whiskers
melonora May 4, 2024
4a434fc
adjust x_size based on canvas size
melonora May 4, 2024
d5c0c72
Merge branch 'main' into screenshot_without_margins
melonora May 5, 2024
4cc0d90
fix 1 pixel off
melonora May 5, 2024
6364fd0
Co-Authored-By: [email protected]
melonora May 5, 2024
fed5a73
Merge branch 'screenshot_without_margins' of https://github.com/melon…
melonora May 5, 2024
d3831bd
add test
melonora May 5, 2024
d012489
fix tests
melonora May 5, 2024
4d9b73f
chance parameter name
melonora May 5, 2024
9212a9f
fix tests
melonora May 5, 2024
d091e32
rename to no_margins
melonora May 5, 2024
9daa6cb
switch to margins parameter and default old behaviour
melonora May 6, 2024
feae403
Merge branch 'main' into screenshot_without_margins
melonora May 8, 2024
fbf0843
merge diverge
melonora May 8, 2024
3fd75a5
back to fit_to_data
melonora May 8, 2024
221692c
revert to fit_to_data
melonora May 12, 2024
41a8326
disallow fit_to_data if canvas_only is False
melonora May 18, 2024
10e86de
adjust test
melonora May 18, 2024
bffcbb2
fix test camera center
melonora May 24, 2024
e0e2498
close viewer prevent dangling animation
melonora May 26, 2024
effce1e
add fit_to_data to nbscreenshot
melonora May 26, 2024
3a79f08
Update napari/components/viewer_model.py
melonora May 27, 2024
0997e89
change parameter
melonora May 27, 2024
33a2a32
update scale_factor calc
melonora May 27, 2024
4bb7854
Update napari/_qt/qt_main_window.py
melonora May 27, 2024
e2ebf1b
update error messages
melonora May 27, 2024
31e8e9a
Merge branch 'screenshot_without_margins' of https://github.com/melon…
melonora May 27, 2024
2f71728
update docstrings
melonora May 27, 2024
6fcf9be
fix error
melonora May 27, 2024
7635530
minor grammar fix + fit docstring in 80c
jni May 27, 2024
65729bb
Make error strings fit in 80c
jni May 27, 2024
1d3140c
Update docstring for screenshot
jni May 27, 2024
5d50d64
Fix outdated docstring for Viewer.reset_view
jni May 27, 2024
8e5cba5
Fix fit-to-data docstring in two more places
jni May 27, 2024
7eabe99
Fix test error message match
jni May 27, 2024
55d6db9
rename parameter
melonora May 27, 2024
7105658
change to export_view
melonora Jun 1, 2024
6afe24b
address comments
melonora Jun 14, 2024
f622097
remove docstring
melonora Jun 14, 2024
df0b991
Merge branch 'main' into screenshot_without_margins
melonora Jun 14, 2024
cb1e951
fix test
melonora Jun 14, 2024
6984781
Merge branch 'main' into screenshot_without_margins
melonora Jun 14, 2024
21feada
revert to FutureWarning
melonora Jun 14, 2024
b840574
Merge branch 'screenshot_without_margins' of https://github.com/melon…
melonora Jun 14, 2024
9fa60ca
change to export_figure
melonora Jun 20, 2024
b5f1cb0
Merge branch 'main' into scale_bar
melonora Jun 20, 2024
f7633b3
merge screenshot_without_margins
melonora Jun 20, 2024
75c3983
merge main
melonora Dec 17, 2024
6380901
Update README.md to bump the recommended python to 3.11 (#7610)
psobolewskiPhD Feb 17, 2025
5ad360a
Enable creation of custom linear colormaps in layer controls (#7600)
lukasz-migas Feb 17, 2025
b2f722f
Add link to napari weather report dashboard in README.md (#7609)
willingc Feb 18, 2025
26f7af4
Fix layout issue in image/surface controls (#7618)
lukasz-migas Feb 18, 2025
3a46b9c
Add example to LayerList and docstrings for link_layers/unlink_layers…
melissawm Feb 19, 2025
256c68e
Merge branch 'main' into scale_bar
melonora Feb 19, 2025
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ We're working on [tutorials](https://napari.org/stable/tutorials/), but you can
It is recommended to install napari into a virtual environment, like this:

```sh
conda create -y -n napari-env -c conda-forge python=3.10
conda create -y -n napari-env -c conda-forge python=3.11
conda activate napari-env
python -m pip install "napari[all]"
```
Expand Down Expand Up @@ -88,7 +88,9 @@ You can see details of [the project roadmap here](https://napari.org/stable/road

Contributions are encouraged! Please read our [contributing guide](https://napari.org/dev/developers/contributing/index.html) to get started. Given that we're in an early stage, you may want to reach out on our [GitHub Issues](https://github.com/napari/napari/issues) before jumping in.

If you want to contribute or edit to our documentation, please go to [napari/docs](https://github.com/napari/docs).
If you want to contribute to or edit our documentation, please go to [napari/docs](https://github.com/napari/docs).

Visit our [project weather report dashboard](https://napari.org/weather-report/) to see metrics and how development is progressing.

## code of conduct

Expand Down
54 changes: 53 additions & 1 deletion napari/_qt/_tests/test_qt_utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from unittest.mock import patch

import numpy as np
import pytest
from qtpy.QtCore import QObject, Signal
from qtpy.QtWidgets import QMainWindow
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QApplication, QColorDialog, QMainWindow

from napari._qt.utils import (
QBYTE_FLAG,
add_flash_animation,
get_color,
is_qbyte,
qbytearray_to_str,
qt_might_be_rich_text,
Expand Down Expand Up @@ -117,3 +122,50 @@ class X:
with pytest.raises(RuntimeError):
f.result()
assert x.a == 2


def test_get_color(qtbot):
"""Test the get_color utility function."""
widget = QMainWindow()
qtbot.addWidget(widget)

with patch.object(QColorDialog, 'exec_') as mock:
mock.return_value = QColorDialog.Accepted
color = get_color(None, 'hex')
assert isinstance(color, str), 'Expected string color'

with patch.object(QColorDialog, 'exec_') as mock:
mock.return_value = QColorDialog.Accepted
color = get_color('#FF00FF', 'hex')
assert isinstance(color, str), 'Expected string color'
assert color == '#ff00ff', 'Expected color to be #FF00FF'

with patch.object(QColorDialog, 'exec_') as mock:
mock.return_value = QColorDialog.Accepted
color = get_color(None, 'array')
assert not isinstance(color, str), 'Expected array color'
assert isinstance(color, np.ndarray), 'Expected numpy array color'

with patch.object(QColorDialog, 'exec_') as mock:
mock.return_value = QColorDialog.Accepted
color = get_color(np.asarray([255, 0, 255]), 'array')
assert not isinstance(color, str), 'Expected array color'
assert isinstance(color, np.ndarray), 'Expected numpy array color'
np.testing.assert_array_equal(color, np.asarray([1, 0, 1]))

with patch.object(QColorDialog, 'exec_') as mock:
mock.return_value = QColorDialog.Accepted
color = get_color(None, 'qcolor')
assert not isinstance(color, np.ndarray), 'Expected QColor color'
assert isinstance(color, QColor), 'Expected QColor color'

with patch.object(QColorDialog, 'exec_') as mock:
mock.return_value = QColorDialog.Rejected
color = get_color(None, 'qcolor')
assert color is None, 'Expected None color'

# close still open popup widgets
for widget in QApplication.topLevelWidgets():
if isinstance(widget, QColorDialog):
widget.accept()
widget.close()
1 change: 0 additions & 1 deletion napari/_qt/_tests/test_qt_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,6 @@ def test_points_layer_display_correct_slice_on_scale(make_napari_viewer):
np.testing.assert_equal(response.indices, [0])


@pytest.mark.slow
@skip_on_win_ci
def test_qt_viewer_clipboard_with_flash(make_napari_viewer, qtbot):
viewer = make_napari_viewer()
Expand Down
18 changes: 17 additions & 1 deletion napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import numpy as np
import pytest
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QPushButton
from qtpy.QtWidgets import QApplication, QColorDialog, QPushButton

from napari._qt.layer_controls.qt_image_controls_base import (
QContrastLimitsPopup,
Expand Down Expand Up @@ -161,3 +161,19 @@ def test_blending_opacity_slider(qtbot):
layer.blending = 'translucent'
assert layer.blending == 'translucent'
assert qtctrl.opacitySlider.isEnabled()


@pytest.mark.parametrize('layer', [Image(_IMAGE), Surface(_SURF)])
def test_custom_colormap(qtbot, layer):
"""Test whether colormap button does anything."""
qtctrl = QtBaseImageControls(layer)

# check widget popup
assert isinstance(qtctrl.colorbarLabel, QPushButton), (
'Colorbar button not found'
)
qtbot.mouseRelease(qtctrl.colorbarLabel, Qt.MouseButton.LeftButton)
# close still open popup widgets
for widget in QApplication.topLevelWidgets():
if isinstance(widget, QColorDialog):
widget.close()
3 changes: 1 addition & 2 deletions napari/_qt/layer_controls/qt_image_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,7 @@ def __init__(self, layer) -> None:
self.colorbarLabel.setVisible(False)
else:
colormap_layout.addWidget(self.colorbarLabel)
colormap_layout.addWidget(self.colormapComboBox)
colormap_layout.addStretch(1)
colormap_layout.addWidget(self.colormapComboBox, stretch=1)

self.layout().addRow(self.button_grid)
self.layout().addRow(self.opacityLabel, self.opacitySlider)
Expand Down
21 changes: 15 additions & 6 deletions napari/_qt/layer_controls/qt_image_controls_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

import numpy as np
from qtpy.QtCore import Qt
from qtpy.QtGui import QImage, QPixmap
from qtpy.QtGui import QIcon, QImage, QPixmap
from qtpy.QtWidgets import (
QHBoxLayout,
QLabel,
QPushButton,
QWidget,
)
Expand Down Expand Up @@ -75,8 +74,8 @@ class QtBaseImageControls(QtLayerControls):
Button to transform image layer.
clim_popup : napari._qt.qt_range_slider_popup.QRangeSliderPopup
Popup widget launching the contrast range slider.
colorbarLabel : qtpy.QtWidgets.QLabel
Label text of colorbar widget.
colorbarLabel : qtpy.QtWidgets.QPushButton
Button showing colorbar widget. Also enables selection of custom colormap.
colormapComboBox : qtpy.QtWidgets.QComboBox
Dropdown widget for selecting the layer colormap.
contrastLimitsSlider : superqt.QRangeSlider
Expand Down Expand Up @@ -146,9 +145,10 @@ def __init__(self, layer: Image) -> None:
connect_setattr(sld.valueChanged, self.layer, 'gamma')
self.gammaSlider = sld

self.colorbarLabel = QLabel(parent=self)
self.colorbarLabel = QPushButton(parent=self)
self.colorbarLabel.setObjectName('colorbar')
self.colorbarLabel.setToolTip(trans._('Colorbar'))
self.colorbarLabel.clicked.connect(self._on_make_colormap)

self._on_colormap_change()
if self.__class__ == QtBaseImageControls:
Expand All @@ -158,6 +158,15 @@ def __init__(self, layer: Image) -> None:
# widgets.
self.layout().addRow(self.button_grid)

def _on_make_colormap(self):
"""Make new colormap when colorbarLabel (pushbutton) is pressed."""
from napari._qt.utils import get_color
from napari.utils.colormaps.colormap_utils import ensure_colormap

color = get_color(self, mode='hex')
if color:
self.layer.colormap = ensure_colormap(color)

def changeColor(self, text):
"""Change colormap on the layer model.

Expand Down Expand Up @@ -215,7 +224,7 @@ def _on_colormap_change(self):
cbar.shape[0],
QImage.Format_RGBA8888,
)
self.colorbarLabel.setPixmap(QPixmap.fromImage(image))
self.colorbarLabel.setIcon(QIcon(QPixmap.fromImage(image)))

def _on_gamma_change(self):
"""Receive the layer model gamma change event and update the slider."""
Expand Down
3 changes: 1 addition & 2 deletions napari/_qt/layer_controls/qt_surface_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ def __init__(self, layer) -> None:

colormap_layout = QHBoxLayout()
colormap_layout.addWidget(self.colorbarLabel)
colormap_layout.addWidget(self.colormapComboBox)
colormap_layout.addStretch(1)
colormap_layout.addWidget(self.colormapComboBox, stretch=1)

shading_comboBox = QComboBox(self)
for display_name, shading in SHADING_TRANSLATION.items():
Expand Down
61 changes: 60 additions & 1 deletion napari/_qt/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import weakref
from collections.abc import Iterable, Sequence
from contextlib import contextmanager
from enum import auto
from functools import partial

import numpy as np
Expand All @@ -20,6 +21,7 @@
)
from qtpy.QtGui import QColor, QCursor, QDrag, QImage, QPainter, QPixmap
from qtpy.QtWidgets import (
QColorDialog,
QGraphicsColorizeEffect,
QGraphicsOpacityEffect,
QHBoxLayout,
Expand All @@ -30,13 +32,29 @@

from napari.utils.colormaps.standardize_color import transform_color
from napari.utils.events.custom_types import Array
from napari.utils.misc import is_sequence
from napari.utils.misc import StringEnum, is_sequence
from napari.utils.translations import trans

QBYTE_FLAG = '!QBYTE_'
RICH_TEXT_PATTERN = re.compile('<[^\n]+>')


class ColorMode(StringEnum):
"""Enum fo selecting the color mode to return the color in.

ColorMode.HEX
Returns color as hex string.
ColorMode.LOOP
Returns color as a numpy array.
ColorMode.QCOLOR
Returns color as a QColor object
"""

HEX = auto()
ARRAY = auto()
QCOLOR = auto()


def is_qbyte(string: str) -> bool:
"""Check if a string is a QByteArray string.

Expand Down Expand Up @@ -388,3 +406,44 @@ def in_qt_main_thread() -> bool:
True if we are in the main thread, False otherwise.
"""
return QCoreApplication.instance().thread() == QThread.currentThread()


def get_color(
color: str | np.ndarray | QColor | None = None,
mode: ColorMode = ColorMode.HEX,
) -> np.ndarray | None:
"""
Helper function to get a color from q QColorDialog.

Parameters
----------
color : str | np.ndarray | QColor | None
Initial color to display in the dialog. Color will be automatically converted to QColor.
mode : ColorMode
Mode to return the color in (hex, array, QColor).

Returns
-------
new_color : str | np.ndarray | QColor
New color in the desired format.
"""

if isinstance(color, str):
color = QColor(color)
elif isinstance(color, np.ndarray):
color = QColor(*color.astype(int))

dlg = QColorDialog(color)
new_color: str | np.ndarray | QColor | None = None
if dlg.exec_():
new_color = dlg.currentColor()
if mode == ColorMode.HEX:
new_color = new_color.name()
elif mode == ColorMode.ARRAY:
new_color = (
np.asarray(
[new_color.red(), new_color.green(), new_color.blue()]
)
/ 255
)
return new_color
29 changes: 29 additions & 0 deletions napari/_vispy/overlays/scale_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from napari._vispy.overlays.base import ViewerOverlayMixin, VispyCanvasOverlay
from napari._vispy.visuals.scale_bar import ScaleBar
from napari.settings import get_settings
from napari.utils._units import PREFERRED_VALUES, get_unit_registry
from napari.utils.colormaps.standardize_color import transform_color
from napari.utils.theme import get_theme
Expand All @@ -26,8 +27,12 @@ def __init__(self, *, viewer, overlay, parent=None) -> None:
self.x_size = 150 # will be updated on zoom anyways
# need to change from defaults because the anchor is in the center
self.y_offset = 20
# TODO: perhaps change name as y_size does not indicate bottom offset.
self.y_size = 5

# In the super().__init__ we see node is scale bar, need to connect its parent, canvas
self.node.events.parent_change.connect(self._on_parent_change)

self.overlay.events.box.connect(self._on_box_change)
self.overlay.events.box_color.connect(self._on_data_change)
self.overlay.events.color.connect(self._on_data_change)
Expand All @@ -42,6 +47,30 @@ def __init__(self, *, viewer, overlay, parent=None) -> None:

self.reset()

def _on_parent_change(self, event):
"""Connect the canvas resize event to scale bar callback function(s)."""
if event.new and self.node.canvas:
event.new.canvas.events.resize.connect(
self._scale_scalebar_on_canvas_resize
)
event.new.canvas.events.resize.connect(self._scale_font_size)

def _scale_font_size(self, event):
"""Scale the font size in response to a canvas resize"""
self.node.text.font_size = (
event.source.size[1]
/ get_settings().experimental.scale_bar_font_size
)

def _scale_scalebar_on_canvas_resize(self, event):
self._target_length = (
event.source.size[0] / get_settings().experimental.scale_bar_length
)
self.y_size = event.source.size[1] / 40
self.x_size = event.source.size[0] / 20
self.node.line._width = event.source.size[1] / 100
self._on_zoom_change(force=True)

def _on_unit_change(self):
self._unit = get_unit_registry()(self.overlay.unit)
self._on_zoom_change(force=True)
Expand Down
6 changes: 1 addition & 5 deletions napari/_vispy/visuals/scale_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ def __init__(self) -> None:
[
[0, 0],
[1, 0],
[0, -5],
[0, 5],
[1, -5],
[1, 5],
]
)

Expand All @@ -26,7 +22,7 @@ def __init__(self) -> None:
anchor_y='top',
font_size=10,
),
Line(connect='segments', method='gl', width=3),
Line(connect='strip', method='gl', width=3),
]
)

Expand Down
Loading
Loading