Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drag and drop selection in editor #20565

Merged
merged 17 commits into from
Feb 21, 2017
Merged

Conversation

rebornix
Copy link
Member

@rebornix rebornix commented Feb 13, 2017

Fix #1046.

Click and hold the left mouse key on a selection now can be monitored but the implementation of moving the selection is not good. I'm creating a secondary cursor to show where the text will be pasted to however it ruins our undo/redo stack as the secondary cursor is a real cursor.

The ideal experience would be similar to Hover. Draw the virtual cursor by decoration and draw a popup window with the text being moved.

TODO:

  • 🏃 Refactor the code by putting the logic into a widget

Verify:

  • Single click on a selection
  • Single click besides a selection
  • Click, hold and move should still do selection. Verify both upwards and downwards.
  • Undo/Redo still works reasonably.

@mention-bot
Copy link

@rebornix, thanks for your PR! By analyzing the history of the files in this pull request, we identified @alexandrudima and @egamma to be potential reviewers.

@rebornix
Copy link
Member Author

Now I already stop adding secondary cursors when drag and move. Instead I put a new widget to simulate the cursor, which is somehow not pixel perfect but not that ugly.

The next step is moving the content cut and paste logic into that widget as well then our code is clean enough.

@rebornix rebornix changed the title [WIP] Drag and drop selection in editor Drag and drop selection in editor Feb 15, 2017
@rebornix
Copy link
Member Author

rebornix commented Feb 15, 2017

The dnd now works pretty reasonably in the editor. The target cursor you see while dragging is actually virtual, which looks the same as a secondary cursor but not a real cursor.

The catch here is I'm putting logic into Contrib part as I create content widget to mimic the cursor but in this way I have to listen to drag and drop related events. That's why I emit these two events in view controller but they are not 100% compatible with Browsers native drag/drop events (lack of dataTransfer).

A proper way to make the data transfer work is adding the dataTransfer object to related events in CommonCodeEditor, where we can access the editor and the model. Then we maintain our own implementation of drag events. Look forward to Alex's comment on this @alexandrudima

Copy link
Member

@alexdima alexdima left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty good!

Some high-level observations:

  • it should sit behind an option and be turned off by default
  • drop outside the editor or drop on a view zone or drop on a widget, etc. all kind of targets except content or perhaps content_empty should result in a no-op
  • the drop target should IMHO be rendered very differently than a regular cursor. I was confused by the drop target looking like a regular cursor.
  • if we want to go on the path of the drop target looking like a regular cursor, then we must change the CSS cursor: to something else, otherwise it is not clear that we are in a dnd operation

