1package sheet23import (4 "fmt"5 "slices"67 "src.rest/cat/012/internal/formula"8)910// Protected ranges and sheets follow Sheets' Data > Protect sheets and11// ranges in its "Show a warning when editing this range" mode: nothing is12// locked, but an edit that touches one asks first. The engine only keeps13// them (saved with the sheet, following inserted and deleted rows and14// columns, every change an undo step); the UI asks.1516// Protection is a protected range, or the whole sheet.17type Protection struct {18 Range Rect // the whole grid when Sheet is set19 Sheet bool // the whole sheet is protected20 Desc string // what the user wrote to describe it, may be ""21}2223// Label names the protection as the UI shows it: its description, or its24// range, or "the sheet".25func (p Protection) Label() string {26 switch {27 case p.Desc != "":28 return p.Desc29 case p.Sheet:30 return "the sheet"31 }32 return p.Range.String()33}3435// Protections returns the sheet's protected ranges, in the order added.36func (s *Sheet) Protections() []Protection { return slices.Clone(s.view.protected) }3738// Protecting returns the first protection r overlaps.39func (s *Sheet) Protecting(r Rect) (Protection, bool) {40 for _, p := range s.view.protected {41 if p.Sheet || overlaps(p.Range, r) {42 return p, true43 }44 }45 return Protection{}, false46}4748// Protect adds a protected range, or protects the whole sheet, as one49// undo step. Protecting the sheet again replaces its description.50func (s *Sheet) Protect(p Protection) {51 if p.Sheet {52 p.Range = Rect{To: Addr{Col: MaxCols - 1, Row: MaxRows - 1}}53 }54 next := slices.Clone(s.view.protected)55 label := "protect " + p.Range.String()56 if p.Sheet {57 label = "protect sheet " + s.name58 next = slices.DeleteFunc(next, func(q Protection) bool { return q.Sheet })59 }60 s.setProtected(label, p.Range, append(next, p))61}6263// Unprotect removes protection i (in Protections' order).64func (s *Sheet) Unprotect(i int) {65 if i < 0 || i >= len(s.view.protected) {66 return67 }68 p := s.view.protected[i]69 next := slices.Delete(slices.Clone(s.view.protected), i, i+1)70 s.setProtected("remove protection of "+p.Label(), p.Range, next)71}7273// UnprotectRange removes every protection r overlaps, the sheet's74// included, as one undo step, and returns how many there were.75func (s *Sheet) UnprotectRange(r Rect) int {76 next := slices.DeleteFunc(slices.Clone(s.view.protected), func(p Protection) bool {77 return p.Sheet || overlaps(p.Range, r)78 })79 n := len(s.view.protected) - len(next)80 if n > 0 {81 s.setProtected("remove protection", r, next)82 }83 return n84}8586func (s *Sheet) setProtected(label string, focus Rect, next []Protection) {87 if len(next) == 0 {88 next = nil89 }90 s.change(label, focus, func() {91 s.recordView()92 s.view.protected = next93 })94}9596// LoadProtection adds a protection as a loader does: without recording97// undo.98func (s *Sheet) LoadProtection(p Protection) {99 if p.Sheet {100 p.Range = Rect{To: Addr{Col: MaxCols - 1, Row: MaxRows - 1}}101 }102 s.view.protected = append(slices.Clone(s.view.protected), p)103}104105// shiftProtected moves protected ranges with inserted or deleted rows or106// columns, as a range reference moves; one whose cells are all deleted107// goes.108func shiftProtected(ps []Protection, rows bool, sp formula.Span) []Protection {109 if len(ps) == 0 {110 return ps111 }112 _, rng := formula.AxisMaps(rows, sp)113 var out []Protection114 for _, p := range ps {115 if !p.Sheet {116 r, ok := rng(p.Range)117 if !ok {118 continue119 }120 p.Range = r121 }122 out = append(out, p)123 }124 return out125}126127// fileProtection is a protection in the file: {"range": "B2:C9",128// "description": "Totals"}, or {"sheet": true}. Earlier builds ignore129// the field, so it needs no version bump; they simply don't warn.130type fileProtection struct {131 Range string `json:"range,omitempty"`132 Sheet bool `json:"sheet,omitempty"`133 Desc string `json:"description,omitempty"`134}135136func encodeProtections(ps []Protection) []fileProtection {137 var out []fileProtection138 for _, p := range ps {139 fp := fileProtection{Sheet: p.Sheet, Desc: p.Desc}140 if !p.Sheet {141 fp.Range = p.Range.String()142 }143 out = append(out, fp)144 }145 return out146}147148func decodeProtections(fps []fileProtection) ([]Protection, error) {149 var out []Protection150 for _, fp := range fps {151 p := Protection{Sheet: fp.Sheet, Desc: fp.Desc}152 if p.Sheet {153 p.Range = Rect{To: Addr{Col: MaxCols - 1, Row: MaxRows - 1}}154 } else {155 r, ok := ParseRange(fp.Range)156 if !ok {157 return nil, fmt.Errorf("invalid protected range %q", fp.Range)158 }159 p.Range = r160 }161 out = append(out, p)162 }163 return out, nil164}