2019-03-08 04:04:46 +00:00
|
|
|
package layout
|
2019-01-31 00:02:38 +00:00
|
|
|
|
|
|
|
import (
|
2019-03-14 03:01:44 +00:00
|
|
|
ui "github.com/gizak/termui/v3"
|
2019-03-08 04:04:46 +00:00
|
|
|
"github.com/sqshq/sampler/component"
|
2019-02-24 04:49:09 +00:00
|
|
|
"github.com/sqshq/sampler/component/runchart"
|
2019-02-17 01:54:48 +00:00
|
|
|
"github.com/sqshq/sampler/config"
|
2019-02-11 02:51:55 +00:00
|
|
|
"github.com/sqshq/sampler/console"
|
2019-03-16 04:35:00 +00:00
|
|
|
"github.com/sqshq/sampler/data"
|
2019-03-02 21:30:16 +00:00
|
|
|
"image"
|
2019-03-01 03:44:01 +00:00
|
|
|
"math"
|
2019-01-31 00:02:38 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type Layout struct {
|
2019-02-11 02:51:55 +00:00
|
|
|
ui.Block
|
2019-03-16 04:35:00 +00:00
|
|
|
Components []*component.Component
|
2019-03-08 04:04:46 +00:00
|
|
|
statusbar *component.StatusBar
|
|
|
|
menu *component.Menu
|
2019-03-16 04:35:00 +00:00
|
|
|
ChangeModeEvents chan Mode
|
2019-02-25 00:08:36 +00:00
|
|
|
mode Mode
|
2019-02-24 04:42:52 +00:00
|
|
|
selection int
|
2019-01-31 00:02:38 +00:00
|
|
|
}
|
|
|
|
|
2019-02-11 02:51:55 +00:00
|
|
|
type Mode rune
|
|
|
|
|
|
|
|
const (
|
|
|
|
ModeDefault Mode = 0
|
2019-03-03 01:04:02 +00:00
|
|
|
ModePause Mode = 1
|
|
|
|
ModeComponentSelect Mode = 2
|
|
|
|
ModeMenuOptionSelect Mode = 3
|
|
|
|
ModeComponentMove Mode = 4
|
|
|
|
ModeComponentResize Mode = 5
|
|
|
|
ModeChartPinpoint Mode = 6
|
2019-02-11 02:51:55 +00:00
|
|
|
)
|
|
|
|
|
2019-01-31 00:02:38 +00:00
|
|
|
const (
|
2019-03-02 21:38:36 +00:00
|
|
|
minDimension = 3
|
2019-02-25 00:37:57 +00:00
|
|
|
statusbarHeight = 1
|
2019-01-31 00:02:38 +00:00
|
|
|
)
|
|
|
|
|
2019-03-08 04:04:46 +00:00
|
|
|
func NewLayout(width, height int, statusline *component.StatusBar, menu *component.Menu) *Layout {
|
2019-01-31 00:02:38 +00:00
|
|
|
|
2019-02-11 02:51:55 +00:00
|
|
|
block := *ui.NewBlock()
|
2019-01-31 00:02:38 +00:00
|
|
|
block.SetRect(0, 0, width, height)
|
2019-02-25 00:37:57 +00:00
|
|
|
statusline.SetRect(0, height-statusbarHeight, width, height)
|
2019-01-31 00:02:38 +00:00
|
|
|
|
|
|
|
return &Layout{
|
2019-02-24 04:42:52 +00:00
|
|
|
Block: block,
|
2019-03-16 04:35:00 +00:00
|
|
|
Components: make([]*component.Component, 0),
|
2019-02-25 00:08:36 +00:00
|
|
|
statusbar: statusline,
|
2019-02-24 04:42:52 +00:00
|
|
|
menu: menu,
|
|
|
|
mode: ModeDefault,
|
|
|
|
selection: 0,
|
|
|
|
ChangeModeEvents: make(chan Mode, 10),
|
2019-01-31 00:02:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-16 23:59:28 +00:00
|
|
|
func (l *Layout) AddComponent(cpt *component.Component) {
|
2019-03-16 04:35:00 +00:00
|
|
|
l.Components = append(l.Components, cpt)
|
2019-01-31 00:02:38 +00:00
|
|
|
}
|
|
|
|
|
2019-02-24 04:42:52 +00:00
|
|
|
func (l *Layout) changeMode(m Mode) {
|
|
|
|
l.mode = m
|
|
|
|
l.ChangeModeEvents <- m
|
|
|
|
}
|
|
|
|
|
2019-04-14 00:24:24 +00:00
|
|
|
func (l *Layout) HandleMouseClick(x int, y int) {
|
|
|
|
l.menu.Idle()
|
|
|
|
selected, i := l.findComponentAtPoint(image.Point{X: x, Y: y})
|
|
|
|
if selected == nil {
|
|
|
|
l.changeMode(ModeDefault)
|
|
|
|
} else {
|
|
|
|
l.selection = i
|
|
|
|
l.menu.Highlight(selected)
|
|
|
|
l.changeMode(ModeComponentSelect)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Layout) HandleKeyboardEvent(e string) {
|
2019-03-16 04:35:00 +00:00
|
|
|
|
|
|
|
selected := l.getSelection()
|
|
|
|
|
2019-02-11 02:51:55 +00:00
|
|
|
switch e {
|
2019-03-03 01:04:02 +00:00
|
|
|
case console.KeyPause:
|
|
|
|
if l.mode == ModePause {
|
|
|
|
l.changeMode(ModeDefault)
|
|
|
|
} else {
|
2019-03-16 04:35:00 +00:00
|
|
|
if selected.Type == config.TypeRunChart {
|
2019-03-16 23:59:28 +00:00
|
|
|
selected.CommandChannel <- &data.Command{Type: runchart.CommandDisableSelection}
|
2019-03-03 01:04:02 +00:00
|
|
|
}
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Idle()
|
2019-03-03 01:04:02 +00:00
|
|
|
l.changeMode(ModePause)
|
|
|
|
}
|
2019-02-11 02:51:55 +00:00
|
|
|
case console.KeyEnter:
|
|
|
|
switch l.mode {
|
|
|
|
case ModeComponentSelect:
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Choose()
|
2019-02-24 04:42:52 +00:00
|
|
|
l.changeMode(ModeMenuOptionSelect)
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeMenuOptionSelect:
|
2019-03-08 04:04:46 +00:00
|
|
|
option := l.menu.GetSelectedOption()
|
2019-02-11 02:51:55 +00:00
|
|
|
switch option {
|
2019-03-08 04:04:46 +00:00
|
|
|
case component.MenuOptionMove:
|
2019-02-24 04:42:52 +00:00
|
|
|
l.changeMode(ModeComponentMove)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.MoveOrResize()
|
|
|
|
case component.MenuOptionResize:
|
2019-02-24 04:42:52 +00:00
|
|
|
l.changeMode(ModeComponentResize)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.MoveOrResize()
|
|
|
|
case component.MenuOptionPinpoint:
|
2019-02-24 04:42:52 +00:00
|
|
|
l.changeMode(ModeChartPinpoint)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Idle()
|
2019-03-16 23:59:28 +00:00
|
|
|
selected.CommandChannel <- &data.Command{Type: runchart.CommandMoveSelection, Value: 0}
|
2019-03-08 04:04:46 +00:00
|
|
|
case component.MenuOptionResume:
|
2019-02-24 04:42:52 +00:00
|
|
|
l.changeMode(ModeDefault)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Idle()
|
2019-02-11 02:51:55 +00:00
|
|
|
}
|
|
|
|
case ModeComponentMove:
|
|
|
|
fallthrough
|
|
|
|
case ModeComponentResize:
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Idle()
|
2019-02-24 04:42:52 +00:00
|
|
|
l.changeMode(ModeDefault)
|
2019-02-11 02:51:55 +00:00
|
|
|
}
|
|
|
|
case console.KeyEsc:
|
2019-03-17 00:54:24 +00:00
|
|
|
l.resetAlerts()
|
2019-02-11 02:51:55 +00:00
|
|
|
switch l.mode {
|
|
|
|
case ModeChartPinpoint:
|
2019-03-16 23:59:28 +00:00
|
|
|
selected.CommandChannel <- &data.Command{Type: runchart.CommandDisableSelection}
|
2019-02-11 02:51:55 +00:00
|
|
|
fallthrough
|
|
|
|
case ModeComponentSelect:
|
|
|
|
fallthrough
|
|
|
|
case ModeMenuOptionSelect:
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Idle()
|
2019-02-24 04:42:52 +00:00
|
|
|
l.changeMode(ModeDefault)
|
2019-04-26 05:11:40 +00:00
|
|
|
case ModeComponentMove:
|
|
|
|
fallthrough
|
|
|
|
case ModeComponentResize:
|
|
|
|
l.menu.Idle()
|
|
|
|
l.changeMode(ModeDefault)
|
2019-02-11 02:51:55 +00:00
|
|
|
}
|
|
|
|
case console.KeyLeft:
|
|
|
|
switch l.mode {
|
|
|
|
case ModeDefault:
|
2019-02-24 04:42:52 +00:00
|
|
|
l.changeMode(ModeComponentSelect)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Highlight(l.getComponent(l.selection))
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeChartPinpoint:
|
2019-03-16 23:59:28 +00:00
|
|
|
selected.CommandChannel <- &data.Command{Type: runchart.CommandMoveSelection, Value: -1}
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeComponentSelect:
|
2019-03-02 21:30:16 +00:00
|
|
|
l.moveSelection(e)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Highlight(l.getComponent(l.selection))
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeComponentMove:
|
2019-03-16 04:35:00 +00:00
|
|
|
selected.Move(-1, 0)
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeComponentResize:
|
2019-03-16 04:35:00 +00:00
|
|
|
selected.Resize(-1, 0)
|
2019-02-11 02:51:55 +00:00
|
|
|
}
|
|
|
|
case console.KeyRight:
|
|
|
|
switch l.mode {
|
|
|
|
case ModeDefault:
|
2019-02-24 04:42:52 +00:00
|
|
|
l.changeMode(ModeComponentSelect)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Highlight(l.getComponent(l.selection))
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeChartPinpoint:
|
2019-03-16 23:59:28 +00:00
|
|
|
selected.CommandChannel <- &data.Command{Type: runchart.CommandMoveSelection, Value: 1}
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeComponentSelect:
|
2019-03-02 21:30:16 +00:00
|
|
|
l.moveSelection(e)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Highlight(l.getComponent(l.selection))
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeComponentMove:
|
2019-03-16 04:35:00 +00:00
|
|
|
selected.Move(1, 0)
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeComponentResize:
|
2019-03-16 04:35:00 +00:00
|
|
|
selected.Resize(1, 0)
|
2019-02-11 02:51:55 +00:00
|
|
|
}
|
|
|
|
case console.KeyUp:
|
|
|
|
switch l.mode {
|
2019-02-16 03:46:03 +00:00
|
|
|
case ModeDefault:
|
2019-02-24 04:42:52 +00:00
|
|
|
l.changeMode(ModeComponentSelect)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Highlight(l.getComponent(l.selection))
|
2019-02-16 03:46:03 +00:00
|
|
|
case ModeComponentSelect:
|
2019-03-02 21:30:16 +00:00
|
|
|
l.moveSelection(e)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Highlight(l.getComponent(l.selection))
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeMenuOptionSelect:
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Up()
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeComponentMove:
|
2019-03-16 04:35:00 +00:00
|
|
|
selected.Move(0, -1)
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeComponentResize:
|
2019-03-16 04:35:00 +00:00
|
|
|
selected.Resize(0, -1)
|
2019-02-11 02:51:55 +00:00
|
|
|
}
|
|
|
|
case console.KeyDown:
|
|
|
|
switch l.mode {
|
2019-02-16 03:46:03 +00:00
|
|
|
case ModeDefault:
|
2019-02-24 04:42:52 +00:00
|
|
|
l.changeMode(ModeComponentSelect)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Highlight(l.getComponent(l.selection))
|
2019-02-16 03:46:03 +00:00
|
|
|
case ModeComponentSelect:
|
2019-03-02 21:30:16 +00:00
|
|
|
l.moveSelection(e)
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Highlight(l.getComponent(l.selection))
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeMenuOptionSelect:
|
2019-03-08 04:04:46 +00:00
|
|
|
l.menu.Down()
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeComponentMove:
|
2019-03-16 04:35:00 +00:00
|
|
|
selected.Move(0, 1)
|
2019-02-11 02:51:55 +00:00
|
|
|
case ModeComponentResize:
|
2019-03-16 04:35:00 +00:00
|
|
|
selected.Resize(0, 1)
|
2019-02-11 02:51:55 +00:00
|
|
|
}
|
|
|
|
}
|
2019-02-08 03:47:43 +00:00
|
|
|
}
|
|
|
|
|
2019-02-11 02:51:55 +00:00
|
|
|
func (l *Layout) ChangeDimensions(width, height int) {
|
|
|
|
l.SetRect(0, 0, width, height)
|
2019-01-31 00:02:38 +00:00
|
|
|
}
|
|
|
|
|
2019-03-08 04:04:46 +00:00
|
|
|
func (l *Layout) getComponent(i int) *component.Component {
|
2019-03-16 04:35:00 +00:00
|
|
|
return l.Components[i]
|
2019-02-11 03:26:51 +00:00
|
|
|
}
|
|
|
|
|
2019-03-16 04:35:00 +00:00
|
|
|
func (l *Layout) getSelection() *component.Component {
|
|
|
|
return l.Components[l.selection]
|
2019-02-11 03:26:51 +00:00
|
|
|
}
|
|
|
|
|
2019-03-02 21:30:16 +00:00
|
|
|
func (l *Layout) moveSelection(direction string) {
|
|
|
|
|
2019-03-16 04:35:00 +00:00
|
|
|
previouslySelected := l.getSelection()
|
2019-03-22 03:47:28 +00:00
|
|
|
newlySelectedIndex := l.selection + 1
|
2019-03-02 21:30:16 +00:00
|
|
|
|
|
|
|
for i, current := range l.Components {
|
|
|
|
|
|
|
|
if current == previouslySelected {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-03-22 03:47:28 +00:00
|
|
|
if newlySelectedIndex >= len(l.Components) {
|
2019-03-02 21:30:16 +00:00
|
|
|
newlySelectedIndex = i
|
|
|
|
}
|
|
|
|
|
|
|
|
var previouslySelectedCornerPoint image.Point
|
|
|
|
var newlySelectedCornerPoint image.Point
|
|
|
|
var currentCornerPoint image.Point
|
|
|
|
|
|
|
|
switch direction {
|
|
|
|
case console.KeyLeft:
|
2019-03-16 04:35:00 +00:00
|
|
|
previouslySelectedCornerPoint = component.GetRectLeftSideCenter(previouslySelected.GetRect())
|
|
|
|
newlySelectedCornerPoint = component.GetRectRightSideCenter(l.getComponent(newlySelectedIndex).GetRect())
|
|
|
|
currentCornerPoint = component.GetRectRightSideCenter(current.GetRect())
|
2019-03-02 21:30:16 +00:00
|
|
|
case console.KeyRight:
|
2019-03-16 04:35:00 +00:00
|
|
|
previouslySelectedCornerPoint = component.GetRectRightSideCenter(previouslySelected.GetRect())
|
|
|
|
newlySelectedCornerPoint = component.GetRectLeftSideCenter(l.getComponent(newlySelectedIndex).GetRect())
|
|
|
|
currentCornerPoint = component.GetRectLeftSideCenter(current.GetRect())
|
2019-03-02 21:30:16 +00:00
|
|
|
case console.KeyUp:
|
2019-03-16 04:35:00 +00:00
|
|
|
previouslySelectedCornerPoint = component.GetRectTopSideCenter(previouslySelected.GetRect())
|
|
|
|
newlySelectedCornerPoint = component.GetRectBottomSideCenter(l.getComponent(newlySelectedIndex).GetRect())
|
|
|
|
currentCornerPoint = component.GetRectBottomSideCenter(current.GetRect())
|
2019-03-02 21:30:16 +00:00
|
|
|
case console.KeyDown:
|
2019-03-16 04:35:00 +00:00
|
|
|
previouslySelectedCornerPoint = component.GetRectBottomSideCenter(previouslySelected.GetRect())
|
|
|
|
newlySelectedCornerPoint = component.GetRectTopSideCenter(l.getComponent(newlySelectedIndex).GetRect())
|
|
|
|
currentCornerPoint = component.GetRectTopSideCenter(current.GetRect())
|
2019-03-02 21:30:16 +00:00
|
|
|
}
|
|
|
|
|
2019-04-17 03:55:23 +00:00
|
|
|
switch direction {
|
|
|
|
case console.KeyLeft:
|
|
|
|
fallthrough
|
|
|
|
case console.KeyRight:
|
|
|
|
if ui.AbsInt(currentCornerPoint.X-previouslySelectedCornerPoint.X) <= ui.AbsInt(newlySelectedCornerPoint.X-previouslySelectedCornerPoint.X) {
|
|
|
|
if ui.AbsInt(currentCornerPoint.Y-previouslySelectedCornerPoint.Y) <= ui.AbsInt(newlySelectedCornerPoint.Y-previouslySelectedCornerPoint.Y) {
|
|
|
|
newlySelectedIndex = i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case console.KeyUp:
|
|
|
|
fallthrough
|
|
|
|
case console.KeyDown:
|
|
|
|
if ui.AbsInt(currentCornerPoint.Y-previouslySelectedCornerPoint.Y) <= ui.AbsInt(newlySelectedCornerPoint.Y-previouslySelectedCornerPoint.Y) {
|
|
|
|
if ui.AbsInt(currentCornerPoint.X-previouslySelectedCornerPoint.X) <= ui.AbsInt(newlySelectedCornerPoint.X-previouslySelectedCornerPoint.X) {
|
|
|
|
newlySelectedIndex = i
|
|
|
|
}
|
|
|
|
}
|
2019-03-02 21:30:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-22 03:47:28 +00:00
|
|
|
if newlySelectedIndex < len(l.Components) {
|
|
|
|
l.selection = newlySelectedIndex
|
|
|
|
}
|
2019-03-02 21:30:16 +00:00
|
|
|
}
|
|
|
|
|
2019-02-11 02:51:55 +00:00
|
|
|
func (l *Layout) Draw(buffer *ui.Buffer) {
|
2019-01-31 00:02:38 +00:00
|
|
|
|
2019-04-26 05:11:40 +00:00
|
|
|
columnWidth := float64(l.GetRect().Dx()) / float64(console.ColumnsCount)
|
|
|
|
rowHeight := float64(l.GetRect().Dy()-statusbarHeight) / float64(console.RowsCount)
|
2019-01-31 00:02:38 +00:00
|
|
|
|
2019-03-08 04:04:46 +00:00
|
|
|
for _, c := range l.Components {
|
2019-04-16 02:12:10 +00:00
|
|
|
rectangle := calculateComponentCoordinates(c, columnWidth, rowHeight)
|
|
|
|
c.SetRect(rectangle.Min.X, rectangle.Min.Y, rectangle.Max.X, rectangle.Max.Y)
|
2019-03-16 04:35:00 +00:00
|
|
|
c.Draw(buffer)
|
2019-01-31 00:02:38 +00:00
|
|
|
}
|
2019-02-11 02:51:55 +00:00
|
|
|
|
2019-02-25 00:37:57 +00:00
|
|
|
l.statusbar.SetRect(
|
|
|
|
0, l.GetRect().Dy()-statusbarHeight,
|
|
|
|
l.GetRect().Dx(), l.GetRect().Dy())
|
|
|
|
|
2019-02-25 00:08:36 +00:00
|
|
|
l.statusbar.Draw(buffer)
|
2019-03-03 01:04:02 +00:00
|
|
|
l.menu.Draw(buffer)
|
2019-01-31 00:02:38 +00:00
|
|
|
}
|
2019-03-17 00:54:24 +00:00
|
|
|
|
2019-04-14 00:24:24 +00:00
|
|
|
func (l *Layout) findComponentAtPoint(point image.Point) (*component.Component, int) {
|
|
|
|
|
2019-04-26 05:11:40 +00:00
|
|
|
columnWidth := float64(l.GetRect().Dx()) / float64(console.ColumnsCount)
|
|
|
|
rowHeight := float64(l.GetRect().Dy()-statusbarHeight) / float64(console.RowsCount)
|
2019-04-14 00:24:24 +00:00
|
|
|
|
|
|
|
for i, c := range l.Components {
|
|
|
|
|
2019-04-16 02:12:10 +00:00
|
|
|
rectangle := calculateComponentCoordinates(c, columnWidth, rowHeight)
|
2019-04-14 00:24:24 +00:00
|
|
|
|
|
|
|
if point.In(rectangle) {
|
|
|
|
return c, i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, -1
|
|
|
|
}
|
|
|
|
|
2019-04-16 02:12:10 +00:00
|
|
|
func calculateComponentCoordinates(c *component.Component, columnWidth float64, rowHeight float64) image.Rectangle {
|
|
|
|
|
|
|
|
x1 := math.Floor(float64(c.Location.X) * columnWidth)
|
|
|
|
y1 := math.Floor(float64(c.Location.Y) * rowHeight)
|
|
|
|
x2 := x1 + math.Floor(float64(c.Size.X))*columnWidth
|
|
|
|
y2 := y1 + math.Floor(float64(c.Size.Y))*rowHeight
|
|
|
|
|
|
|
|
if x2-x1 < minDimension {
|
|
|
|
x2 = x1 + minDimension
|
|
|
|
}
|
|
|
|
|
|
|
|
if y2-y1 < minDimension {
|
|
|
|
y2 = y1 + minDimension
|
|
|
|
}
|
|
|
|
|
|
|
|
return image.Rectangle{Min: image.Point{
|
|
|
|
X: int(x1), Y: int(y1)},
|
|
|
|
Max: image.Point{X: int(x2), Y: int(y2)},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-17 00:54:24 +00:00
|
|
|
func (l *Layout) resetAlerts() {
|
|
|
|
for _, c := range l.Components {
|
|
|
|
c.AlertChannel <- nil
|
|
|
|
}
|
|
|
|
}
|