@@ -346,8 +346,10 @@ class MouseDownOperation extends Disposable {

private _mouseMoveMonitor: GlobalEditorMouseMoveMonitor;
private _mouseDownThenMoveEventHandler: EventGateKeeper<EditorMouseEvent>;
private _mouseDragThenMoveEventHandler: EventGateKeeper<EditorMouseEvent>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for a separate handler, I suggest to use mouseState.lastMouseDownEvent (which should be renamed to something better, see other comment) to differentiate the two cases inside onMouseDownThenMove

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed.

@@ -384,6 +386,12 @@ class MouseDownOperation extends Disposable {
() => !this._viewHelper.isDirty()
)
);
this._mouseDragThenMoveEventHandler = this._register(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for a separate handler, I suggest to code the dnd case inside onMouseDownThenMove

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed.

@@ -407,12 +415,30 @@ class MouseDownOperation extends Disposable {
this._dispatchMouse(position, true);
}

private _onMouseDragThenMove(e: EditorMouseEvent): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge into _onMouseDownThenMove and use mouseState.lastMouseDownEvent (which should be renamed to something better, see other comment) to differentiate the two cases

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed.

private _onMouseDragThenMove(e: EditorMouseEvent): void {
this._lastMouseEvent = e;
this._mouseState.setModifiers(e);
this._mouseState.setMouseDownEvent('drag');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this._mouseState.setMouseDownEvent('drag'); should move to the start method when the operation starts. The lastMouseDownEvent should be set only once when the operation starts inside start.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed.

&& this._currentSelection.containsPosition(position.position) // single click on a selection
) {
this._isActive = true;
this._dropTarget = this._createMouseTarget(e, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where this._mouseState.setMouseDownEvent('drag'); should be set

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed.

let newCursorPosition = new Position(targetPosition.lineNumber, targetPosition.column);

if (this._dragSelection.containsPosition(newCursorPosition)) {
let newSelections = this._editor.getSelections().map(selection => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code can be simplified once we guard in the mouseHandler to only begin dnd if the editor has precisely one selection

}
}

this._hideWidgets();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a single widget => _hideWidget

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed.

private _cursorStyle: TextEditorCursorStyle;
private _lineHeight: number;
private _typicalHalfwidthCharacterWidth: number;
private readonly _domNode: FastDomNode;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the compile error, use FastDomNode<HTMLElement>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed

this._domNode.setVisibility('hidden');
this._editor.addContentWidget(this);

this._cursorStyle = this._editor.getConfiguration().viewInfo.cursorStyle;
Copy link
Member

@alexdima alexdima Feb 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that we reuse here the cursor style and the cursor CSS. It makes it difficult to make changes in the CSS and e.g. it makes no sense to use a block cursor to paint the insertion point.

IMHO the insertion point should always be rendered as a line, regardless of user settings. It is not a cursor, it is an insertion point.

On top, I would render the insertion point in a distinct way than the cursor. I got very confused when I tried it out, it looks too much like a cursor, it should be orange or dashed or something special to let me know that it represents the drop point and is not some editor bug where a cursor is painted.

It should also go away if the mouse is not sitting directly on text (mouseEvent.target !== CONTENT). If the drag is on top of a view zone, the margin, etc. there should be no drop. IMHO, only on top of content.

this._domNode.setWidth(this._typicalHalfwidthCharacterWidth);
}

if (this._cursorStyle === TextEditorCursorStyle.Underline) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use its own styles, doing this is nasty and to be honest, confused me a lot. I couldn't tell apart the cursor from the drop hint.

@alexdima alexdima self-assigned this Feb 20, 2017
@rebornix
Copy link
Member Author

@alexandrudima I've made some updates to the code

it should sit behind an option and be turned off by default

Add editor.enableDragAndDrop

drop outside the editor or drop on a view zone or drop on a widget, etc. all kind of targets except content or perhaps content_empty should result in a no-op

Right now drop on elements other than content or content_empty of current editor will lead to no-op. When you drag and move the cursor, we still try to draw the target hint in the editor, just like what we did for selection.

the drop target should IMHO be rendered very differently than a regular cursor. I was confused by the drop target looking like a regular cursor.

Now I drop dependency to Cursor and use its own CSS. The reason that I leveraged original cursor style is to make sure it looks reasonable in different third party themes. Right now I used simple dotted line to represent the drop target.

About the comment you put in #20565 (comment) , the reason that I set this._mouseState.lastMouseDownEvent as drop only when users drag and move is because users might just single click on a selection and the outgoing events should only be mouseDown + mouseUp. So basically previous workflow is like

  • If user clicks on a selection, monitor MouseDownThenDrag, otherwise monitor MouseDownThenMove handler.
  • Emit mouseDown
  • When user drags, emit mouseDrag Event
  • When user stops dragging or moving or cancel, check whether it's a click or a drag. For the former, update cursor position; For the latter, emit mouseDrop event.

Right now we only update this._mouseState.lastMouseDownEvent in the start phrase. The workflow is like

  • User clicks on a selection
  • Emit MouseDrag Event, monitor MouseMove
  • Emit MouseDown (this is emitted always whether it's a drag or not)
  • Emit MouseUp when User releases the mouse
  • Emit MouseDrop and update the cursor

In this case, MouseDrag and MouseDrop events are always emitted even if users just click on a selection. But it's not a big deal as we hide these events from external. I don't favor this part so you may want to change it somehow.

Most comments are addressed, feel free to polish it :)

@alexdima
Copy link
Member

Very very nice!

@alexdima alexdima merged commit 66f06e0 into microsoft:master Feb 21, 2017
@alexdima alexdima added this to the February 2017 milestone Feb 21, 2017
@github-actions github-actions bot locked and limited conversation to collaborators Mar 27, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support to drag and drop selected text
4 participants