Terminal.Gui icon indicating copy to clipboard operation
Terminal.Gui copied to clipboard

`Popover` - A consistent way of enabling a Subview to popup outside of a View

Open tig opened this issue 1 year ago • 6 comments

(Very drafty for now... just some thoughts).

This Issue is a proposal for how to build into TG v2 a consistent way for a View to have a UI element show itself outside of the View's Viewport.

Popover - A View that that is displayed outside of the View that owns it's Viewport.   There are at least these use-cases of Popovers:

Autocomplete Popup

TextView would like a list of autocomplete items to be displayed below the view, at the cursor position as the user types. The user can use mouse/keyboard to select an autocomplete item and the pop over disappears.

Current Implementation

  • Adds _popup (a ListView) to this.SuperView
  • Uses Visible to show/hide
  • (In #3627 I've had to change the implementation so _popup only gets removed/disposed when the host is removed/disposed).  

Drop-down Combobox

Current Implementation

  • Does not support pop-over; The ComboBox needs to be sized to allow the drop-down to show if HideDropdownListOnClick is true.
  • _listview (of type ComboListView) is a subview of Combobox
  • ComboListView overrides OnMouseEvent (which is internal!) with a bunch of hackery to deal with when it should be displayed

Menus (from MenuBar)

Current Implementation

  • MenuBar is a "special" subview of Application.Top (Toplevel has knowledge of it).
  • When MenuBar is activated (via keyboard/mouse/api) it creates instances of Menu and adds them to Top
  • Both MenuBar and Menu have convoluted logic for sensing when the menus should close (via mouse & keyboard).
  • When an open Menu closes, it is removed from Top

Context Menus

Current Implementation

  • Uses a "fake" MenuBar that is created when Show is called.
  • _menuBar is never actually added to any views; BeginInit/EndInit are called manually.

Tooltip

As a user, when I hover the mouse over a View, I'd like a temporary popup to appear providing a "tip" regarding the View's purpose.

Current Implementation

  • Not currently implemented

Proposal

Tenets (Unless you know better ones...)

  • There can only be one - Just like in Highlander, there can only be one Popover visible and active to the user at a time. None of the use-cases above lead to needing to have more than one, thus this is a simplifier. [^1]
  • Its Rude to Mess With Someone Elses' Subviews - Asking View developers to have to code defensively around the fact that some Subview they add, or some other agent, is adding/removing subviews is just bad juju. It leads to bugs in object lifecycles and requires skill to get right (e.g. adding a Subview during draw can result in Subviews being changed during a foreach iteration). In this design we will ensure there's no need to mess with another View's Subviews.
  • Any View can be a Popover - The design should not require a View that "Pops Over" be coded specially for that purpose.

Design

At the Application level we'll add a peer of RunState.Toplevel named RunState.Popover:

  • In the run loop, after RunState.Top (remember, Current goes away with #2491) Top.Draw() is called, if Popover.Visible we'll call Popover.Draw(). This will ensure the Popover is always drawn over everything else.
  • In keyboard handling, if Popover.Visible && Popover.HasFocus we'll give Popover a chance to handle key events.
  • In mouse handling, if Popover.VIsible and Popover == Applicaiton.MouseGrabView we'll let it have mouse events. If a click happens outside of Popover.Frame we set Popover.Visible = false.
  • In focus/navigation handling, if Popover.HasFocus, if any other View gets focus, we'll set Popover.Visible = false.
  • For layout, whenever Top.LayoutSubviews is called, we'll then call Popover.SetRelativeLayout
  • Two APIs:
    • Applicaton.ShowPopover (View popoverView)
public static bool ShowPopover (View popoverView) 
{
   if (RunState.Popover is {})
   {
      RunState.Popover.Visible = false;
   }

   if (!popoverView.IsInitialized)
   {
      popoverView.BeginInit();
      popoverView.EndInit();
   }

   RunState.Popover = popoverView;
   RunState.Popover.Visible = true;
   RunState.Popover.SetRelativeLayout(screen);
}
  • Application.HidePopover ()
public static void HidePopover () 
{
   if (RunState.Popover is {})
   {
      RunState.Popover.Visible = false;
   }
}

View will have know knowledge of the popover concept.

Any View subclass that wants a Popover, will:

  • _myPopover = new MyPopoverView();
  • Note, _myPopover will always have this.SuperView is null.
  • Any time the popover should be displayed:
    • Set _myPopover.X/Y/Width/Height to ??? (NEED TO FIGURE THIS OUT)
    • Call Application.ShowPopover(_myPopover)
  • If the view wants to explicitly hide the popover,
    • Call Application.HidePopover()   [^1]: I've considered "As a user, when press a special key, I'd like all visible Views to show temporary popup providing a "tip" regarding the View's purpose." This could be implemented by having the container view showing a Popover who's job it was to show "tips" for each subview.

tig avatar Aug 24 '24 15:08 tig

  • There can only be one - Just like in Highlander, there can only be one Popover visible and active to the user at a time. None of the use-cases above lead to needing to have more than one, thus this is a simplifier. 1

If I remember, only occur to me that ComboBox has an option to let the dropdown list always visible. How you'll deal about that?

I love the Tooltip implementation and all the others stuff.

BDisp avatar Aug 24 '24 16:08 BDisp

  • There can only be one - Just like in Highlander, there can only be one Popover visible and active to the user at a time. None of the use-cases above lead to needing to have more than one, thus this is a simplifier. 1

If I remember, only occur to me that ComboBox has an option to let the dropdown list always visible. How you'll deal about that?

If the list is always available it is not a "Dropdown Combobox", but just a ComboBox. The current implementation doesn't really provide a "Dropdown Combobox".

tig avatar Aug 24 '24 17:08 tig

Q: What's the difference between a Modal and a Popover?

  • The showing of a Modal is blocking (synchronous) to the caller (via Run). The showing of Popover is not blocking.

Q: Can't I use a Modal for something like the Autocomplete popup?

  • No. If you did, then while it was shown, the user couldn't continue to type in the TextField

Q: But, can Modal be implemented using the Popover support described above?

  • Maybe. Worth thinking through.

tig avatar Aug 24 '24 17:08 tig

  • In focus/navigation handling, if Popover.HasFocus, if any other View gets focus, we'll set Popover.Visible = false.

This should say "if any other View than the Popover's owner gets focus".

Otherwise, how would the TextViewAutoComplete popover work?

tig avatar Aug 24 '24 17:08 tig

If the list is always available it is not a "Dropdown Combobox", but just a ComboBox. The current implementation doesn't really provide a "Dropdown Combobox".

I have a suggestion for this. If it's a "Dropdown Combobox" then Popover will be used. With a fixed ComboBox that always has the ListView visible, then ComboBox must have the available dimension to acommodate the TextField and the ListView.

BDisp avatar Aug 24 '24 17:08 BDisp

If the list is always available it is not a "Dropdown Combobox", but just a ComboBox. The current implementation doesn't really provide a "Dropdown Combobox".

I have a suggestion for this. If it's a "Dropdown Combobox" then Popover will be used. With a fixed ComboBox that always has the ListView visible, then ComboBox must have the available dimension to acommodate the TextField and the ListView.

Yes!

tig avatar Aug 24 '24 18:08 tig