Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
2f4e9536
Unverified
Commit
2f4e9536
authored
Aug 10, 2022
by
chunhtai
Committed by
GitHub
Aug 10, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implements browser context menu in selectable region (#108909)
parent
ddc08cf5
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
390 additions
and
5 deletions
+390
-5
_platform_selectable_region_context_menu_io.dart
.../widgets/_platform_selectable_region_context_menu_io.dart
+41
-0
_platform_selectable_region_context_menu_web.dart
...widgets/_platform_selectable_region_context_menu_web.dart
+143
-0
platform_selectable_region_context_menu.dart
.../src/widgets/platform_selectable_region_context_menu.dart
+5
-0
selectable_region.dart
packages/flutter/lib/src/widgets/selectable_region.dart
+18
-5
widgets.dart
packages/flutter/lib/widgets.dart
+1
-0
selectable_region_context_menu_test.dart
...ter/test/widgets/selectable_region_context_menu_test.dart
+172
-0
selectable_region_test.dart
packages/flutter/test/widgets/selectable_region_test.dart
+10
-0
No files found.
packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_io.dart
0 → 100644
View file @
2f4e9536
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The widget in this file is an empty mock for non-web platforms. See
// `_platform_selectable_region_context_menu_web.dart` for the web
// implementation.
import
'framework.dart'
;
import
'selection_container.dart'
;
/// A widget that provides native selection context menu for its child subtree.
///
/// This widget currently only supports Flutter web. Using this widget in non-web
/// platforms will throw [UnimplementedError]s.
///
/// In web platform, this widget registers a singleton platform view, i.e. a
/// HTML DOM element. The created platform view will be shared between all
/// [PlatformSelectableRegionContextMenu]s.
///
/// Only one [SelectionContainerDelegate] can attach to the
/// [PlatformSelectableRegionContextMenu] at a time. Use [attach] method to make
/// a [SelectionContainerDelegate] to be the active client.
class
PlatformSelectableRegionContextMenu
extends
StatelessWidget
{
/// Creates a [PlatformSelectableRegionContextMenu]
// ignore: prefer_const_constructors_in_immutables
PlatformSelectableRegionContextMenu
({
// ignore: avoid_unused_constructor_parameters
required
Widget
child
,
super
.
key
,
});
/// Attaches the `client` to be able to open platform-appropriate context menus.
static
void
attach
(
SelectionContainerDelegate
client
)
=>
throw
UnimplementedError
();
/// Detaches the `client` from the platform-appropriate selection context menus.
static
void
detach
(
SelectionContainerDelegate
client
)
=>
throw
UnimplementedError
();
@override
Widget
build
(
BuildContext
context
)
=>
throw
UnimplementedError
();
}
packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart
0 → 100644
View file @
2f4e9536
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:html'
as
html
;
import
'dart:ui'
as
ui
;
import
'package:flutter/rendering.dart'
;
import
'basic.dart'
;
import
'framework.dart'
;
import
'platform_view.dart'
;
import
'selection_container.dart'
;
const
String
_viewType
=
'Browser__WebContextMenuViewType__'
;
const
String
_kClassName
=
'web-electable-region-context-menu'
;
// These css rules hides the dom element with the class name.
const
String
_kClassSelectionRule
=
'.
$_kClassName
::selection { background: transparent; }'
;
const
String
_kClassRule
=
'''
.
$_kClassName
{
color: transparent;
user-select: text;
-webkit-user-select: text; /* Safari */
-moz-user-select: text; /* Firefox */
-ms-user-select: text; /* IE10+ */
}
'''
;
const
int
_kRightClickButton
=
2
;
typedef
_WebSelectionCallBack
=
void
Function
(
html
.
Element
,
html
.
MouseEvent
);
/// Function signature for `ui.platformViewRegistry.registerViewFactory`.
@visibleForTesting
typedef
RegisterViewFactory
=
void
Function
(
String
,
Object
Function
(
int
viewId
),
{
bool
isVisible
});
/// See `_platform_selectable_region_context_menu_io.dart` for full
/// documentation.
class
PlatformSelectableRegionContextMenu
extends
StatelessWidget
{
/// See `_platform_selectable_region_context_menu_io.dart`.
PlatformSelectableRegionContextMenu
({
required
this
.
child
,
super
.
key
,
})
{
if
(
_registeredViewType
==
null
)
{
_register
();
}
}
/// See `_platform_selectable_region_context_menu_io.dart`.
final
Widget
child
;
/// See `_platform_selectable_region_context_menu_io.dart`.
// ignore: use_setters_to_change_properties
static
void
attach
(
SelectionContainerDelegate
client
)
{
_activeClient
=
client
;
}
/// See `_platform_selectable_region_context_menu_io.dart`.
static
void
detach
(
SelectionContainerDelegate
client
)
{
if
(
_activeClient
!=
client
)
{
_activeClient
=
null
;
}
}
static
SelectionContainerDelegate
?
_activeClient
;
// Keeps track if this widget has already registered its view factories or not.
static
String
?
_registeredViewType
;
/// See `_platform_selectable_region_context_menu_io.dart`.
@visibleForTesting
// ignore: undefined_prefixed_name, invalid_assignment, avoid_dynamic_calls
static
RegisterViewFactory
registerViewFactory
=
ui
.
platformViewRegistry
.
registerViewFactory
;
// Registers the view factories for the interceptor widgets.
static
void
_register
()
{
assert
(
_registeredViewType
==
null
);
_registeredViewType
=
_registerWebSelectionCallback
((
html
.
Element
element
,
html
.
MouseEvent
event
)
{
final
SelectionContainerDelegate
?
client
=
_activeClient
;
if
(
client
!=
null
)
{
// Converts the html right click event to flutter coordinate.
final
Offset
localOffset
=
Offset
(
event
.
offset
.
x
.
toDouble
(),
event
.
offset
.
y
.
toDouble
());
final
Matrix4
transform
=
client
.
getTransformTo
(
null
);
final
Offset
globalOffset
=
MatrixUtils
.
transformPoint
(
transform
,
localOffset
);
client
.
dispatchSelectionEvent
(
SelectWordSelectionEvent
(
globalPosition:
globalOffset
));
// The innerText must contain the text in order to be selected by
// the browser.
element
.
innerText
=
client
.
getSelectedContent
()?.
plainText
??
''
;
// Programmatically select the dom element in browser.
final
html
.
Range
range
=
html
.
document
.
createRange
();
range
.
selectNode
(
element
);
final
html
.
Selection
?
selection
=
html
.
window
.
getSelection
();
if
(
selection
!=
null
)
{
selection
.
removeAllRanges
();
selection
.
addRange
(
range
);
}
}
});
}
static
String
_registerWebSelectionCallback
(
_WebSelectionCallBack
callback
)
{
registerViewFactory
(
_viewType
,
(
int
viewId
)
{
final
html
.
Element
htmlElement
=
html
.
DivElement
();
htmlElement
..
style
.
width
=
'100%'
..
style
.
height
=
'100%'
..
classes
.
add
(
_kClassName
);
// Create css style for _kClassName.
final
html
.
StyleElement
styleElement
=
html
.
StyleElement
();
html
.
document
.
head
!.
append
(
styleElement
);
final
html
.
CssStyleSheet
sheet
=
styleElement
.
sheet
!
as
html
.
CssStyleSheet
;
sheet
.
insertRule
(
_kClassRule
,
0
);
sheet
.
insertRule
(
_kClassSelectionRule
,
1
);
htmlElement
.
onMouseDown
.
listen
((
html
.
MouseEvent
event
)
{
if
(
event
.
button
!=
_kRightClickButton
)
{
return
;
}
callback
(
htmlElement
,
event
);
});
return
htmlElement
;
},
isVisible:
false
);
return
_viewType
;
}
@override
Widget
build
(
BuildContext
context
)
{
return
Stack
(
alignment:
Alignment
.
center
,
children:
<
Widget
>[
const
Positioned
.
fill
(
child:
HtmlElementView
(
viewType:
_viewType
,
),
),
child
,
],
);
}
}
packages/flutter/lib/src/widgets/platform_selectable_region_context_menu.dart
0 → 100644
View file @
2f4e9536
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export
'_platform_selectable_region_context_menu_io.dart'
if
(
dart
.
library
.
html
)
'_platform_selectable_region_context_menu_web.dart'
;
packages/flutter/lib/src/widgets/selectable_region.dart
View file @
2f4e9536
...
...
@@ -20,6 +20,7 @@ import 'framework.dart';
import
'gesture_detector.dart'
;
import
'media_query.dart'
;
import
'overlay.dart'
;
import
'platform_selectable_region_context_menu.dart'
;
import
'selection_container.dart'
;
import
'text_editing_intents.dart'
;
import
'text_selection.dart'
;
...
...
@@ -288,8 +289,14 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
void
_handleFocusChanged
()
{
if
(!
widget
.
focusNode
.
hasFocus
)
{
if
(
kIsWeb
)
{
PlatformSelectableRegionContextMenu
.
detach
(
_selectionDelegate
);
}
_clearSelection
();
}
if
(
kIsWeb
)
{
PlatformSelectableRegionContextMenu
.
attach
(
_selectionDelegate
);
}
}
void
_updateSelectionStatus
()
{
...
...
@@ -867,6 +874,16 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
@override
Widget
build
(
BuildContext
context
)
{
assert
(
debugCheckHasOverlay
(
context
));
Widget
result
=
SelectionContainer
(
registrar:
this
,
delegate:
_selectionDelegate
,
child:
widget
.
child
,
);
if
(
kIsWeb
)
{
result
=
PlatformSelectableRegionContextMenu
(
child:
result
,
);
}
return
CompositedTransformTarget
(
link:
_toolbarLayerLink
,
child:
RawGestureDetector
(
...
...
@@ -878,11 +895,7 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
child:
Focus
(
includeSemantics:
false
,
focusNode:
widget
.
focusNode
,
child:
SelectionContainer
(
registrar:
this
,
delegate:
_selectionDelegate
,
child:
widget
.
child
,
),
child:
result
,
),
),
),
...
...
packages/flutter/lib/widgets.dart
View file @
2f4e9536
...
...
@@ -87,6 +87,7 @@ export 'src/widgets/pages.dart';
export
'src/widgets/performance_overlay.dart'
;
export
'src/widgets/placeholder.dart'
;
export
'src/widgets/platform_menu_bar.dart'
;
export
'src/widgets/platform_selectable_region_context_menu.dart'
;
export
'src/widgets/platform_view.dart'
;
export
'src/widgets/preferred_size.dart'
;
export
'src/widgets/primary_scroll_controller.dart'
;
...
...
packages/flutter/test/widgets/selectable_region_context_menu_test.dart
0 → 100644
View file @
2f4e9536
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: undefined_class, undefined_getter, undefined_setter
@TestOn
(
'browser'
)
// This file contains web-only library.
import
'dart:html'
as
html
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
html
.
Element
?
element
;
final
RegisterViewFactory
originalFactory
=
PlatformSelectableRegionContextMenu
.
registerViewFactory
;
PlatformSelectableRegionContextMenu
.
registerViewFactory
=
(
String
viewType
,
Object
Function
(
int
viewId
)
fn
,
{
bool
isVisible
=
true
})
{
element
=
fn
(
0
)
as
html
.
Element
;
// The element needs to be attached to the document body to receive mouse
// events.
html
.
document
.
body
!.
append
(
element
!);
};
// This force register the dom element.
PlatformSelectableRegionContextMenu
(
child:
const
Placeholder
());
PlatformSelectableRegionContextMenu
.
registerViewFactory
=
originalFactory
;
test
(
'DOM element is set up correctly'
,
()
async
{
expect
(
element
,
isNotNull
);
expect
(
element
!.
style
.
width
,
'100%'
);
expect
(
element
!.
style
.
height
,
'100%'
);
expect
(
element
!.
classes
.
length
,
1
);
final
String
className
=
element
!.
classes
.
first
;
expect
(
html
.
document
.
head
!.
children
,
isNotEmpty
);
bool
foundStyle
=
false
;
for
(
final
html
.
Element
element
in
html
.
document
.
head
!.
children
)
{
if
(
element
is
!
html
.
StyleElement
)
{
continue
;
}
final
html
.
CssStyleSheet
sheet
=
element
.
sheet
!
as
html
.
CssStyleSheet
;
foundStyle
=
sheet
.
rules
!.
any
((
html
.
CssRule
rule
)
=>
rule
.
cssText
!.
contains
(
className
));
}
expect
(
foundStyle
,
isTrue
);
});
testWidgets
(
'right click can trigger select word'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
();
final
UniqueKey
spy
=
UniqueKey
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
SelectableRegion
(
focusNode:
focusNode
,
selectionControls:
materialTextSelectionControls
,
child:
SelectionSpy
(
key:
spy
),
),
)
);
expect
(
element
,
isNotNull
);
focusNode
.
requestFocus
();
await
tester
.
pump
();
// Dispatch right click.
element
!.
dispatchEvent
(
html
.
MouseEvent
(
'mousedown'
,
button:
2
,
clientX:
200
,
clientY:
300
,
),
);
final
RenderSelectionSpy
renderSelectionSpy
=
tester
.
renderObject
<
RenderSelectionSpy
>(
find
.
byKey
(
spy
));
expect
(
renderSelectionSpy
.
events
,
isNotEmpty
);
SelectWordSelectionEvent
?
selectWordEvent
;
for
(
final
SelectionEvent
event
in
renderSelectionSpy
.
events
)
{
if
(
event
is
SelectWordSelectionEvent
)
{
selectWordEvent
=
event
;
break
;
}
}
expect
(
selectWordEvent
,
isNotNull
);
expect
((
selectWordEvent
!.
globalPosition
.
dx
-
200
).
abs
()
<
precisionErrorTolerance
,
isTrue
);
expect
((
selectWordEvent
.
globalPosition
.
dy
-
300
).
abs
()
<
precisionErrorTolerance
,
isTrue
);
});
}
class
SelectionSpy
extends
LeafRenderObjectWidget
{
const
SelectionSpy
({
super
.
key
,
});
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
RenderSelectionSpy
(
SelectionContainer
.
maybeOf
(
context
),
);
}
@override
void
updateRenderObject
(
BuildContext
context
,
covariant
RenderObject
renderObject
)
{
}
}
class
RenderSelectionSpy
extends
RenderProxyBox
with
Selectable
,
SelectionRegistrant
{
RenderSelectionSpy
(
SelectionRegistrar
?
registrar
,
)
{
this
.
registrar
=
registrar
;
}
final
Set
<
VoidCallback
>
listeners
=
<
VoidCallback
>{};
List
<
SelectionEvent
>
events
=
<
SelectionEvent
>[];
@override
Size
get
size
=>
_size
;
Size
_size
=
Size
.
zero
;
@override
Size
computeDryLayout
(
BoxConstraints
constraints
)
{
_size
=
Size
(
constraints
.
maxWidth
,
constraints
.
maxHeight
);
return
_size
;
}
@override
void
addListener
(
VoidCallback
listener
)
=>
listeners
.
add
(
listener
);
@override
void
removeListener
(
VoidCallback
listener
)
=>
listeners
.
remove
(
listener
);
@override
SelectionResult
dispatchSelectionEvent
(
SelectionEvent
event
)
{
events
.
add
(
event
);
return
SelectionResult
.
end
;
}
@override
SelectedContent
?
getSelectedContent
()
{
return
const
SelectedContent
(
plainText:
'content'
);
}
@override
SelectionGeometry
get
value
=>
_value
;
SelectionGeometry
_value
=
SelectionGeometry
(
hasContent:
true
,
status:
SelectionStatus
.
uncollapsed
,
startSelectionPoint:
const
SelectionPoint
(
localPosition:
Offset
.
zero
,
lineHeight:
0.0
,
handleType:
TextSelectionHandleType
.
left
,
),
endSelectionPoint:
const
SelectionPoint
(
localPosition:
Offset
.
zero
,
lineHeight:
0.0
,
handleType:
TextSelectionHandleType
.
left
,
),
);
set
value
(
SelectionGeometry
other
)
{
if
(
other
==
_value
)
{
return
;
}
_value
=
other
;
for
(
final
VoidCallback
callback
in
listeners
)
{
callback
();
}
}
@override
void
pushHandleLayers
(
LayerLink
?
startHandle
,
LayerLink
?
endHandle
)
{
}
}
packages/flutter/test/widgets/selectable_region_test.dart
View file @
2f4e9536
...
...
@@ -1237,6 +1237,16 @@ class RenderSelectionSpy extends RenderProxyBox
final
Set
<
VoidCallback
>
listeners
=
<
VoidCallback
>{};
List
<
SelectionEvent
>
events
=
<
SelectionEvent
>[];
@override
Size
get
size
=>
_size
;
Size
_size
=
Size
.
zero
;
@override
Size
computeDryLayout
(
BoxConstraints
constraints
)
{
_size
=
Size
(
constraints
.
maxWidth
,
constraints
.
maxHeight
);
return
_size
;
}
@override
void
addListener
(
VoidCallback
listener
)
=>
listeners
.
add
(
listener
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment