From a49613f7e9154faa4c827e6b99a7d90b77c251e1 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 16 Jun 2020 14:23:17 +1000 Subject: [PATCH] Added the notion of a grid view model. --- commandmap.go | 122 ++++++++++++++++++++++++++------------- model.go | 21 ------- session.go | 64 ++++++++++++++------- ui/grid.go | 11 +++- viewmodel.go | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 291 insertions(+), 81 deletions(-) create mode 100644 viewmodel.go diff --git a/commandmap.go b/commandmap.go index ff33e48..0bee41f 100644 --- a/commandmap.go +++ b/commandmap.go @@ -110,23 +110,13 @@ func (cm *CommandMapping) RegisterViewCommands() { grid := ctx.Frame().Grid() _, cellY := grid.CellPosition() - if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel { - DeleteRow(rwModel, cellY) - return nil - } - - return errors.New("model is read-only") + return ctx.ModelVC().DeleteRow(cellY) }) cm.Define("delete-col", "Removes the currently selected column", "", func(ctx *CommandContext) error { grid := ctx.Frame().Grid() cellX, _ := grid.CellPosition() - if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel { - DeleteCol(rwModel, cellX) - return nil - } - - return errors.New("model is read-only") + return ctx.ModelVC().DeleteCol(cellX) }) cm.Define("search", "Search for a cell", "", func(ctx *CommandContext) error { ctx.Frame().Prompt(PromptOptions{ Prompt: "/" }, func(res string) error { @@ -145,7 +135,7 @@ func (cm *CommandMapping) RegisterViewCommands() { ctx.Session().Commands.Eval(ctx, "search") } - height, width := ctx.session.Model.Dimensions() + height, width := ctx.ModelVC().Model().Dimensions() startX, startY := ctx.Frame().Grid().CellPosition() cellX, cellY := startX, startY @@ -155,7 +145,7 @@ func (cm *CommandMapping) RegisterViewCommands() { cellX = 0 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) return nil } else if (cellX == startX) && (cellY == startY) { @@ -168,30 +158,22 @@ func (cm *CommandMapping) RegisterViewCommands() { grid := ctx.Frame().Grid() cellX, _ := grid.CellPosition() - if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel { - height, width := rwModel.Dimensions() - if cellX == width-1 { - rwModel.Resize(height, width+1) - } - return nil + height, width := ctx.ModelVC().Model().Dimensions() + if cellX == width-1 { + return ctx.ModelVC().Resize(height, width+1) } - - return errors.New("model is read-only") + return nil }) cm.Define("open-down", "Inserts a row below the curser", "", func(ctx *CommandContext) error { grid := ctx.Frame().Grid() _, cellY := grid.CellPosition() - if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel { - height, width := rwModel.Dimensions() - if cellY == height-1 { - rwModel.Resize(height+1, width) - } - return nil + height, width := ctx.ModelVC().Model().Dimensions() + if cellY == height-1 { + return ctx.ModelVC().Resize(height+1, width) } - - return errors.New("model is read-only") + return nil }) 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") }) + 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 { ctx.Frame().Prompt(PromptOptions{ Prompt: ":" }, func(res string) error { 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 { grid := ctx.Frame().Grid() 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 { - rwModel.SetCellValue(cellY, cellX, res) - return nil + return ctx.ModelVC().SetCellValue(cellY, cellX, res) }) } return nil @@ -229,13 +268,12 @@ func (cm *CommandMapping) RegisterViewCommands() { grid := ctx.Frame().Grid() cellX, cellY := grid.CellPosition() - if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel { + if _, isRwModel := ctx.ModelVC().Model().(RWModel); isRwModel { ctx.Frame().Prompt(PromptOptions{ Prompt: "> ", - InitialValue: grid.Model().CellValue(cellY, cellX), + InitialValue: grid.Model().CellValue(cellX, cellY), }, func(res string) error { - rwModel.SetCellValue(cellY, cellX, res) - return nil + return ctx.ModelVC().SetCellValue(cellY, cellX, res) }) } return nil @@ -247,7 +285,7 @@ func (cm *CommandMapping) RegisterViewCommands() { 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 } @@ -304,6 +342,14 @@ func (cm *CommandMapping) RegisterViewKeyBindings() { cm.MapKey('/', cm.Command("search")) 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")) } diff --git a/model.go b/model.go index a838e20..78ba1c0 100644 --- a/model.go +++ b/model.go @@ -27,25 +27,4 @@ type RWModel interface { 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) -} diff --git a/session.go b/session.go index 6a212bd..d36d296 100644 --- a/session.go +++ b/session.go @@ -8,25 +8,28 @@ import ( // The session is responsible for managing the UI and the model and handling // the interaction between the two and the user. type Session struct { - Model Model - Source ModelSource - Frame *Frame - Commands *CommandMapping - UIManager *ui.Ui + model Model + Source ModelSource + Frame *Frame + Commands *CommandMapping + UIManager *ui.Ui + modelController *ModelViewCtrl LastSearch *regexp.Regexp } func NewSession(uiManager *ui.Ui, frame *Frame, source ModelSource) *Session { + model := NewSingleCellStdModel() session := &Session{ - Model: NewSingleCellStdModel(), - Source: source, - Frame: frame, - Commands: NewCommandMapping(), - UIManager: uiManager, + model: model, + Source: source, + Frame: frame, + Commands: NewCommandMapping(), + UIManager: uiManager, + modelController: NewGridViewModel(model), } - frame.SetModel(&SessionGridModel{session}) + frame.SetModel(&SessionGridModel{session.modelController}) session.Commands.RegisterViewCommands() session.Commands.RegisterViewKeyBindings() @@ -45,7 +48,8 @@ func (session *Session) LoadFromSource() { return } - session.Model = newModel + session.model = newModel + session.modelController.SetModel(newModel) } // Input from the frame @@ -69,6 +73,10 @@ type CommandContext struct { session *Session } +func (scc *CommandContext) ModelVC() *ModelViewCtrl { + return scc.session.modelController +} + func (scc *CommandContext) Session() *Session { return scc.session } @@ -84,26 +92,44 @@ func (scc *CommandContext) Error(err error) { // Session grid model type SessionGridModel struct { - Session *Session + GridViewModel *ModelViewCtrl } // Returns the size of the grid model (width x height) func (sgm *SessionGridModel) Dimensions() (int, int) { - rs, cs := sgm.Session.Model.Dimensions() + rs, cs := sgm.GridViewModel.Model().Dimensions() return cs, rs } // 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 { - return 24 +func (sgm *SessionGridModel) ColWidth(col int) int { + 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. -func (sgm *SessionGridModel) RowHeight(int) int { - return 1 +func (sgm *SessionGridModel) RowHeight(row int) int { + return sgm.GridViewModel.RowAttrs(row).Size } // Returns the value of the cell a position X, Y 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, +} \ No newline at end of file diff --git a/ui/grid.go b/ui/grid.go index 8ce62e5..16f6994 100644 --- a/ui/grid.go +++ b/ui/grid.go @@ -28,6 +28,8 @@ type GridModel interface { * Returns the value of the cell a position X, Y */ CellValue(int, int) string + + CellAttributes(int, int) (fg, bg Attribute) } type gridPoint int @@ -173,10 +175,13 @@ func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) } else { // The data from the model 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) { - return grid.model.CellValue(modelCellX, modelCellY), AttrReverse, AttrReverse + return value, fg | AttrReverse, bg | AttrReverse } else { - return grid.model.CellValue(modelCellX, modelCellY), 0, 0 + return value, fg, bg } } else { return "~", ColorBlue, 0 @@ -365,7 +370,7 @@ func (grid *Grid) KeyPressed(key rune, mod int) { } // -------------------------------------------------------------------------------------------- -// Test Model +// Test ModelVC type TestModel struct { thing int diff --git a/viewmodel.go b/viewmodel.go new file mode 100644 index 0000000..1d0b3cd --- /dev/null +++ b/viewmodel.go @@ -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")