From 910bdbc85453aa721749b30bdab2066539b1b1e0 Mon Sep 17 00:00:00 2001 From: lmika Date: Tue, 6 Jan 2015 22:34:06 +1100 Subject: [PATCH] A lot of work. Started working on the session objects and added a new text display component and componant switcher. --- commandmap.go | 126 +++++++++++++++++++++++++++++++++++++++++++++++++ frame.go | 84 +++++++++++++++++++++++++++++++++ main.go | 14 ++++-- session.go | 51 ++++++++++++++++++++ ui/grid.go | 111 ++++++++++++++++++++++++++++--------------- ui/layout.go | 23 +++++++++ ui/stdcomps.go | 24 ++++++++++ 7 files changed, 391 insertions(+), 42 deletions(-) create mode 100644 commandmap.go create mode 100644 frame.go create mode 100644 session.go diff --git a/commandmap.go b/commandmap.go new file mode 100644 index 0000000..ab9481f --- /dev/null +++ b/commandmap.go @@ -0,0 +1,126 @@ +package main + +import ( + "./ui" +) + +const ( + ModAlt rune = 1 << 31 - 1 +) + + +// A command +type Command struct { + Name string + Doc string + Action func(ctx CommandContext) error +} + +// Execute the command +func (cmd *Command) Do(ctx CommandContext) error { + return cmd.Action(ctx) +} + + +// The command context +type CommandContext interface { + // Returns the current frame. If no frame is defined, returns nil. + Frame() *Frame +} + +// A command mapping +type CommandMapping struct { + Commands map[string]*Command + KeyMappings map[rune]*Command +} + +// Creates a new, empty command mapping +func NewCommandMapping() *CommandMapping { + return &CommandMapping{make(map[string]*Command), make(map[rune]*Command)} +} + +// Adds a new command +func (cm *CommandMapping) Define(name string, doc string, opts string, fn func(ctx CommandContext) error) { + cm.Commands[name] = &Command{name, doc, fn} +} + +// Adds a key mapping +func (cm *CommandMapping) MapKey(key rune, cmd *Command) { + cm.KeyMappings[key] = cmd +} + +// Searches for a command by name. Returns the command or null +func (cm *CommandMapping) Command(name string) *Command { + return cm.Commands[name] +} + +// Searches for a command by key mapping +func (cm *CommandMapping) KeyMapping(key rune) *Command { + return cm.KeyMappings[key] +} + +// Registers the standard view navigation commands. These commands require the frame +func (cm *CommandMapping) RegisterViewCommands() { + cm.Define("move-down", "Moves the cursor down one row", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, 1) })) + cm.Define("move-up", "Moves the cursor up one row", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, -1) })) + cm.Define("move-left", "Moves the cursor left one column", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(-1, 0) })) + cm.Define("move-right", "Moves the cursor right one column", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(1, 0) })) + + // TODO: Pages are just 25 rows and 15 columns at the moment + cm.Define("page-down", "Moves the cursor down one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, 25) })) + cm.Define("page-up", "Moves the cursor up one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, -25) })) + cm.Define("page-left", "Moves the cursor left one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(-15, 0) })) + cm.Define("page-right", "Moves the cursor right one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(15, 0) })) + + cm.Define("row-top", "Moves the cursor to the top of the row", "", gridNavOperation(func(grid *ui.Grid) { + cellX, _ := grid.CellPosition() + grid.MoveTo(cellX, 0) + })) + cm.Define("row-bottom", "Moves the cursor to the bottom of the row", "", gridNavOperation(func(grid *ui.Grid) { + cellX, _ := grid.CellPosition() + _, dimY := grid.Model().Dimensions() + grid.MoveTo(cellX, dimY - 1) + })) + cm.Define("col-left", "Moves the cursor to the left-most column", "", gridNavOperation(func(grid *ui.Grid) { + _, cellY := grid.CellPosition() + grid.MoveTo(0, cellY) + })) + + cm.Define("col-right", "Moves the cursor to the right-most column", "", gridNavOperation(func(grid *ui.Grid) { + _, cellY := grid.CellPosition() + dimX, _ := grid.Model().Dimensions() + grid.MoveTo(dimX - 1, cellY) + })) +} + +// Registers the standard view key bindings. These commands require the frame +func (cm *CommandMapping) RegisterViewKeyBindings() { + cm.MapKey('i', cm.Command("move-up")) + cm.MapKey('k', cm.Command("move-down")) + cm.MapKey('j', cm.Command("move-left")) + cm.MapKey('l', cm.Command("move-right")) + cm.MapKey('I', cm.Command("page-up")) + cm.MapKey('K', cm.Command("page-down")) + cm.MapKey('J', cm.Command("page-left")) + cm.MapKey('L', cm.Command("page-right")) + cm.MapKey(ui.KeyCtrlI, cm.Command("row-top")) + cm.MapKey(ui.KeyCtrlK, cm.Command("row-bottom")) + cm.MapKey(ui.KeyCtrlJ, cm.Command("col-left")) + cm.MapKey(ui.KeyCtrlL, cm.Command("col-right")) + + cm.MapKey(ui.KeyArrowUp, cm.Command("move-up")) + cm.MapKey(ui.KeyArrowDown, cm.Command("move-down")) + cm.MapKey(ui.KeyArrowLeft, cm.Command("move-left")) + cm.MapKey(ui.KeyArrowRight, cm.Command("move-right")) +} + + +// A nativation command factory. This will perform the passed in operation with the current grid and +// will display the cell value in the message box. +func gridNavOperation(op func(grid *ui.Grid)) func(ctx CommandContext) error { + return func(ctx CommandContext) error { + op(ctx.Frame().Grid()) + ctx.Frame().ShowCellValue() + return nil + } +} \ No newline at end of file diff --git a/frame.go b/frame.go new file mode 100644 index 0000000..a4b0f9f --- /dev/null +++ b/frame.go @@ -0,0 +1,84 @@ +package main + +import ( + "./ui" +) + +type Mode int + +const ( + // The grid is selectable + GridMode Mode = iota +) + +// A frame is a UI instance. +type Frame struct { + Session *Session + + uiManager *ui.Ui + clientArea *ui.RelativeLayout + grid *ui.Grid + messageView *ui.TextView + textEntry *ui.TextEntry + statusBar *ui.StatusBar + textEntrySwitch *ui.ProxyLayout +} + +// Creates the UI and returns a new frame +func NewFrame(uiManager *ui.Ui) *Frame { + frame := &Frame{ + uiManager: uiManager, + } + + frame.grid = ui.NewGrid(&ui.TestModel{}) + frame.messageView = &ui.TextView{"Hello"} + frame.statusBar = &ui.StatusBar{"Test", "Status"} + frame.textEntrySwitch = &ui.ProxyLayout{frame.messageView} + frame.textEntry = &ui.TextEntry{} + + // Build the UI frame + statusLayout := &ui.VertLinearLayout{} + statusLayout.Append(frame.statusBar) + statusLayout.Append(frame.textEntrySwitch) + + frame.clientArea = &ui.RelativeLayout{ Client: frame.grid, South: statusLayout } + return frame +} + +// Returns the root component of the frame +func (frame *Frame) RootComponent() ui.UiComponent { + return frame.clientArea +} + +// Returns the grid component +func (frame *Frame) Grid() *ui.Grid { + return frame.grid +} + +// Sets the specific mode. +func (frame *Frame) EnterMode(mode Mode) { + switch mode { + case GridMode: + frame.uiManager.SetFocusedComponent(frame) + } +} + +// Show a message. This will switch the bottom to the messageView and select the frame +func (frame *Frame) ShowMessage(msg string) { + frame.messageView.Text = msg + frame.textEntrySwitch.Component = frame.messageView + //frame.EnterMode(GridMode) +} + +// Shows the value of the currently select grid cell +func (frame *Frame) ShowCellValue() { + displayValue := frame.grid.CurrentCellDisplayValue() + frame.ShowMessage(displayValue) +} + +// Handle the main grid input as this is the "component" that handles command input. +func (frame *Frame) KeyPressed(key rune, mod int) { + if frame.Session != nil { + frame.Session.KeyPressed(key, mod) + } +} \ No newline at end of file diff --git a/main.go b/main.go index eb21a70..5cbfad7 100644 --- a/main.go +++ b/main.go @@ -11,8 +11,15 @@ func main() { } defer uiManager.Close() + frame := NewFrame(uiManager) + NewSession(frame) - cmdText := &ui.TextEntry{ Prompt: "Enter: " } + uiManager.SetRootComponent(frame.RootComponent()) + frame.EnterMode(GridMode) + + uiManager.Loop() +/* + cmdText := &ui.TextEntry{Prompt: "Enter: "} statusLayout := &ui.VertLinearLayout{} statusLayout.Append(&ui.StatusBar{"Test", "Component"}) @@ -25,10 +32,11 @@ func main() { clientArea := &ui.RelativeLayout{ Client: grid, South: statusLayout } uiManager.SetRootComponent(clientArea) - //uiManager.SetFocusedComponent(grid) - uiManager.SetFocusedComponent(cmdText) + uiManager.SetFocusedComponent(grid) + //uiManager.SetFocusedComponent(cmdText) uiManager.Loop() +*/ /* uiCtx, _ := NewUI() diff --git a/session.go b/session.go new file mode 100644 index 0000000..2028966 --- /dev/null +++ b/session.go @@ -0,0 +1,51 @@ +package main + +import "./ui" + +// The session is responsible for managing the UI and the model and handling +// the interaction between the two and the user. +type Session struct { + Frame *Frame + Commands *CommandMapping +} + +func NewSession(frame *Frame) *Session { + session := &Session{ + Frame: frame, + Commands: NewCommandMapping(), + } + + session.Commands.RegisterViewCommands() + session.Commands.RegisterViewKeyBindings() + + // Also assign this session with the frame + frame.Session = session + + return session +} + +// Input from the frame +func (session *Session) KeyPressed(key rune, mod int) { + // Add the mod key modifier + if (mod & ui.ModKeyAlt != 0) { + key |= ModAlt + } + + cmd := session.Commands.KeyMapping(key) + if cmd != nil { + err := cmd.Do(SessionCommandContext{session}) + if err != nil { + session.Frame.ShowMessage(err.Error()) + } + } +} + + +// The command context used by the session +type SessionCommandContext struct { + Session *Session +} + +func (scc SessionCommandContext) Frame() *Frame { + return scc.Session.Frame +} \ No newline at end of file diff --git a/ui/grid.go b/ui/grid.go index 8bf8951..cb09a55 100644 --- a/ui/grid.go +++ b/ui/grid.go @@ -13,22 +13,22 @@ type GridModel interface { /** * Returns the size of the grid model (width x height) */ - GetDimensions() (int, int) + Dimensions() (int, int) /** * Returns the size of the particular column. If the size is 0, this indicates that the column is hidden. */ - GetColWidth(int) int + ColWidth(int) int /** * Returns the size of the particular row. If the size is 0, this indicates that the row is hidden. */ - GetRowHeight(int) int + RowHeight(int) int /** * Returns the value of the cell a position X, Y */ - GetCellValue(int, int) string + CellValue(int, int) string } @@ -38,7 +38,8 @@ type gridPoint int * The grid component. */ type Grid struct { - model GridModel + model GridModel // The grid model + viewCellX int // Left most cell viewCellY int // Top most cell selCellX int // The currently selected cell @@ -72,11 +73,9 @@ func NewGrid(model GridModel) *Grid { return &Grid{model, 0, 0, 0, 0, -1, -1} } -/** - * Returns the requested dimensions of a grid (as required by UiComponent) - */ -func (grid *Grid) Remeasure(w, h int) (int, int) { - return w, h +// Returns the model +func (grid *Grid) Model() GridModel { + return grid.model } /** @@ -88,11 +87,44 @@ func (grid *Grid) ShiftBy(x int, y int) { } -// Moves the currently selected cell by a delta. +// Returns the display value of the currently selected cell. +func (grid *Grid) CurrentCellDisplayValue() string { + if grid.isCellValid(grid.selCellX, grid.selCellY) { + return grid.model.CellValue(grid.selCellX, grid.selCellY) + } else { + return "" + } +} + +// Moves the currently selected cell by a delta. This will be implemented as single stepped +// moveTo calls to handle invalid cells. func (grid *Grid) MoveBy(x int, y int) { - grid.selCellX += x - grid.selCellY += y - grid.reposition() + grid.MoveTo(grid.selCellX + x, grid.selCellY + y) +} + +// Moves the currently selected cell to a specific row. The row must be valid, otherwise the +// currently selected cell will not be changed. Returns true if the move was successful +func (grid *Grid) MoveTo(newX, newY int) { + maxX, maxY := grid.model.Dimensions() + newX = intMinMax(newX, 0, maxX - 1) + newY = intMinMax(newY, 0, maxY - 1) + + if grid.isCellValid(newX, newY) { + grid.selCellX = newX + grid.selCellY = newY + grid.reposition() + } +} + +// Returns the currently selected cell position. +func (grid *Grid) CellPosition() (int, int) { + return grid.selCellX, grid.selCellY +} + +// Returns true if the user can enter the specific cell +func (grid *Grid) isCellValid(x int, y int) bool { + maxX, maxY := grid.model.Dimensions() + return (x >= 0) && (y >= 0) && (x < maxX) && (y < maxY) } @@ -118,17 +150,15 @@ func (grid *Grid) reposition() { } - - // Gets the cell value and attributes of a particular cell func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) { // The fixed cells modelCellX := cellX - 1 + grid.viewCellX modelCellY := cellY - 1 + grid.viewCellY - modelMaxX, modelMaxY := grid.model.GetDimensions() + modelMaxX, modelMaxY := grid.model.Dimensions() if (cellX == 0) && (cellY == 0) { - return strconv.Itoa(grid.cellsWide), AttrBold, AttrBold + return "", AttrBold, AttrBold } else if (cellX == 0) { if (modelCellY == grid.selCellY) { return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrBold | AttrReverse @@ -145,18 +175,14 @@ func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) // The data from the model if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) { if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) { - return grid.model.GetCellValue(modelCellX, modelCellY), AttrReverse, AttrReverse + return grid.model.CellValue(modelCellX, modelCellY), AttrReverse, AttrReverse } else { - return grid.model.GetCellValue(modelCellX, modelCellY), 0, 0 + return grid.model.CellValue(modelCellX, modelCellY), 0, 0 } } else { - return "~", 0, 0 + return "~", ColorBlue | AttrBold, 0 } } - - // XXX: Workaround for bug in compiler - panic("Unreachable code") - return "", 0, 0 } // Gets the cell dimensions @@ -166,17 +192,17 @@ func (grid *Grid) getCellDimensions(cellX, cellY int) (width, height int) { modelCellX := cellX - 1 + grid.viewCellX modelCellY := cellY - 1 + grid.viewCellY - modelMaxX, modelMaxY := grid.model.GetDimensions() + modelMaxX, modelMaxY := grid.model.Dimensions() // Get the cell width & height from model (if within range) if (modelCellX >= 0) && (modelCellX < modelMaxX) { - cellWidth = grid.model.GetColWidth(modelCellX) + cellWidth = grid.model.ColWidth(modelCellX) } else { cellWidth = 8 } if (modelCellY >= 0) && (modelCellY < modelMaxY) { - cellHeight = grid.model.GetRowHeight(modelCellY) + cellHeight = grid.model.RowHeight(modelCellY) } else { cellHeight = 2 } @@ -212,8 +238,6 @@ func (grid *Grid) renderCell(ctx *DrawContext, cellClipRect gridRect, sx int, sy } } - //termbox.SetCell(int(x - cellClipRect.x1) + sx, int(y - cellClipRect.y1) + sy, currRune, fg, bg) - // TODO: This might be better if this wasn't so low-level ctx.DrawRuneWithAttrs(int(x - cellClipRect.x1) + sx, int(y - cellClipRect.y1) + sy, currRune, fg, bg) } @@ -288,7 +312,7 @@ func (grid *Grid) renderGrid(ctx *DrawContext, screenViewPort gridRect, cellX in * Returns the cell of the particular point, along with the top-left position of the cell. */ func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, posY int) { - var wid, hei int = grid.model.GetDimensions() + var wid, hei int = grid.model.Dimensions() posX = 0 posY = 0 @@ -297,7 +321,7 @@ func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, pos // Go through columns to locate the particular cellX for cx := 0; cx < wid; cx++ { - if (x >= posX) && (x < posX + grid.model.GetColWidth(cx)) { + if (x >= posX) && (x < posX + grid.model.ColWidth(cx)) { // We found the X position cellX = int(cx) break @@ -305,7 +329,7 @@ func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, pos } for cy := 0; cy < hei; cy++ { - if (y >= posY) && (y < posY + grid.model.GetRowHeight(cy)) { + if (y >= posY) && (y < posY + grid.model.RowHeight(cy)) { // And the Y position cellY = int(cy) break @@ -315,6 +339,13 @@ func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, pos return } +/** + * Returns the requested dimensions of a grid (as required by UiComponent) + */ +func (grid *Grid) Remeasure(w, h int) (int, int) { + return w, h +} + /** * Redraws the grid. */ @@ -323,8 +354,10 @@ func (grid *Grid) Redraw(ctx *DrawContext) { grid.cellsWide, grid.cellsHigh = grid.renderGrid(ctx, viewportRect, 0, 0, 0, 0) } -// Called when the component has focus and a key has been pressed -func (grid *Grid) KeyPressed(key rune) { +// Called when the component has focus and a key has been pressed. +// This is the default behaviour of the grid, but it is not used by the main grid. +func (grid *Grid) KeyPressed(key rune, mod int) { + // TODO: Not sure if this would be better handled using commands if (key == 'i') || (key == KeyArrowUp) { grid.MoveBy(0, -1) } else if (key == 'k') || (key == KeyArrowDown) { @@ -347,27 +380,27 @@ type TestModel struct { /** * Returns the size of the grid model (width x height) */ -func (model *TestModel) GetDimensions() (int, int) { +func (model *TestModel) Dimensions() (int, int) { return 100, 100 } /** * Returns the size of the particular column. If the size is 0, this indicates that the column is hidden. */ -func (model *TestModel) GetColWidth(int) int { +func (model *TestModel) ColWidth(int) int { return 16 } /** * Returns the size of the particular row. If the size is 0, this indicates that the row is hidden. */ -func (model *TestModel) GetRowHeight(int) int { +func (model *TestModel) RowHeight(int) int { return 1 } /** * Returns the value of the cell a position X, Y */ -func (model *TestModel) GetCellValue(x int, y int) string { +func (model *TestModel) CellValue(x int, y int) string { return strconv.Itoa(x) + "," + strconv.Itoa(y) } diff --git a/ui/layout.go b/ui/layout.go index 8f31ef6..714daca 100644 --- a/ui/layout.go +++ b/ui/layout.go @@ -61,6 +61,29 @@ func (vl *VertLinearLayout) Remeasure(w, h int) (int, int) { } + +// A layout component which defers calls to the nested component. If no control is defined, +// will not draw itself. Used for switching controls dynamically. +type ProxyLayout struct { + Component UiComponent +} + +func (pl *ProxyLayout) Remeasure(w, h int) (int, int) { + if pl.Component != nil { + return pl.Component.Remeasure(w, h) + } else { + return 0, 0 + } +} + +func (pl *ProxyLayout) Redraw(context *DrawContext) { + if pl.Component != nil { + pl.Component.Redraw(context) + } +} + + + // A relative layout component. This has a "client" component, bordered by a north, // south, east and west component. The N,S,E,W components will be provided with the full dimensions // whereas the client component will be provided with the remaining size. Each one of the components diff --git a/ui/stdcomps.go b/ui/stdcomps.go index 12ad103..5a7eb78 100644 --- a/ui/stdcomps.go +++ b/ui/stdcomps.go @@ -4,6 +4,30 @@ package ui import "unicode" + +// A text component. This simply renders a text string. +type TextView struct { + + // The string to render + Text string +} + +// Minimum dimensions +func (tv *TextView) Remeasure(w, h int) (int, int) { + return w, 1 +} + +// Status bar redraw +func (tv *TextView) Redraw(context *DrawContext) { + context.SetFgAttr(0) + context.SetBgAttr(0) + + context.HorizRule(0, ' ') + context.Print(0, 0, tv.Text) +} + + + // Status bar component. This component displays text on the left and right of it's // allocated space. type StatusBar struct {