Repository: napari/napari

Issue:
Napari windows resizes out of screen in case `layer.features` contains many columns
## 🐛 Bug

When `layer.features` contains a wide table and a user hovers over the labels layer, the Napari window resizes out of screen. I presume it has something to do with the status text field on the bottom of the window showing label properties.

## To Reproduce

Steps to reproduce the behavior:

1. Run this code:
```
import numpy as np
import napari
import pandas

viewer = napari.Viewer()

viewer.add_image(np.asarray([[1,2],[2,2]]))
labels_layer = viewer.add_labels(np.asarray([[1,2],[3,3]]))

labels_layer.features = {
    "label": [1, 2, 3],
    "very_wide_column_name_1": [1/3, 1/3, 1/3],
    "very_wide_column_name_2": [1/3, 1/3, 1/3],
    "very_wide_column_name_3": [1/3, 1/3, 1/3],
    "very_wide_column_name_4": [1/3, 1/3, 1/3],
    "very_wide_column_name_5": [1/3, 1/3, 1/3],
    "very_wide_column_name_6": [1/3, 1/3, 1/3],
    "very_wide_column_name_7": [1/3, 1/3, 1/3],
    "very_wide_column_name_8": [1/3, 1/3, 1/3],
    "very_wide_column_name_9": [1/3, 1/3, 1/3],
}
```
3. Afterwards, hover with your mouse over a label in the image.
4. Napari's screen window size will become very wide and the image jumps out of screen:


https://user-images.githubusercontent.com/12660498/208253942-fa83204e-f0cd-49a8-a14b-815b9216911d.mp4



## Expected behavior

Napari's window size does not change when the user hovers with their mouse over the label image.

## Environment

 - Please copy and paste the information at napari info option in help menubar here:

```
napari: 0.4.17
Platform: Windows-10-10.0.19045-SP0
Python: 3.9.15 | packaged by conda-forge | (main, Nov 22 2022, 08:41:22) [MSC v.1929 64 bit (AMD64)]
Qt: 5.15.6
PyQt5: 5.15.7
NumPy: 1.23.5
SciPy: 1.9.3
Dask: 2022.12.1
VisPy: 0.11.0
magicgui: 0.6.1
superqt: unknown
in-n-out: 0.1.6
app-model: 0.1.1
npe2: 0.6.1

OpenGL:
- GL version: 4.6.14802 Compatibility Profile Context 21.40.18.19 30.0.14018.19001
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1920x1200, scale 1.0

```

 - Any other relevant information:

I'm running on Windows 10

## Workaround:

```
mamba install napari=0.4.15 -c conda-forge
```


Discussion:
Addendum: Sometimes, this warning is printed out confirming that a resize event is the issue here:

WARNING: QWindowsWindow::setGeometry: Unable to set geometry 2654x698+1309+366 (frame: 2670x737+1301+335) on QWidgetWindow/"_QtMainWindowClassWindow" on "\\.\DISPLAY1". Resulting geometry: 1924x698+1309+366 (frame: 1940x737+1301+335) margins: 8, 31, 8, 8 minimum size: 374x557 MINMAXINFO maxSize=0,0 maxpos=0,0 mintrack=390,596 maxtrack=0,0)

One more workaround using [napari-skimage-regionprops 0.5.7](https://github.com/haesleinhuepf/napari-skimage-regionprops/releases/tag/0.5.7):

```
from napari_skimage_regionprops import add_table
add_table(labels_layer, viewer)
```
@haesleinhuepf It looks like there is a problem with the outdated superqt version. Could you install the latest superqt and check if the error is still present? 
The [latest version of superqt](https://anaconda.org/conda-forge/superqt) appears to be 0.4.1 which I have installed. 
Duplicate of #5177.
> The [latest version of superqt](https://anaconda.org/conda-forge/superqt) appears to be 0.4.1 which I have installed.

You are right. The issue is here https://github.com/pyapp-kit/superqt/issues/148

Resolving patch:
diff --git a/napari/_qt/widgets/qt_viewer_status_bar.py b/napari/_qt/widgets/qt_viewer_status_bar.py
--- 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)


Test patch: