Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
36 changes: 29 additions & 7 deletions rayforge/ui_gtk/shared/status_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,32 @@


class StatusBar(Gtk.Box):
"""
Displays context sensitive shortcut hints in a single row.

Entries are added and removed whenever tool state changes, so their
combined width varies. The row is therefore hosted in a scrolled
window with an EXTERNAL horizontal policy: the bar never propagates
the content width into the window's size request. Otherwise such
state changes would resize the entire window while dragging in the
sketcher (observed on macOS, see issue #385). If the bar is too
narrow, entries are clipped instead of widening the window.
"""

def __init__(self, **kwargs):
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, **kwargs)
super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs)
self.add_css_class("status-bar")
self.set_spacing(24)

self._content = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL, spacing=24
)
self._scroller = Gtk.ScrolledWindow()
self._scroller.set_policy(
Gtk.PolicyType.EXTERNAL, Gtk.PolicyType.NEVER
)
self._scroller.set_child(self._content)
self._scroller.set_hexpand(True)
self.append(self._scroller)

def add_shortcut_entry(
self,
Expand All @@ -19,19 +41,19 @@ def add_shortcut_entry(
shortcut = Shortcut(
keys=keys, description=description, separator=separator
)
self.append(shortcut)
self._content.append(shortcut)

def add_separator(self):
"""Add a visual separator between shortcuts."""
separator = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
separator.set_size_request(1, 16)
separator.add_css_class("separator")
separator.get_style_context().add_class("separator")
self.append(separator)
self._content.append(separator)

def clear(self):
"""Remove all shortcuts from the status bar."""
child = self.get_first_child()
child = self._content.get_first_child()
while child is not None:
self.remove(child)
child = self.get_first_child()
self._content.remove(child)
child = self._content.get_first_child()
90 changes: 90 additions & 0 deletions tests/ui_gtk/shared/test_status_bar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# flake8: noqa: E402
"""UI tests for the shared StatusBar widget."""

import gi

gi.require_version("Gtk", "4.0")

from gi.repository import Gtk

import pytest

from rayforge.ui_gtk.shared.status_bar import StatusBar


def _measure_width(widget: Gtk.Widget) -> int:
minimum, _, _, _ = widget.measure(Gtk.Orientation.HORIZONTAL, -1)
return minimum


def _measure_height(widget: Gtk.Widget) -> int:
minimum, _, _, _ = widget.measure(Gtk.Orientation.VERTICAL, -1)
return minimum


def _count_children(box: Gtk.Box) -> int:
count = 0
child = box.get_first_child()
while child is not None:
count += 1
child = child.get_next_sibling()
return count


@pytest.mark.ui
def test_entries_are_added_to_content_row():
bar = StatusBar()
bar.add_shortcut_entry(["Space"], "Pan", separator="")
bar.add_shortcut_entry(["Shift", "Tab"], "Constrain to Axis")
assert _count_children(bar._content) == 2


@pytest.mark.ui
def test_clear_removes_all_entries():
bar = StatusBar()
bar.add_shortcut_entry(["Space"], "Pan", separator="")
bar.add_separator()
bar.add_shortcut_entry(["Shift"], "Constrain to Axis")
assert _count_children(bar._content) == 3

bar.clear()
assert _count_children(bar._content) == 0


@pytest.mark.ui
def test_min_width_does_not_follow_content():
"""
The status bar must not propagate the width of its shortcut entries
into the window's size request. Entry changes during sketch drags
would otherwise resize the whole window (issue #385).
"""
bar = StatusBar()
empty_width = _measure_width(bar)

bar.add_shortcut_entry(["Space"], "Pan", separator="")
idle_width = _measure_width(bar)

bar.add_shortcut_entry(
["Shift", "Doubleclick"],
"Select Connected with a very long description",
)
bar.add_shortcut_entry(
["Ctrl", "Shift", "Alt", "Super"],
"Yet another very long shortcut description",
)
assert _count_children(bar._content) == 3
assert idle_width == empty_width
assert _measure_width(bar) == idle_width


@pytest.mark.ui
def test_height_follows_content():
bar = StatusBar()
empty_height = _measure_height(bar)

bar.add_shortcut_entry(["Space"], "Pan", separator="")
idle_height = _measure_height(bar)
assert idle_height > empty_height

bar.clear()
assert _measure_height(bar) == empty_height
Loading