2023-01-08 23:03:19 -07:00
|
|
|
package x
|
|
|
|
|
2023-03-27 18:44:39 -06:00
|
|
|
import "bytes"
|
2023-03-07 10:48:29 -07:00
|
|
|
import "image"
|
2023-03-27 18:44:39 -06:00
|
|
|
import "errors"
|
2023-02-01 23:47:55 -07:00
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
2023-01-08 23:03:19 -07:00
|
|
|
|
|
|
|
import "github.com/jezek/xgbutil"
|
|
|
|
import "github.com/jezek/xgb/xproto"
|
2023-03-27 18:44:39 -06:00
|
|
|
import "github.com/jezek/xgbutil/xprop"
|
2023-01-08 23:03:19 -07:00
|
|
|
import "github.com/jezek/xgbutil/xevent"
|
|
|
|
|
|
|
|
type scrollSum struct {
|
|
|
|
x, y int
|
|
|
|
}
|
|
|
|
|
2023-01-20 14:44:07 -07:00
|
|
|
const scrollDistance = 16
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) {
|
2023-01-20 14:44:07 -07:00
|
|
|
shift :=
|
|
|
|
(state & xproto.ModMaskShift) > 0 ||
|
|
|
|
(state & window.backend.modifierMasks.shiftLock) > 0
|
|
|
|
if shift {
|
|
|
|
switch button {
|
|
|
|
case 4:
|
|
|
|
sum.x -= scrollDistance
|
|
|
|
case 5:
|
|
|
|
sum.x += scrollDistance
|
|
|
|
case 6:
|
|
|
|
sum.y -= scrollDistance
|
|
|
|
case 7:
|
|
|
|
sum.y += scrollDistance
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
switch button {
|
|
|
|
case 4:
|
|
|
|
sum.y -= scrollDistance
|
|
|
|
case 5:
|
|
|
|
sum.y += scrollDistance
|
|
|
|
case 6:
|
|
|
|
sum.x -= scrollDistance
|
|
|
|
case 7:
|
|
|
|
sum.x += scrollDistance
|
|
|
|
}
|
2023-01-08 23:03:19 -07:00
|
|
|
}
|
2023-01-20 14:44:07 -07:00
|
|
|
|
2023-01-08 23:03:19 -07:00
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) handleExpose (
|
2023-01-26 00:08:07 -07:00
|
|
|
connection *xgbutil.XUtil,
|
|
|
|
event xevent.ExposeEvent,
|
|
|
|
) {
|
2023-03-07 10:48:29 -07:00
|
|
|
_, region := window.compressExpose(*event.ExposeEvent)
|
|
|
|
window.pushRegion(region)
|
2023-01-26 00:08:07 -07:00
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) handleConfigureNotify (
|
2023-01-08 23:03:19 -07:00
|
|
|
connection *xgbutil.XUtil,
|
|
|
|
event xevent.ConfigureNotifyEvent,
|
|
|
|
) {
|
2023-01-15 10:23:13 -07:00
|
|
|
if window.child == nil { return }
|
|
|
|
|
2023-01-08 23:03:19 -07:00
|
|
|
configureEvent := *event.ConfigureNotifyEvent
|
|
|
|
|
|
|
|
newWidth := int(configureEvent.Width)
|
|
|
|
newHeight := int(configureEvent.Height)
|
|
|
|
sizeChanged :=
|
|
|
|
window.metrics.width != newWidth ||
|
|
|
|
window.metrics.height != newHeight
|
|
|
|
window.metrics.width = newWidth
|
|
|
|
window.metrics.height = newHeight
|
|
|
|
|
|
|
|
if sizeChanged {
|
|
|
|
configureEvent = window.compressConfigureNotify(configureEvent)
|
|
|
|
window.metrics.width = int(configureEvent.Width)
|
|
|
|
window.metrics.height = int(configureEvent.Height)
|
|
|
|
window.reallocateCanvas()
|
|
|
|
window.resizeChildToFit()
|
2023-01-26 00:08:07 -07:00
|
|
|
|
|
|
|
if !window.exposeEventFollows(configureEvent) {
|
|
|
|
window.redrawChildEntirely()
|
|
|
|
}
|
2023-01-08 23:03:19 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) exposeEventFollows (event xproto.ConfigureNotifyEvent) (found bool) {
|
2023-01-26 00:08:07 -07:00
|
|
|
nextEvents := xevent.Peek(window.backend.connection)
|
|
|
|
if len(nextEvents) > 0 {
|
|
|
|
untypedEvent := nextEvents[0]
|
|
|
|
if untypedEvent.Err == nil {
|
|
|
|
typedEvent, ok :=
|
|
|
|
untypedEvent.Event.(xproto.ConfigureNotifyEvent)
|
|
|
|
|
|
|
|
if ok && typedEvent.Window == event.Window {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) modifiersFromState (
|
2023-01-20 15:40:28 -07:00
|
|
|
state uint16,
|
|
|
|
) (
|
2023-02-01 23:47:55 -07:00
|
|
|
modifiers input.Modifiers,
|
2023-01-20 15:40:28 -07:00
|
|
|
) {
|
2023-02-01 23:47:55 -07:00
|
|
|
return input.Modifiers {
|
2023-01-20 15:40:28 -07:00
|
|
|
Shift:
|
|
|
|
(state & xproto.ModMaskShift) > 0 ||
|
|
|
|
(state & window.backend.modifierMasks.shiftLock) > 0,
|
|
|
|
Control: (state & xproto.ModMaskControl) > 0,
|
|
|
|
Alt: (state & window.backend.modifierMasks.alt) > 0,
|
|
|
|
Meta: (state & window.backend.modifierMasks.meta) > 0,
|
|
|
|
Super: (state & window.backend.modifierMasks.super) > 0,
|
|
|
|
Hyper: (state & window.backend.modifierMasks.hyper) > 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) handleKeyPress (
|
2023-01-08 23:03:19 -07:00
|
|
|
connection *xgbutil.XUtil,
|
|
|
|
event xevent.KeyPressEvent,
|
|
|
|
) {
|
2023-01-20 14:44:07 -07:00
|
|
|
if window.child == nil { return }
|
2023-03-23 23:31:40 -06:00
|
|
|
if window.hasModal { return }
|
2023-01-08 23:03:19 -07:00
|
|
|
|
|
|
|
keyEvent := *event.KeyPressEvent
|
|
|
|
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
|
2023-01-20 15:40:28 -07:00
|
|
|
modifiers := window.modifiersFromState(keyEvent.State)
|
|
|
|
modifiers.NumberPad = numberPad
|
2023-01-08 23:03:19 -07:00
|
|
|
|
2023-02-01 23:47:55 -07:00
|
|
|
if key == input.KeyTab && modifiers.Alt {
|
|
|
|
if child, ok := window.child.(elements.Focusable); ok {
|
|
|
|
direction := input.KeynavDirectionForward
|
2023-01-15 10:23:13 -07:00
|
|
|
if modifiers.Shift {
|
2023-02-01 23:47:55 -07:00
|
|
|
direction = input.KeynavDirectionBackward
|
2023-01-11 16:32:02 -07:00
|
|
|
}
|
|
|
|
|
2023-01-30 15:01:47 -07:00
|
|
|
if !child.HandleFocus(direction) {
|
|
|
|
child.HandleUnfocus()
|
2023-01-16 10:21:47 -07:00
|
|
|
}
|
2023-01-11 16:32:02 -07:00
|
|
|
}
|
2023-02-01 23:47:55 -07:00
|
|
|
} else if child, ok := window.child.(elements.KeyboardTarget); ok {
|
2023-01-20 15:40:28 -07:00
|
|
|
child.HandleKeyDown(key, modifiers)
|
2023-01-11 16:32:02 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) handleKeyRelease (
|
2023-01-08 23:03:19 -07:00
|
|
|
connection *xgbutil.XUtil,
|
|
|
|
event xevent.KeyReleaseEvent,
|
|
|
|
) {
|
|
|
|
if window.child == nil { return }
|
|
|
|
|
|
|
|
keyEvent := *event.KeyReleaseEvent
|
2023-01-20 15:40:28 -07:00
|
|
|
|
|
|
|
// do not process this event if it was generated from a key repeat
|
|
|
|
nextEvents := xevent.Peek(window.backend.connection)
|
|
|
|
if len(nextEvents) > 0 {
|
|
|
|
untypedEvent := nextEvents[0]
|
|
|
|
if untypedEvent.Err == nil {
|
|
|
|
typedEvent, ok :=
|
2023-02-09 15:26:36 -07:00
|
|
|
untypedEvent.Event.(xproto.KeyPressEvent)
|
2023-01-20 15:40:28 -07:00
|
|
|
|
|
|
|
if ok && typedEvent.Detail == keyEvent.Detail &&
|
|
|
|
typedEvent.Event == keyEvent.Event &&
|
|
|
|
typedEvent.State == keyEvent.State {
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2023-01-08 23:03:19 -07:00
|
|
|
}
|
2023-01-15 10:23:13 -07:00
|
|
|
|
2023-01-20 15:40:28 -07:00
|
|
|
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
|
|
|
|
modifiers := window.modifiersFromState(keyEvent.State)
|
|
|
|
modifiers.NumberPad = numberPad
|
|
|
|
|
2023-02-01 23:47:55 -07:00
|
|
|
if child, ok := window.child.(elements.KeyboardTarget); ok {
|
2023-01-15 10:23:13 -07:00
|
|
|
child.HandleKeyUp(key, modifiers)
|
|
|
|
}
|
2023-01-08 23:03:19 -07:00
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) handleButtonPress (
|
2023-01-08 23:03:19 -07:00
|
|
|
connection *xgbutil.XUtil,
|
|
|
|
event xevent.ButtonPressEvent,
|
|
|
|
) {
|
|
|
|
if window.child == nil { return }
|
2023-03-23 23:31:40 -06:00
|
|
|
if window.hasModal { return }
|
2023-01-08 23:03:19 -07:00
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
buttonEvent := *event.ButtonPressEvent
|
|
|
|
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 {
|
|
|
|
if child, ok := window.child.(elements.ScrollTarget); ok {
|
2023-01-15 10:23:13 -07:00
|
|
|
sum := scrollSum { }
|
2023-01-20 14:44:07 -07:00
|
|
|
sum.add(buttonEvent.Detail, window, buttonEvent.State)
|
2023-01-15 10:23:13 -07:00
|
|
|
window.compressScrollSum(buttonEvent, &sum)
|
2023-03-14 16:54:24 -06:00
|
|
|
child.HandleScroll (
|
2023-01-15 10:23:13 -07:00
|
|
|
int(buttonEvent.EventX),
|
|
|
|
int(buttonEvent.EventY),
|
|
|
|
float64(sum.x), float64(sum.y))
|
2023-03-14 16:54:24 -06:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if child, ok := window.child.(elements.MouseTarget); ok {
|
2023-01-15 10:23:13 -07:00
|
|
|
child.HandleMouseDown (
|
|
|
|
int(buttonEvent.EventX),
|
|
|
|
int(buttonEvent.EventY),
|
2023-02-01 23:47:55 -07:00
|
|
|
input.Button(buttonEvent.Detail))
|
2023-01-15 10:23:13 -07:00
|
|
|
}
|
2023-01-08 23:03:19 -07:00
|
|
|
}
|
2023-01-15 10:23:13 -07:00
|
|
|
|
2023-01-08 23:03:19 -07:00
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) handleButtonRelease (
|
2023-01-08 23:03:19 -07:00
|
|
|
connection *xgbutil.XUtil,
|
|
|
|
event xevent.ButtonReleaseEvent,
|
|
|
|
) {
|
2023-01-15 10:23:13 -07:00
|
|
|
if window.child == nil { return }
|
|
|
|
|
2023-02-01 23:47:55 -07:00
|
|
|
if child, ok := window.child.(elements.MouseTarget); ok {
|
2023-01-15 10:23:13 -07:00
|
|
|
buttonEvent := *event.ButtonReleaseEvent
|
|
|
|
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
|
|
|
|
child.HandleMouseUp (
|
|
|
|
int(buttonEvent.EventX),
|
|
|
|
int(buttonEvent.EventY),
|
2023-02-01 23:47:55 -07:00
|
|
|
input.Button(buttonEvent.Detail))
|
2023-01-15 10:23:13 -07:00
|
|
|
}
|
2023-01-08 23:03:19 -07:00
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) handleMotionNotify (
|
2023-01-08 23:03:19 -07:00
|
|
|
connection *xgbutil.XUtil,
|
|
|
|
event xevent.MotionNotifyEvent,
|
|
|
|
) {
|
2023-01-15 10:23:13 -07:00
|
|
|
if window.child == nil { return }
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
if child, ok := window.child.(elements.MotionTarget); ok {
|
2023-01-15 10:23:13 -07:00
|
|
|
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
|
2023-03-14 16:54:24 -06:00
|
|
|
child.HandleMotion (
|
2023-01-15 10:23:13 -07:00
|
|
|
int(motionEvent.EventX),
|
|
|
|
int(motionEvent.EventY))
|
|
|
|
}
|
2023-01-08 23:03:19 -07:00
|
|
|
}
|
|
|
|
|
2023-03-27 18:44:39 -06:00
|
|
|
func (window *window) handleSelectionNotify (
|
|
|
|
connection *xgbutil.XUtil,
|
|
|
|
event xevent.SelectionNotifyEvent,
|
|
|
|
) {
|
|
|
|
// Follow:
|
|
|
|
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.4
|
|
|
|
if window.selectionRequest == nil { return }
|
2023-03-27 23:00:54 -06:00
|
|
|
die := func (err error) {
|
|
|
|
window.selectionRequest(nil, err)
|
|
|
|
window.selectionRequest = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the property argument is None, the conversion has been refused.
|
|
|
|
// This can mean either that there is no owner for the selection, that
|
|
|
|
// the owner does not support the conversion implied by the target, or
|
|
|
|
// that the server did not have sufficient space to accommodate the
|
|
|
|
// data.
|
|
|
|
if event.Property == 0 { die(nil); return }
|
2023-03-27 18:44:39 -06:00
|
|
|
|
|
|
|
// When using GetProperty to retrieve the value of a selection, the
|
|
|
|
// property argument should be set to the corresponding value in the
|
|
|
|
// SelectionNotify event. Because the requestor has no way of knowing
|
|
|
|
// beforehand what type the selection owner will use, the type argument
|
|
|
|
// should be set to AnyPropertyType. Several GetProperty requests may be
|
|
|
|
// needed to retrieve all the data in the selection; each should set the
|
|
|
|
// long-offset argument to the amount of data received so far, and the
|
|
|
|
// size argument to some reasonable buffer size (see section 2.5). If
|
|
|
|
// the returned value of bytes-after is zero, the whole property has
|
|
|
|
// been transferred.
|
|
|
|
reply, err := xproto.GetProperty (
|
|
|
|
connection.Conn(), false, window.xWindow.Id, event.Property,
|
|
|
|
xproto.GetPropertyTypeAny, 0, (1 << 32) - 1).Reply()
|
|
|
|
if err != nil { die(err); return }
|
|
|
|
if reply.Format == 0 {
|
|
|
|
die(errors.New("x: missing selection property"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Once all the data in the selection has been retrieved (which may
|
|
|
|
// require getting the values of several properties &emdash; see section
|
|
|
|
// 2.7), the requestor should delete the property in the SelectionNotify
|
|
|
|
// request by using a GetProperty request with the delete argument set
|
|
|
|
// to True. As previously discussed, the owner has no way of knowing
|
|
|
|
// when the data has been transferred to the requestor unless the
|
|
|
|
// property is removed.
|
|
|
|
propertyAtom, err := xprop.Atm(window.backend.connection, "TOMO_SELECTION")
|
|
|
|
if err != nil { die(err); return }
|
|
|
|
err = xproto.DeletePropertyChecked (
|
|
|
|
window.backend.connection.Conn(),
|
|
|
|
window.xWindow.Id,
|
|
|
|
propertyAtom).Check()
|
|
|
|
if err != nil { die(err); return }
|
|
|
|
|
|
|
|
// TODO: possibly do some conversion here?
|
|
|
|
window.selectionRequest(bytes.NewReader(reply.Value), nil)
|
|
|
|
window.selectionRequest = nil
|
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) compressExpose (
|
2023-01-26 00:08:07 -07:00
|
|
|
firstEvent xproto.ExposeEvent,
|
|
|
|
) (
|
|
|
|
lastEvent xproto.ExposeEvent,
|
2023-03-07 10:48:29 -07:00
|
|
|
region image.Rectangle,
|
2023-01-26 00:08:07 -07:00
|
|
|
) {
|
2023-03-07 10:48:29 -07:00
|
|
|
region = image.Rect (
|
|
|
|
int(firstEvent.X), int(firstEvent.Y),
|
|
|
|
int(firstEvent.X + firstEvent.Width),
|
|
|
|
int(firstEvent.Y + firstEvent.Height))
|
|
|
|
|
2023-01-26 00:08:07 -07:00
|
|
|
window.backend.connection.Sync()
|
|
|
|
xevent.Read(window.backend.connection, false)
|
|
|
|
lastEvent = firstEvent
|
|
|
|
|
|
|
|
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
|
|
|
if untypedEvent.Err != nil { continue }
|
|
|
|
|
|
|
|
typedEvent, ok := untypedEvent.Event.(xproto.ExposeEvent)
|
|
|
|
if !ok { continue }
|
|
|
|
|
|
|
|
if firstEvent.Window == typedEvent.Window {
|
2023-03-07 10:48:29 -07:00
|
|
|
region = region.Union (image.Rect (
|
|
|
|
int(typedEvent.X), int(typedEvent.Y),
|
|
|
|
int(typedEvent.X + typedEvent.Width),
|
|
|
|
int(typedEvent.Y + typedEvent.Height)))
|
|
|
|
|
2023-01-26 00:08:07 -07:00
|
|
|
lastEvent = typedEvent
|
|
|
|
defer func (index int) {
|
|
|
|
xevent.DequeueAt(window.backend.connection, index)
|
|
|
|
} (index)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
2023-01-08 23:03:19 -07:00
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) compressConfigureNotify (
|
2023-01-08 23:03:19 -07:00
|
|
|
firstEvent xproto.ConfigureNotifyEvent,
|
|
|
|
) (
|
|
|
|
lastEvent xproto.ConfigureNotifyEvent,
|
|
|
|
) {
|
|
|
|
window.backend.connection.Sync()
|
|
|
|
xevent.Read(window.backend.connection, false)
|
|
|
|
lastEvent = firstEvent
|
|
|
|
|
|
|
|
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
|
|
|
if untypedEvent.Err != nil { continue }
|
|
|
|
|
|
|
|
typedEvent, ok := untypedEvent.Event.(xproto.ConfigureNotifyEvent)
|
|
|
|
if !ok { continue }
|
|
|
|
|
|
|
|
if firstEvent.Event == typedEvent.Event &&
|
|
|
|
firstEvent.Window == typedEvent.Window {
|
|
|
|
|
|
|
|
lastEvent = typedEvent
|
|
|
|
defer func (index int) {
|
|
|
|
xevent.DequeueAt(window.backend.connection, index)
|
|
|
|
} (index)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) compressScrollSum (
|
2023-01-08 23:03:19 -07:00
|
|
|
firstEvent xproto.ButtonPressEvent,
|
|
|
|
sum *scrollSum,
|
|
|
|
) {
|
|
|
|
window.backend.connection.Sync()
|
|
|
|
xevent.Read(window.backend.connection, false)
|
|
|
|
|
|
|
|
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
|
|
|
if untypedEvent.Err != nil { continue }
|
|
|
|
|
|
|
|
typedEvent, ok := untypedEvent.Event.(xproto.ButtonPressEvent)
|
|
|
|
if !ok { continue }
|
|
|
|
|
|
|
|
if firstEvent.Event == typedEvent.Event &&
|
|
|
|
typedEvent.Detail >= 4 &&
|
|
|
|
typedEvent.Detail <= 7 {
|
|
|
|
|
2023-01-20 14:44:07 -07:00
|
|
|
sum.add(typedEvent.Detail, window, typedEvent.State)
|
2023-01-08 23:03:19 -07:00
|
|
|
defer func (index int) {
|
|
|
|
xevent.DequeueAt(window.backend.connection, index)
|
|
|
|
} (index)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-03-14 16:54:24 -06:00
|
|
|
func (window *window) compressMotionNotify (
|
2023-01-08 23:03:19 -07:00
|
|
|
firstEvent xproto.MotionNotifyEvent,
|
|
|
|
) (
|
|
|
|
lastEvent xproto.MotionNotifyEvent,
|
|
|
|
) {
|
|
|
|
window.backend.connection.Sync()
|
|
|
|
xevent.Read(window.backend.connection, false)
|
|
|
|
lastEvent = firstEvent
|
|
|
|
|
|
|
|
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
|
|
|
if untypedEvent.Err != nil { continue }
|
|
|
|
|
|
|
|
typedEvent, ok := untypedEvent.Event.(xproto.MotionNotifyEvent)
|
|
|
|
if !ok { continue }
|
|
|
|
|
2023-01-20 23:24:24 -07:00
|
|
|
if firstEvent.Event == typedEvent.Event {
|
2023-01-08 23:03:19 -07:00
|
|
|
lastEvent = typedEvent
|
|
|
|
defer func (index int) {
|
|
|
|
xevent.DequeueAt(window.backend.connection, index)
|
|
|
|
} (index)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|