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

Add mouse/keyboard resizing of views

Open tig opened this issue 2 years ago • 16 comments

Right now, dragging is handled w/in TopLevel. This needs to change to be handled within Border (or in Application, but via an interface with Border).

  • [x] Define View.Movable - if true and view has a top Border, it can be dragged
  • [ ] Define View.Sizable - if true and a view has left/right/bottom Border it can be resized
  • [ ] Determine keyboard drag/size UI
  • [ ] Remove temporary hack in Application.cs: image

Related:

  • #https://github.com/gui-cs/Terminal.Gui/issues/2502.

My proposal:

Tiled Mode:

Mouse:

  • Dragging Border top or bottom resizes the view vertically
  • Dragging Border left or right resizes the view horizontally

Keyboard:

  • Hit Ctrl-F10 to enable drag/resize mode
  • A line will get a indicating it's active for resizing. Left/Right/Up/Down will size.
  • Tab to move to next line
  • Shift-Tab to move back
  • Ctrl-F10 to exit drag/resize mode

Overlapped mode:

Mouse:

  • Dragging Border top moves the view
  • Dragging Border bottom resizes the view vertically
  • Dragging Border left or right resizes the view horizontally

Keyboard:

  • Hit Ctrl-F10 to enable drag/resize mode
  • A line will get a indicating it's active for resizing.
    • If the selected line is Border top, then Left/Right/Up/Down will move the view.
    • If the selected line is Border bottom, left, or right, then Left/Right/Up/Down will size.
  • Tab to move to next line
  • Shift-Tab to move back
  • Ctrl-F10 to exit drag/resize mode

tig avatar Apr 13 '23 09:04 tig

This needs to fix #2536 too

tig avatar Apr 16 '23 15:04 tig

Based on @Nutzzz's excellent SpinnerView enhancments, I propose we use SpinnerView to show which border has focus for sizing instead of the diamond that @tznind originally used for TileView.

E.g. This one for horizontal: HNlkyoS 1

and this one for vertical: TubYa08 1

This will require LineCanvas to support hosting a SpinnerView as a point (where .Length == 0) I think.

tig avatar Apr 17 '23 15:04 tig

Y'know, I like the idea in general, but I'm personally not thrilled about those spinners in particular. I feel like one of these might be better (if not perfect).

[What do you think, 2-frame or 3-frame?]

2 frames, horizontal and vertical: Recording 2023-04-18 112921 3 frames, horizontal and vertical: Recording 2023-04-17 175433

Nutzzz avatar Apr 18 '23 19:04 Nutzzz

@BDisp this is another Issue related to

  • #2994

tig avatar Nov 26 '23 13:11 tig

If one of the goals of this feature is to support TGD resizing use cases then I think that the solution needs to be decoupled from Borders.

It must be possible to drag and resize any view (regardless of whether it has a Border). For example:

resize-updated

It also needs to be possible to drag multiple views at once e.g.

drag-multiple

tznind avatar Jun 01 '24 15:06 tznind

The relevant code in TGD is in:

MouseManager DragOperation DragMemento ResizeOperation

tznind avatar Jun 01 '24 15:06 tznind

If one of the goals of this feature is to support TGD resizing use cases then I think that the solution needs to be decoupled from Borders.

It must be possible to drag and resize any view (regardless of whether it has a Border). For example:

resize-updated resize-updated

It also needs to be possible to drag multiple views at once e.g.

drag-multiple drag-multiple

I'll start thinking about this...

I can't see anything really challenging, but it will require some thought...

Are you interested in collaborating with me on it?

tig avatar Jun 01 '24 19:06 tig

Are you interested in collaborating with me on it?

Definetly! lets see what we can do.

tznind avatar Jun 02 '24 01:06 tznind

Drag

Drag Phases (i.e. State)

In order to support Undo and other stuff (like cancelling an ongoing drag) I think we should keep state in the same way TGD does with DragOperation and it's DragMemento:

  • Only one drag operation can be running at once
  • Use ContinueDrag style

So the process is:

// Establishes the origin from which all vectors are measured
StartDrag (x,y)

// Updates positions of Views, each vector replaces the last (i.e. they are not additive)
ContinueDrag(new Vector(2,5))
ContinueDrag(new Vector(3,5))
ContinueDrag(new Vector(4,5))

// View is moved to x+4 y+5 and drag state is cleared
EndDrag()

Drag Multiple At Once

TGD lets you select multiple views with a selection box. I think this can be left in TGD. But we do need the ability to drag multiple views at once.

For sanity sake probably we should require that all views in a multiple drag should be in the same parent View.

Drag Boundary

When a drag results in some/all of a view spilling out of it's parent:

    /// <summary>
    /// True to allow dragging a view such that part/all of it is
    /// outside of its parent View's viewport
    /// </summary>
    bool AllowDragOffscreen;

Drag Initiation

There are 3 options for a View when it comes to initiating a drag

enum Dragability
{
    /// <summary>
    /// View cannot be dragged
    /// </summary>
    NotDraggable,

    /// <summary>
    /// View can be dragged by clicking and dragging at row 0 of
    /// the view.
    /// </summary>
    DragFromTop,

    /// <summary>
    /// The view can be dragged by clicking and dragging anywhere
    /// in the view
    /// </summary>
    DragFromAnywhere
}

