Skip to content

DropDownList leaves stale popover contents after handled Enter #5635

Description

@YourRobotOverlord

Terminal.Gui version

2.4.18-develop.46 (b5c08b3f91e0c066d5d78df71944f3d58009b166)

Observed on Windows. Other drivers have not been tested.

Description

A read-only DropDownList can leave the dropped list's text behind after an item is accepted with Enter when the application handles Accepting and moves focus. The selection and focus change occur correctly, and the popover is no longer active, but the cells it previously covered are not repainted.

Selecting the same item with the mouse does not leave stale content.

Minimal pattern

DropDownList dropDown = new ()
{
    ReadOnly = true,
    Source = new ListWrapper<string> (
        new ObservableCollection<string> (["Labor", "Fuel/Energy", "Equipment"]))
};

ListView nextView = new ();

dropDown.Accepting += (_, e) =>
{
    // Prevent Command.Accept from bubbling to and accepting the containing Dialog.
    e.Handled = true;
    nextView.SetFocus ();
};

Steps to reproduce

  1. Place the controls above in a Dialog and run it.
  2. Open the dropdown using the keyboard.
  3. Move to another item and press Enter.
  4. Observe that focus moves and the dropdown closes, but some option text remains drawn over the controls below it.
  5. Repeat using the mouse to select an item; the covered area is repainted correctly.

Expected behavior

Whenever the popover is removed after focus changes, its former screen region should be repainted regardless of whether selection was made with the keyboard or mouse.

Suspected cause

DropDownList.OnHasFocusChanging calls App?.Popovers?.DeRegister (_listPopover) when losing focus:

protected override bool OnHasFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused)
{
if (base.OnHasFocusChanging (currentHasFocus, newHasFocus, currentFocused, newFocused))
{
return true;
}
if (newHasFocus)
{
App?.Popovers?.Register (_listPopover);
return false;
}
App?.Popovers?.DeRegister (_listPopover);
return false;

ApplicationPopover.DeRegister removes the active popover without hiding it or invalidating the underlying runnable view:

public bool DeRegister (IPopoverView? popover)
{
if (popover is null || !IsRegistered (popover))
{
return false;
}
if (GetActivePopover () == popover)
{
_activePopover = null;
}
_popovers.Remove (popover);
PopoverDeRegistered?.Invoke (this, new EventArgs<IPopoverView> (popover));
return true;

In contrast, ApplicationPopover.Hide calls SetNeedsDraw() on the top runnable view:

public void Hide (IPopoverView? popover = null)
{
popover ??= GetActivePopover ();
if (_activePopover != popover)
{
return;
}
// If there's an existing popover, hide it.
_activePopover = null;
popover?.Visible = false;
// Need View cast for TopRunnableView access
if (popover is View popoverView)
{
popoverView.App?.TopRunnableView?.SetNeedsDraw ();
}

The mouse selection path hides the popover through OnActivated, while this handled keyboard path loses focus and reaches the deregistration path.

Workaround

Explicitly invalidating the containing dialog after moving focus clears the stale cells:

dropDown.Accepting += (_, e) =>
{
    e.Handled = true;
    nextView.SetFocus ();
    dialog.SetNeedsDraw ();
};

A possible library-level fix may be to hide the active popover before deregistering it on focus loss, or otherwise invalidate the region it covered.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions