diff --git a/napari/_qt/widgets/qt_viewer_status_bar.py b/napari/_qt/widgets/qt_viewer_status_bar.py index 25888943f63..95aee82462b 100644 --- a/napari/_qt/widgets/qt_viewer_status_bar.py +++ b/napari/_qt/widgets/qt_viewer_status_bar.py @@ -1,14 +1,9 @@ """Status bar widget on the viewer MainWindow""" -from typing import TYPE_CHECKING, Optional - -from qtpy.QtCore import Qt -from qtpy.QtWidgets import ( - QHBoxLayout, - QLabel, - QSizePolicy, - QStatusBar, - QWidget, -) +from typing import TYPE_CHECKING, Optional, cast + +from qtpy.QtCore import QEvent, Qt +from qtpy.QtGui import QFontMetrics, QResizeEvent +from qtpy.QtWidgets import QLabel, QStatusBar, QWidget from superqt import QElidingLabel from napari._qt.dialogs.qt_activity_dialog import ActivityToggleItem @@ -22,48 +17,45 @@ class ViewerStatusBar(QStatusBar): def __init__(self, parent: '_QtMainWindow') -> None: super().__init__(parent=parent) - main_widget = QWidget() - - layout = QHBoxLayout() - self._status = QLabel(trans._('Ready')) self._status.setContentsMargins(0, 0, 0, 0) self._layer_base = QElidingLabel(trans._('')) + self._layer_base.setObjectName('layer_base status') self._layer_base.setElideMode(Qt.TextElideMode.ElideMiddle) self._layer_base.setMinimumSize(100, 16) self._layer_base.setContentsMargins(0, 0, 0, 0) - self._layer_base.setSizePolicy( - QSizePolicy.Minimum, QSizePolicy.Maximum - ) self._plugin_reader = QElidingLabel(trans._('')) + self._plugin_reader.setObjectName('plugin-reader status') self._plugin_reader.setMinimumSize(80, 16) self._plugin_reader.setContentsMargins(0, 0, 0, 0) self._plugin_reader.setElideMode(Qt.TextElideMode.ElideMiddle) self._source_type = QLabel('') + self._source_type.setObjectName('source-type status') self._source_type.setContentsMargins(0, 0, 0, 0) - self._coordinates = QLabel('') + self._coordinates = QElidingLabel('') + self._coordinates.setObjectName('coordinates status') + self._coordinates.setMinimumSize(100, 16) self._coordinates.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self._status) - layout.addWidget(self._layer_base) - layout.addWidget(self._source_type) - layout.addWidget(self._plugin_reader) - layout.addWidget(self._coordinates) - layout.addStretch(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - main_widget.setLayout(layout) + self._help = QElidingLabel('') + self._help.setObjectName('help status') + self._help.setAlignment( + Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter + ) + main_widget = StatusBarWidget( + self._status, + self._layer_base, + self._source_type, + self._plugin_reader, + self._coordinates, + self._help, + ) self.addWidget(main_widget, 1) - self._help = QElidingLabel('') - self._help.setAlignment(Qt.AlignmentFlag.AlignRight) - self._help.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) - layout.addWidget(self._help, 1) self._activity_item = ActivityToggleItem() self._activity_item._activityBtn.clicked.connect( @@ -78,46 +70,32 @@ def setHelpText(self, text: str) -> None: def setStatusText( self, - text: str = None, - layer_base=None, + text: str = "", + layer_base: str = "", source_type=None, - plugin=None, - coordinates=None, + plugin: str = "", + coordinates: str = "", ) -> None: # The method used to set a single value as the status and not # all the layer information. + self._status.setText(text) - if text: - self._status.setText(text) - else: - self._status.setText('') - - if layer_base: - self._layer_base.show() - self._layer_base.setText(layer_base) - else: - self._layer_base.hide() + self._layer_base.setVisible(bool(layer_base)) + self._layer_base.setText(layer_base) + self._source_type.setVisible(bool(source_type)) if source_type: - self._source_type.show() self._source_type.setText(f'{source_type}: ') - else: - self._source_type.hide() - if plugin: - self._plugin_reader.show() - self._plugin_reader.setText(plugin) - else: - self._plugin_reader.hide() + self._plugin_reader.setVisible(bool(plugin)) - if coordinates: - self._coordinates.show() - self._coordinates.setText(coordinates) - else: - self._coordinates.hide() + self._plugin_reader.setText(plugin) + + self._coordinates.setVisible(bool(coordinates)) + self._coordinates.setText(coordinates) def _toggle_activity_dock(self, visible: Optional[bool] = None): - par: _QtMainWindow = self.parent() + par = cast('_QtMainWindow', self.parent()) if visible is None: visible = not par._activity_dialog.isVisible() if visible: @@ -129,3 +107,120 @@ def _toggle_activity_dock(self, visible: Optional[bool] = None): else: par._activity_dialog.hide() self._activity_item._activityBtn.setArrowType(Qt.ArrowType.UpArrow) + + +class StatusBarWidget(QWidget): + def __init__( + self, + status_label: QLabel, + layer_label: QLabel, + source_label: QLabel, + plugin_label: QLabel, + coordinates_label: QLabel, + help_label: QLabel, + parent: QWidget = None, + ): + super().__init__(parent=parent) + self._status_label = status_label + self._layer_label = layer_label + self._source_label = source_label + self._plugin_label = plugin_label + self._coordinates_label = coordinates_label + self._help_label = help_label + + self._status_label.setParent(self) + self._layer_label.setParent(self) + self._source_label.setParent(self) + self._plugin_label.setParent(self) + self._coordinates_label.setParent(self) + self._help_label.setParent(self) + + def resizeEvent(self, event: QResizeEvent) -> None: + super().resizeEvent(event) + self.do_layout() + + def event(self, event: QEvent) -> bool: + if event.type() == QEvent.Type.LayoutRequest: + self.do_layout() + return super().event(event) + + @staticmethod + def _calc_width(fm: QFontMetrics, label: QLabel) -> int: + # magical nuber +2 is from superqt code + # magical number +12 is from experiments + # Adding this values is required to avoid the text to be elided + # if there is enough space to show it. + return ( + ( + fm.boundingRect(label.text()).width() + + label.margin() * 2 + + 2 + + 12 + ) + if label.isVisible() + else 0 + ) + + def do_layout(self): + width = self.width() + height = self.height() + + fm = QFontMetrics(self._status_label.font()) + + status_width = self._calc_width(fm, self._status_label) + layer_width = self._calc_width(fm, self._layer_label) + source_width = self._calc_width(fm, self._source_label) + plugin_width = self._calc_width(fm, self._plugin_label) + coordinates_width = self._calc_width(fm, self._coordinates_label) + + base_width = ( + status_width + + layer_width + + source_width + + plugin_width + + coordinates_width + ) + + help_width = max(0, width - base_width) + + if coordinates_width: + help_width = 0 + + if base_width > width: + self._help_label.setVisible(False) + layer_width = max( + int((layer_width / base_width) * layer_width), + min(self._layer_label.minimumWidth(), layer_width), + ) + source_width = max( + int((source_width / base_width) * source_width), + min(self._source_label.minimumWidth(), source_width), + ) + plugin_width = max( + int((plugin_width / base_width) * plugin_width), + min(self._plugin_label.minimumWidth(), plugin_width), + ) + coordinates_width = ( + base_width + - status_width + - layer_width + - source_width + - plugin_width + ) + + else: + self._help_label.setVisible(True) + + self._status_label.setGeometry(0, 0, status_width, height) + shift = status_width + self._layer_label.setGeometry(shift, 0, layer_width, height) + shift += layer_width + self._source_label.setGeometry(shift, 0, source_width, height) + shift += source_width + self._plugin_label.setGeometry(shift, 0, plugin_width, height) + shift += plugin_width + self._coordinates_label.setGeometry( + shift, 0, coordinates_width, height + ) + shift += coordinates_width + self._help_label.setGeometry(shift, 0, help_width, height)