Skip to content

Commit

Permalink
This closes qax-os#301, support delete and add radio button form control
Browse files Browse the repository at this point in the history
- New exported function `DeleteFormControl` has been added
- Update unit tests
- Fix comments was missing after form control added
- Update pull request templates
  • Loading branch information
xuri committed Jul 12, 2023
1 parent 2c8dc5c commit b667987
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 79 deletions.
2 changes: 1 addition & 1 deletion PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
<!--- See how your change affects other areas of the code, etc. -->

## Types of changes

Expand Down
240 changes: 164 additions & 76 deletions vml.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type FormControlType byte
const (
FormControlNote FormControlType = iota
FormControlButton
FormControlCheckbox
FormControlRadio
)

Expand Down Expand Up @@ -114,8 +115,9 @@ func (f *File) AddComment(sheet string, opts Comment) error {
})
}

// DeleteComment provides the method to delete comment in a sheet by given
// worksheet name. For example, delete the comment in Sheet1!$A$30:
// DeleteComment provides the method to delete comment in a worksheet by given
// worksheet name and cell reference. For example, delete the comment in
// Sheet1!$A$30:
//
// err := f.DeleteComment("Sheet1", "A30")
func (f *File) DeleteComment(sheet, cell string) error {
Expand Down Expand Up @@ -315,6 +317,80 @@ func (f *File) AddFormControl(sheet string, opts FormControl) error {
})
}

// DeleteFormControl provides the method to delete form control in a worksheet
// by given worksheet name and cell reference. For example, delete the form
// control in Sheet1!$A$30:
//
// err := f.DeleteFormControl("Sheet1", "A30")
func (f *File) DeleteFormControl(sheet, cell string) error {
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return err
}
if ws.LegacyDrawing == nil {
return err
}
sheetRelationshipsDrawingVML := f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID)
vmlID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
drawingVML := strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
vml := f.VMLDrawing[drawingVML]
if vml == nil {
vml = &vmlDrawing{
XMLNSv: "urn:schemas-microsoft-com:vml",
XMLNSo: "urn:schemas-microsoft-com:office:office",
XMLNSx: "urn:schemas-microsoft-com:office:excel",
XMLNSmv: "http://macVmlSchemaUri",
ShapeLayout: &xlsxShapeLayout{
Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: vmlID},
},
ShapeType: &xlsxShapeType{
Stroke: &xlsxStroke{JoinStyle: "miter"},
VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"},
},
}
// load exist VML shapes from xl/drawings/vmlDrawing%d.vml
d, err := f.decodeVMLDrawingReader(drawingVML)
if err != nil {
return err
}
if d != nil {
vml.ShapeType.ID = d.ShapeType.ID
vml.ShapeType.CoordSize = d.ShapeType.CoordSize
vml.ShapeType.Spt = d.ShapeType.Spt
vml.ShapeType.Path = d.ShapeType.Path
for _, v := range d.Shape {
s := xlsxShape{
ID: v.ID,
Type: v.Type,
Style: v.Style,
Button: v.Button,
Filled: v.Filled,
FillColor: v.FillColor,
InsetMode: v.InsetMode,
Stroked: v.Stroked,
StrokeColor: v.StrokeColor,
Val: v.Val,
}
vml.Shape = append(vml.Shape, s)
}
}
}
for i, sp := range vml.Shape {
var shapeVal decodeShapeVal
if err = xml.Unmarshal([]byte(fmt.Sprintf("<shape>%s</shape>", sp.Val)), &shapeVal); err == nil &&
shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Column == col-1 && shapeVal.ClientData.Row == row-1 {
vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...)
break
}
}
f.VMLDrawing[drawingVML] = vml
return err
}

// countVMLDrawing provides a function to get VML drawing files count storage
// in the folder xl/drawings.
func (f *File) countVMLDrawing() int {
Expand Down Expand Up @@ -380,20 +456,16 @@ func (f *File) addVMLObject(opts vmlOptions) error {
}
drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet)
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
if ws.LegacyDrawing != nil {
// The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.Sheet, ws.LegacyDrawing.RID)
vmlID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
} else {
// Add first VML drawing for given sheet.
sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet)
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
if !opts.FormCtrl {
sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml"
f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
}
f.addSheetNameSpace(opts.Sheet, SourceRelationship)
f.addSheetLegacyDrawing(opts.Sheet, rID)
}
Expand All @@ -405,6 +477,10 @@ func (f *File) addVMLObject(opts vmlOptions) error {
if err = f.addComment(commentsXML, opts); err != nil {
return err
}
if sheetXMLPath, ok := f.getSheetXMLPath(opts.Sheet); ok && f.getSheetComments(filepath.Base(sheetXMLPath)) == "" {
sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml"
f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
}
}
return f.addContentTypePart(vmlID, "comments")
}
Expand Down Expand Up @@ -475,75 +551,87 @@ func formCtrlText(opts *vmlOptions) []vmlFont {
return font
}

var (
formCtrlPresets = map[FormControlType]struct {
objectType string
filled string
fillColor string
stroked string
strokeColor string
strokeButton string
fill *vFill
textHAlign string
textVAlign string
noThreeD *string
firstButton *string
shadow *vShadow
}{
FormControlNote: {
objectType: "Note",
filled: "",
fillColor: "#FBF6D6",
stroked: "",
strokeColor: "#EDEAA1",
strokeButton: "",
fill: &vFill{
Color2: "#FBFE82",
Angle: -180,
Type: "gradient",
Fill: &oFill{Ext: "view", Type: "gradientUnscaled"},
},
textHAlign: "",
textVAlign: "",
noThreeD: nil,
firstButton: nil,
shadow: &vShadow{On: "t", Color: "black", Obscured: "t"},
},
FormControlButton: {
objectType: "Button",
filled: "",
fillColor: "buttonFace [67]",
stroked: "",
strokeColor: "windowText [64]",
strokeButton: "t",
fill: &vFill{
Color2: "buttonFace [67]",
Angle: -180,
Type: "gradient",
Fill: &oFill{Ext: "view", Type: "gradientUnscaled"},
},
textHAlign: "Center",
textVAlign: "Center",
noThreeD: nil,
firstButton: nil,
shadow: nil,
var formCtrlPresets = map[FormControlType]struct {
objectType string
filled string
fillColor string
stroked string
strokeColor string
strokeButton string
fill *vFill
textHAlign string
textVAlign string
noThreeD *string
firstButton *string
shadow *vShadow
}{
FormControlNote: {
objectType: "Note",
filled: "",
fillColor: "#FBF6D6",
stroked: "",
strokeColor: "#EDEAA1",
strokeButton: "",
fill: &vFill{
Color2: "#FBFE82",
Angle: -180,
Type: "gradient",
Fill: &oFill{Ext: "view", Type: "gradientUnscaled"},
},
FormControlRadio: {
objectType: "Radio",
filled: "f",
fillColor: "window [65]",
stroked: "f",
strokeColor: "windowText [64]",
strokeButton: "",
fill: nil,
textHAlign: "",
textVAlign: "Center",
noThreeD: stringPtr(""),
firstButton: stringPtr(""),
shadow: nil,
textHAlign: "",
textVAlign: "",
noThreeD: nil,
firstButton: nil,
shadow: &vShadow{On: "t", Color: "black", Obscured: "t"},
},
FormControlButton: {
objectType: "Button",
filled: "",
fillColor: "buttonFace [67]",
stroked: "",
strokeColor: "windowText [64]",
strokeButton: "t",
fill: &vFill{
Color2: "buttonFace [67]",
Angle: -180,
Type: "gradient",
Fill: &oFill{Ext: "view", Type: "gradientUnscaled"},
},
}
)
textHAlign: "Center",
textVAlign: "Center",
noThreeD: nil,
firstButton: nil,
shadow: nil,
},
FormControlCheckbox: {
objectType: "Checkbox",
filled: "f",
fillColor: "window [65]",
stroked: "f",
strokeColor: "windowText [64]",
strokeButton: "",
fill: nil,
textHAlign: "",
textVAlign: "Center",
noThreeD: stringPtr(""),
firstButton: nil,
shadow: nil,
},
FormControlRadio: {
objectType: "Radio",
filled: "f",
fillColor: "window [65]",
stroked: "f",
strokeColor: "windowText [64]",
strokeButton: "",
fill: nil,
textHAlign: "",
textVAlign: "Center",
noThreeD: stringPtr(""),
firstButton: stringPtr(""),
shadow: nil,
},
}

// addDrawingVML provides a function to create VML drawing XML as
// xl/drawings/vmlDrawing%d.vml by given data ID, XML path and VML options. The
Expand Down Expand Up @@ -634,7 +722,7 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er
if opts.FormCtrl {
sp.ClientData.FmlaMacro = opts.Macro
}
if opts.Type == FormControlRadio && opts.Checked {
if (opts.Type == FormControlCheckbox || opts.Type == FormControlRadio) && opts.Checked {
sp.ClientData.Checked = stringPtr("1")
}
s, _ := xml.Marshal(sp)
Expand Down
14 changes: 14 additions & 0 deletions vmlDrawing.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@ type decodeShape struct {
Val string `xml:",innerxml"`
}

// decodeShapeVal defines the structure used to parse the sub-element of the
// shape in the file xl/drawings/vmlDrawing%d.vml.
type decodeShapeVal struct {
ClientData decodeVMLClientData `xml:"ClientData"`
}

// decodeVMLClientData defines the structure used to parse the x:ClientData
// element in the file xl/drawings/vmlDrawing%d.vml.
type decodeVMLClientData struct {
ObjectType string `xml:"ObjectType,attr"`
Column int
Row int
}

// encodeShape defines the structure used to re-serialization shape element.
type encodeShape struct {
Fill *vFill `xml:"v:fill"`
Expand Down
35 changes: 33 additions & 2 deletions vml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func TestAddDrawingVML(t *testing.T) {
assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8")
}

func TestAddFormControl(t *testing.T) {
func TestFormControl(t *testing.T) {
f := NewFile()
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "D1",
Expand Down Expand Up @@ -185,12 +185,23 @@ func TestAddFormControl(t *testing.T) {
}))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A5",
Type: FormControlCheckbox,
Text: "Check Box 1",
Checked: true,
}))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A6",
Type: FormControlCheckbox,
Text: "Check Box 2",
}))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A7",
Type: FormControlRadio,
Text: "Option Button 1",
Checked: true,
}))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A6",
Cell: "A8",
Type: FormControlRadio,
Text: "Option Button 2",
}))
Expand Down Expand Up @@ -221,4 +232,24 @@ func TestAddFormControl(t *testing.T) {
Macro: "Button1_Click",
}), newNoExistSheetError("SheetN"))
assert.NoError(t, f.Close())
// Test delete form control
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
assert.NoError(t, err)
assert.NoError(t, f.DeleteFormControl("Sheet1", "D1"))
assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
// Test delete form control on not exists worksheet
assert.Equal(t, f.DeleteFormControl("SheetN", "A1"), newNoExistSheetError("SheetN"))
// Test delete form control on not exists worksheet
assert.Equal(t, f.DeleteFormControl("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteFormControl.xlsm")))
assert.NoError(t, f.Close())
// Test delete form control with expected element
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
assert.NoError(t, err)
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test delete form control on a worksheet without form control
f = NewFile()
assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
}

0 comments on commit b667987

Please sign in to comment.