Added the notion of a grid view model.

This commit is contained in:
Leon Mika 2020-06-16 14:23:17 +10:00
parent 598d9bd962
commit a49613f7e9
5 changed files with 291 additions and 81 deletions

View file

@ -110,23 +110,13 @@ func (cm *CommandMapping) RegisterViewCommands() {
grid := ctx.Frame().Grid() grid := ctx.Frame().Grid()
_, cellY := grid.CellPosition() _, cellY := grid.CellPosition()
if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel { return ctx.ModelVC().DeleteRow(cellY)
DeleteRow(rwModel, cellY)
return nil
}
return errors.New("model is read-only")
}) })
cm.Define("delete-col", "Removes the currently selected column", "", func(ctx *CommandContext) error { cm.Define("delete-col", "Removes the currently selected column", "", func(ctx *CommandContext) error {
grid := ctx.Frame().Grid() grid := ctx.Frame().Grid()
cellX, _ := grid.CellPosition() cellX, _ := grid.CellPosition()
if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel { return ctx.ModelVC().DeleteCol(cellX)
DeleteCol(rwModel, cellX)
return nil
}
return errors.New("model is read-only")
}) })
cm.Define("search", "Search for a cell", "", func(ctx *CommandContext) error { cm.Define("search", "Search for a cell", "", func(ctx *CommandContext) error {
ctx.Frame().Prompt(PromptOptions{ Prompt: "/" }, func(res string) error { ctx.Frame().Prompt(PromptOptions{ Prompt: "/" }, func(res string) error {
@ -145,7 +135,7 @@ func (cm *CommandMapping) RegisterViewCommands() {
ctx.Session().Commands.Eval(ctx, "search") ctx.Session().Commands.Eval(ctx, "search")
} }
height, width := ctx.session.Model.Dimensions() height, width := ctx.ModelVC().Model().Dimensions()
startX, startY := ctx.Frame().Grid().CellPosition() startX, startY := ctx.Frame().Grid().CellPosition()
cellX, cellY := startX, startY cellX, cellY := startX, startY
@ -155,7 +145,7 @@ func (cm *CommandMapping) RegisterViewCommands() {
cellX = 0 cellX = 0
cellY = (cellY + 1) % height cellY = (cellY + 1) % height
} }
if ctx.session.LastSearch.MatchString(ctx.session.Model.CellValue(cellY, cellX)) { if ctx.session.LastSearch.MatchString(ctx.ModelVC().Model().CellValue(cellY, cellX)) {
ctx.Frame().Grid().MoveTo(cellX, cellY) ctx.Frame().Grid().MoveTo(cellX, cellY)
return nil return nil
} else if (cellX == startX) && (cellY == startY) { } else if (cellX == startX) && (cellY == startY) {
@ -168,30 +158,22 @@ func (cm *CommandMapping) RegisterViewCommands() {
grid := ctx.Frame().Grid() grid := ctx.Frame().Grid()
cellX, _ := grid.CellPosition() cellX, _ := grid.CellPosition()
if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel { height, width := ctx.ModelVC().Model().Dimensions()
height, width := rwModel.Dimensions()
if cellX == width-1 { if cellX == width-1 {
rwModel.Resize(height, width+1) return ctx.ModelVC().Resize(height, width+1)
} }
return nil return nil
}
return errors.New("model is read-only")
}) })
cm.Define("open-down", "Inserts a row below the curser", "", func(ctx *CommandContext) error { cm.Define("open-down", "Inserts a row below the curser", "", func(ctx *CommandContext) error {
grid := ctx.Frame().Grid() grid := ctx.Frame().Grid()
_, cellY := grid.CellPosition() _, cellY := grid.CellPosition()
if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel { height, width := ctx.ModelVC().Model().Dimensions()
height, width := rwModel.Dimensions()
if cellY == height-1 { if cellY == height-1 {
rwModel.Resize(height+1, width) return ctx.ModelVC().Resize(height+1, width)
} }
return nil return nil
}
return errors.New("model is read-only")
}) })
cm.Define("append", "Inserts a row below the curser", "", func(ctx *CommandContext) error { cm.Define("append", "Inserts a row below the curser", "", func(ctx *CommandContext) error {
@ -207,6 +189,64 @@ func (cm *CommandMapping) RegisterViewCommands() {
return ctx.Session().Commands.Eval(ctx, "set-cell") return ctx.Session().Commands.Eval(ctx, "set-cell")
}) })
cm.Define("inc-col-width", "Increase the width of the current column", "", func(ctx *CommandContext) error {
cellX, _ := ctx.Frame().Grid().CellPosition()
attrs := ctx.ModelVC().ColAttrs(cellX)
attrs.Size += 2
ctx.ModelVC().SetColAttrs(cellX, attrs)
return nil
})
cm.Define("dec-col-width", "Decrease the width of the current column", "", func(ctx *CommandContext) error {
cellX, _ := ctx.Frame().Grid().CellPosition()
attrs := ctx.ModelVC().ColAttrs(cellX)
attrs.Size -= 2
if attrs.Size < 4 {
attrs.Size = 4
}
ctx.ModelVC().SetColAttrs(cellX, attrs)
return nil
})
cm.Define("clear-row-marker", "Clears any row markers", "", func(ctx *CommandContext) error {
_, cellY := ctx.Frame().Grid().CellPosition()
attrs := ctx.ModelVC().RowAttrs(cellY)
attrs.Marker = MarkerNone
ctx.ModelVC().SetRowAttrs(cellY, attrs)
return nil
})
cm.Define("mark-row-red", "Set row marker to red", "", func(ctx *CommandContext) error {
_, cellY := ctx.Frame().Grid().CellPosition()
attrs := ctx.ModelVC().RowAttrs(cellY)
attrs.Marker = MarkerRed
ctx.ModelVC().SetRowAttrs(cellY, attrs)
return nil
})
cm.Define("mark-row-green", "Set row marker to green", "", func(ctx *CommandContext) error {
_, cellY := ctx.Frame().Grid().CellPosition()
attrs := ctx.ModelVC().RowAttrs(cellY)
attrs.Marker = MarkerGreen
ctx.ModelVC().SetRowAttrs(cellY, attrs)
return nil
})
cm.Define("mark-row-blue", "Set row marker to blue", "", func(ctx *CommandContext) error {
_, cellY := ctx.Frame().Grid().CellPosition()
attrs := ctx.ModelVC().RowAttrs(cellY)
attrs.Marker = MarkerBlue
ctx.ModelVC().SetRowAttrs(cellY, attrs)
return nil
})
cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error { cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error {
ctx.Frame().Prompt(PromptOptions{ Prompt: ":" }, func(res string) error { ctx.Frame().Prompt(PromptOptions{ Prompt: ":" }, func(res string) error {
return cm.Eval(ctx, res) return cm.Eval(ctx, res)
@ -217,10 +257,9 @@ func (cm *CommandMapping) RegisterViewCommands() {
cm.Define("replace-cell", "Replace the value of the selected cell", "", func(ctx *CommandContext) error { cm.Define("replace-cell", "Replace the value of the selected cell", "", func(ctx *CommandContext) error {
grid := ctx.Frame().Grid() grid := ctx.Frame().Grid()
cellX, cellY := grid.CellPosition() cellX, cellY := grid.CellPosition()
if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel { if _, isRwModel := ctx.ModelVC().Model().(RWModel); isRwModel {
ctx.Frame().Prompt(PromptOptions{ Prompt: "> " }, func(res string) error { ctx.Frame().Prompt(PromptOptions{ Prompt: "> " }, func(res string) error {
rwModel.SetCellValue(cellY, cellX, res) return ctx.ModelVC().SetCellValue(cellY, cellX, res)
return nil
}) })
} }
return nil return nil
@ -229,13 +268,12 @@ func (cm *CommandMapping) RegisterViewCommands() {
grid := ctx.Frame().Grid() grid := ctx.Frame().Grid()
cellX, cellY := grid.CellPosition() cellX, cellY := grid.CellPosition()
if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel { if _, isRwModel := ctx.ModelVC().Model().(RWModel); isRwModel {
ctx.Frame().Prompt(PromptOptions{ ctx.Frame().Prompt(PromptOptions{
Prompt: "> ", Prompt: "> ",
InitialValue: grid.Model().CellValue(cellY, cellX), InitialValue: grid.Model().CellValue(cellX, cellY),
}, func(res string) error { }, func(res string) error {
rwModel.SetCellValue(cellY, cellX, res) return ctx.ModelVC().SetCellValue(cellY, cellX, res)
return nil
}) })
} }
return nil return nil
@ -247,7 +285,7 @@ func (cm *CommandMapping) RegisterViewCommands() {
return fmt.Errorf("model is not writable") return fmt.Errorf("model is not writable")
} }
if err := wSource.Write(ctx.Session().Model); err != nil { if err := wSource.Write(ctx.ModelVC().Model()); err != nil {
return err return err
} }
@ -304,6 +342,14 @@ func (cm *CommandMapping) RegisterViewKeyBindings() {
cm.MapKey('/', cm.Command("search")) cm.MapKey('/', cm.Command("search"))
cm.MapKey('n', cm.Command("search-next")) cm.MapKey('n', cm.Command("search-next"))
cm.MapKey('0', cm.Command("clear-row-marker"))
cm.MapKey('1', cm.Command("mark-row-red"))
cm.MapKey('2', cm.Command("mark-row-green"))
cm.MapKey('3', cm.Command("mark-row-blue"))
cm.MapKey('{', cm.Command("dec-col-width"))
cm.MapKey('}', cm.Command("inc-col-width"))
cm.MapKey(':', cm.Command("enter-command")) cm.MapKey(':', cm.Command("enter-command"))
} }

View file

@ -27,25 +27,4 @@ type RWModel interface {
IsDirty() bool IsDirty() bool
} }
// Deletes a row of a model
func DeleteRow(model RWModel, row int) {
h, w := model.Dimensions()
for r := row; r < h-1; r++ {
for c := 0; c < w; c++ {
model.SetCellValue(r, c, model.CellValue(r+1, c))
}
}
model.Resize(h-1, w)
}
// Deletes a column of a model
func DeleteCol(model RWModel, col int) {
h, w := model.Dimensions()
for c := col; c < w-1; c++ {
for r := 0; r < h; r++ {
model.SetCellValue(r, c, model.CellValue(r, c+1))
}
}
model.Resize(h, w-1)
}

View file

@ -8,25 +8,28 @@ import (
// The session is responsible for managing the UI and the model and handling // The session is responsible for managing the UI and the model and handling
// the interaction between the two and the user. // the interaction between the two and the user.
type Session struct { type Session struct {
Model Model model Model
Source ModelSource Source ModelSource
Frame *Frame Frame *Frame
Commands *CommandMapping Commands *CommandMapping
UIManager *ui.Ui UIManager *ui.Ui
modelController *ModelViewCtrl
LastSearch *regexp.Regexp LastSearch *regexp.Regexp
} }
func NewSession(uiManager *ui.Ui, frame *Frame, source ModelSource) *Session { func NewSession(uiManager *ui.Ui, frame *Frame, source ModelSource) *Session {
model := NewSingleCellStdModel()
session := &Session{ session := &Session{
Model: NewSingleCellStdModel(), model: model,
Source: source, Source: source,
Frame: frame, Frame: frame,
Commands: NewCommandMapping(), Commands: NewCommandMapping(),
UIManager: uiManager, UIManager: uiManager,
modelController: NewGridViewModel(model),
} }
frame.SetModel(&SessionGridModel{session}) frame.SetModel(&SessionGridModel{session.modelController})
session.Commands.RegisterViewCommands() session.Commands.RegisterViewCommands()
session.Commands.RegisterViewKeyBindings() session.Commands.RegisterViewKeyBindings()
@ -45,7 +48,8 @@ func (session *Session) LoadFromSource() {
return return
} }
session.Model = newModel session.model = newModel
session.modelController.SetModel(newModel)
} }
// Input from the frame // Input from the frame
@ -69,6 +73,10 @@ type CommandContext struct {
session *Session session *Session
} }
func (scc *CommandContext) ModelVC() *ModelViewCtrl {
return scc.session.modelController
}
func (scc *CommandContext) Session() *Session { func (scc *CommandContext) Session() *Session {
return scc.session return scc.session
} }
@ -84,26 +92,44 @@ func (scc *CommandContext) Error(err error) {
// Session grid model // Session grid model
type SessionGridModel struct { type SessionGridModel struct {
Session *Session GridViewModel *ModelViewCtrl
} }
// Returns the size of the grid model (width x height) // Returns the size of the grid model (width x height)
func (sgm *SessionGridModel) Dimensions() (int, int) { func (sgm *SessionGridModel) Dimensions() (int, int) {
rs, cs := sgm.Session.Model.Dimensions() rs, cs := sgm.GridViewModel.Model().Dimensions()
return cs, rs return cs, rs
} }
// Returns the size of the particular column. If the size is 0, this indicates that the column is hidden. // Returns the size of the particular column. If the size is 0, this indicates that the column is hidden.
func (sgm *SessionGridModel) ColWidth(int) int { func (sgm *SessionGridModel) ColWidth(col int) int {
return 24 return sgm.GridViewModel.ColAttrs(col).Size
} }
// Returns the size of the particular row. If the size is 0, this indicates that the row is hidden. // Returns the size of the particular row. If the size is 0, this indicates that the row is hidden.
func (sgm *SessionGridModel) RowHeight(int) int { func (sgm *SessionGridModel) RowHeight(row int) int {
return 1 return sgm.GridViewModel.RowAttrs(row).Size
} }
// Returns the value of the cell a position X, Y // Returns the value of the cell a position X, Y
func (sgm *SessionGridModel) CellValue(x int, y int) string { func (sgm *SessionGridModel) CellValue(x int, y int) string {
return sgm.Session.Model.CellValue(y, x) return sgm.GridViewModel.Model().CellValue(y, x)
}
func (sgm *SessionGridModel) CellAttributes(x int, y int) (fg, bg ui.Attribute) {
rowAttrs := sgm.GridViewModel.RowAttrs(y)
colAttrs := sgm.GridViewModel.ColAttrs(y)
if rowAttrs.Marker != MarkerNone {
return markerAttributes[rowAttrs.Marker], 0
} else if colAttrs.Marker != MarkerNone {
return markerAttributes[colAttrs.Marker], 0
}
return 0,0
}
var markerAttributes = map[Marker]ui.Attribute {
MarkerRed: ui.ColorRed,
MarkerGreen: ui.ColorGreen,
MarkerBlue: ui.ColorBlue,
} }

View file

@ -28,6 +28,8 @@ type GridModel interface {
* Returns the value of the cell a position X, Y * Returns the value of the cell a position X, Y
*/ */
CellValue(int, int) string CellValue(int, int) string
CellAttributes(int, int) (fg, bg Attribute)
} }
type gridPoint int type gridPoint int
@ -173,10 +175,13 @@ func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute)
} else { } else {
// The data from the model // The data from the model
if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) { if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) {
value := grid.model.CellValue(modelCellX, modelCellY)
fg, bg := grid.model.CellAttributes(modelCellX, modelCellY)
if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) { if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) {
return grid.model.CellValue(modelCellX, modelCellY), AttrReverse, AttrReverse return value, fg | AttrReverse, bg | AttrReverse
} else { } else {
return grid.model.CellValue(modelCellX, modelCellY), 0, 0 return value, fg, bg
} }
} else { } else {
return "~", ColorBlue, 0 return "~", ColorBlue, 0
@ -365,7 +370,7 @@ func (grid *Grid) KeyPressed(key rune, mod int) {
} }
// -------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------
// Test Model // Test ModelVC
type TestModel struct { type TestModel struct {
thing int thing int

154
viewmodel.go Normal file
View file

@ -0,0 +1,154 @@
package main
import (
"errors"
)
type ModelViewCtrl struct {
model Model
rowAttrs []SliceAttr
colAttrs []SliceAttr
}
func NewGridViewModel(model Model) *ModelViewCtrl {
gvm := &ModelViewCtrl{}
gvm.SetModel(model)
return gvm
}
func (gvm *ModelViewCtrl) Model() Model {
return gvm.model
}
func (gvm *ModelViewCtrl) SetModel(m Model) {
gvm.model = m
gvm.modelWasResized()
}
func (gvm *ModelViewCtrl) RowAttrs(row int) SliceAttr {
if row < len(gvm.rowAttrs) {
return gvm.rowAttrs[row]
}
return DefaultRowAttrs
}
func (gvm *ModelViewCtrl) ColAttrs(col int) SliceAttr {
if col < len(gvm.colAttrs) {
return gvm.colAttrs[col]
}
return DefaultColAttrs
}
func (gvm *ModelViewCtrl) SetRowAttrs(row int, newAttrs SliceAttr) {
gvm.rowAttrs[row] = newAttrs
}
func (gvm *ModelViewCtrl) SetColAttrs(col int, newAttrs SliceAttr) {
gvm.colAttrs[col] = newAttrs
}
func (gvm *ModelViewCtrl) SetCellValue(r, c int, newValue string) error {
rwModel, isRWModel := gvm.model.(RWModel)
if !isRWModel {
return ErrModelReadOnly
}
rwModel.SetCellValue(r, c, newValue)
return nil
}
func (gvm *ModelViewCtrl) Resize(newRow, newCol int) error {
rwModel, isRWModel := gvm.model.(RWModel)
if !isRWModel {
return ErrModelReadOnly
}
rwModel.Resize(newRow, newCol)
gvm.modelWasResized()
return nil
}
// Deletes a row of a model
func (gvm *ModelViewCtrl) DeleteRow(row int) error {
rwModel, isRWModel := gvm.model.(RWModel)
if !isRWModel {
return ErrModelReadOnly
}
h, w := rwModel.Dimensions()
for r := row; r < h-1; r++ {
for c := 0; c < w; c++ {
rwModel.SetCellValue(r, c, rwModel.CellValue(r+1, c))
gvm.rowAttrs[r] = gvm.rowAttrs[r+1]
}
}
rwModel.Resize(h-1, w)
gvm.modelWasResized()
return nil
}
// Deletes a column of a model
func (gvm *ModelViewCtrl) DeleteCol(col int) error {
rwModel, isRWModel := gvm.model.(RWModel)
if !isRWModel {
return ErrModelReadOnly
}
h, w := rwModel.Dimensions()
for c := col; c < w-1; c++ {
for r := 0; r < h; r++ {
rwModel.SetCellValue(r, c, rwModel.CellValue(r, c+1))
gvm.colAttrs[c] = gvm.colAttrs[c+1]
}
}
rwModel.Resize(h, w-1)
gvm.modelWasResized()
return nil
}
func (gvm *ModelViewCtrl) modelWasResized() {
rows, cols := gvm.model.Dimensions()
gvm.rowAttrs = gvm.resizeAttrSlice(gvm.rowAttrs, rows, DefaultRowAttrs)
gvm.colAttrs = gvm.resizeAttrSlice(gvm.colAttrs, cols, DefaultColAttrs)
}
func (gvm *ModelViewCtrl) resizeAttrSlice(oldSlice []SliceAttr, newSize int, defaultAttrs SliceAttr) []SliceAttr {
oldLen := len(oldSlice)
newSlice := oldSlice
if newSize > oldLen {
newSlice = make([]SliceAttr, newSize)
for i := 0; i < newSize; i++ {
if i < oldLen {
newSlice[i] = oldSlice[i]
} else {
newSlice[i] = defaultAttrs
}
}
} else {
newSlice = newSlice[:newSize]
}
return newSlice
}
type SliceAttr struct {
Size int
Marker Marker
}
type Marker int
const (
MarkerNone Marker = iota
MarkerRed
MarkerGreen
MarkerBlue
)
var DefaultRowAttrs = SliceAttr{Size: 1}
var DefaultColAttrs = SliceAttr{Size: 24}
var ErrModelReadOnly = errors.New("ModelVC is read-only")