Pos Types

When dragging we need to decide which Pos types can be moved and which cannot.

For example:

  • PosAbsolute (yes)
  • PosPercent (yes but be careful to translate new coordinates to keep as Percent)
  • PosFunc (no)
  • PosView (maybe)

Dragging a view with another that is PosView connected to it (e.g. Left of) needs considered also. Especially if dragging to another view.

Drag into another View

This is an opt in feature. It could be a global value or View specific. But if it is tied to Views then it makes multiple view drags more complicated.

bool AllowDragToOtherViews;

To support dragging a view into another view we must think:

  • Does it make sense to drag a view into a Button (probably not)
  • Does it make sense to drag a view into a FrameView (yes)

So we need some kind of confirmation probably an event with a Cancel member is easiest

event EventHandler<DragEventArgs> StartDrag;
event EventHandler<DragEventArgs> EndDrag;
class DragEventArgs
{
    /// <summary>
    /// Describes the views being moved and where they are being moved to.
    /// Note that you can use set to change which views are dragged e.g. to  drag multiple
    /// </summary>
    public DragOperation DragOperation { get; set; }

    /// <summary>
    /// Set to true to abandon the <see cref="DragOperation"/> and return
    /// all views to original positions
    /// </summary>
    public bool Cancel { get; set; }
}

Composite views

Some views have sub views that complicate click and drag. For example TabView. We need something more structured than the current hacky implementation I have:

/// <summary>
/// <para>
/// Sometimes <see cref="View.FindDeepestView"/> returns what the library considers
/// the clicked View rather than what the user would expect.  For example clicking in
/// the <see cref="Border"/> area of a <see cref="TabView"/>.
/// </para>
/// <para>This works out what the real view is.</para>
/// </summary>
/// <param name="hit"></param>
/// <returns></returns>
public static View? UnpackHitView(this View? hit)
{
    if (hit != null && hit.GetType().Name.Equals("TabRowView"))
    {
        hit = hit.SuperView;
    }

    // Translate clicks in the border as the real View being clicked
    if (hit is Border b)
    {
        hit = b.Parent;

    }

    // TabView nesting of 'fake' views goes:
    // TabView
    //   - TabViewRow
    //   - View (pane)
    //     - Border (note you need Parent not SuperView to find Border parent)

    if (hit?.SuperView is TabView tv)
    {
        hit = tv;
    }

    return hit;
}

tznind avatar Jun 02 '24 08:06 tznind

A few years ago I was interested in implementing mouse/keyboard drag and drop and resizing. Then I lost the will to continue. Indeed, @tznind is right in stating that some of the features only make sense in TGD, otherwise any user could create another IDE very easily if TG already provides it. For example, it doesn't make sense that a TextField can be resized or moved in TG, but only in TGD. Drag from FileDialog to a TextView is valid in TG to open a file but in TGD it is moving the FileDialog to the limits of the TextView. Drag from ListView over the selected item to a TextField is valid in TG, and a ContextMenu can be opened to choose to copy or move the item, but in TGD it is moving the ListView to the limits of the TextField, or possibly linking the TextField.Text with the selected item. In fact, the resizing and movement of a view must be decoupled from the border and only be coupled to the limits of the frame for resizing and within the limits of the frame for movement. In TG, only views that need to be resized and movable, their code must be overwritten in the border. In TGD the behavior will be normal, activating the necessary properties to be able to use them. In TG, by default, a view cannot be resizable or movable, but only in some derived classes, such as Window and Dialog, if they are configured for that purpose. If it is resizable then a border must exist in order to select the appropriate glyph on either side of its limits. If it is simultaneously movable, in addition to the border, the thickness of the top of the border must be at least two, which will allow the view to be moved if the mouse is placed from the second thickness. I'm glad this is being debated and will be very relevant for the advancement of this resource.

BDisp avatar Jun 02 '24 13:06 BDisp

Good stuff.

Not a lot of thoughts so far. But one...

Add a flag to Arrangement:

  • DesignMode

When set, effectively hides the Viewport from the mouse hit test code. Thus press and drag anywhere from Border-in works. Even if Border thickness is 0.

tig avatar Jun 02 '24 13:06 tig

Also, look at Highlight/HighlightMode. Can be extended to be combined with Arrangement.DesignMode to enable the whole view to be highlighted.

tig avatar Jun 02 '24 13:06 tig

But it would be useful if Arrangement.DesignMode was only visible internally, therefore only to TG and TGD.

BDisp avatar Jun 02 '24 13:06 BDisp

But it would be useful if Arrangement.DesignMode was only visible internally, therefore only to TG and TGD.

Why would we not want someone else to build a design-time tool like TGD?

tig avatar Jun 02 '24 21:06 tig

Why would we not want someone else to build a design-time tool like TGD?

I thought you wanted TGD exclusivity that belongs to gui-cs. If that's not the case then I take back what I said.

BDisp avatar Jun 02 '24 21:06 BDisp

It sucks so much for us that every platform and use tha forces us to have to have a "driver" to jump through different hoops to make this all happen. 😤

It's things like this that make libraries like TG so damn nice to use - abstracting away all the BS so the consumer can have a more consistent experience without worrying about it.

/gripe 😆

dodexahedron avatar Jun 04 '24 21:06 dodexahedron