Skip to content

Commit b667987

Browse files
committed
This closes qax-os#301, support delete and add radio button form control
- New exported function `DeleteFormControl` has been added - Update unit tests - Fix comments was missing after form control added - Update pull request templates
1 parent 2c8dc5c commit b667987

File tree

4 files changed

+212
-79
lines changed

4 files changed

+212
-79
lines changed

PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

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

2626
## Types of changes
2727

vml.go

Lines changed: 164 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type FormControlType byte
2828
const (
2929
FormControlNote FormControlType = iota
3030
FormControlButton
31+
FormControlCheckbox
3132
FormControlRadio
3233
)
3334

@@ -114,8 +115,9 @@ func (f *File) AddComment(sheet string, opts Comment) error {
114115
})
115116
}
116117

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

320+
// DeleteFormControl provides the method to delete form control in a worksheet
321+
// by given worksheet name and cell reference. For example, delete the form
322+
// control in Sheet1!$A$30:
323+
//
324+
// err := f.DeleteFormControl("Sheet1", "A30")
325+
func (f *File) DeleteFormControl(sheet, cell string) error {
326+
ws, err := f.workSheetReader(sheet)
327+
if err != nil {
328+
return err
329+
}
330+
col, row, err := CellNameToCoordinates(cell)
331+
if err != nil {
332+
return err
333+
}
334+
if ws.LegacyDrawing == nil {
335+
return err
336+
}
337+
sheetRelationshipsDrawingVML := f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID)
338+
vmlID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
339+
drawingVML := strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
340+
vml := f.VMLDrawing[drawingVML]
341+
if vml == nil {
342+
vml = &vmlDrawing{
343+
XMLNSv: "urn:schemas-microsoft-com:vml",
344+
XMLNSo: "urn:schemas-microsoft-com:office:office",
345+
XMLNSx: "urn:schemas-microsoft-com:office:excel",
346+
XMLNSmv: "http://macVmlSchemaUri",
347+
ShapeLayout: &xlsxShapeLayout{
348+
Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: vmlID},
349+
},
350+
ShapeType: &xlsxShapeType{
351+
Stroke: &xlsxStroke{JoinStyle: "miter"},
352+
VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"},
353+
},
354+
}
355+
// load exist VML shapes from xl/drawings/vmlDrawing%d.vml
356+
d, err := f.decodeVMLDrawingReader(drawingVML)
357+
if err != nil {
358+
return err
359+
}
360+
if d != nil {
361+
vml.ShapeType.ID = d.ShapeType.ID
362+
vml.ShapeType.CoordSize = d.ShapeType.CoordSize
363+
vml.ShapeType.Spt = d.ShapeType.Spt
364+
vml.ShapeType.Path = d.ShapeType.Path
365+
for _, v := range d.Shape {
366+
s := xlsxShape{
367+
ID: v.ID,
368+
Type: v.Type,
369+
Style: v.Style,
370+
Button: v.Button,
371+
Filled: v.Filled,
372+
FillColor: v.FillColor,
373+
InsetMode: v.InsetMode,
374+
Stroked: v.Stroked,
375+
StrokeColor: v.StrokeColor,
376+
Val: v.Val,
377+
}
378+
vml.Shape = append(vml.Shape, s)
379+
}
380+
}
381+
}
382+
for i, sp := range vml.Shape {
383+
var shapeVal decodeShapeVal
384+
if err = xml.Unmarshal([]byte(fmt.Sprintf("<shape>%s</shape>", sp.Val)), &shapeVal); err == nil &&
385+
shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Column == col-1 && shapeVal.ClientData.Row == row-1 {
386+
vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...)
387+
break
388+
}
389+
}
390+
f.VMLDrawing[drawingVML] = vml
391+
return err
392+
}
393+
318394
// countVMLDrawing provides a function to get VML drawing files count storage
319395
// in the folder xl/drawings.
320396
func (f *File) countVMLDrawing() int {
@@ -380,20 +456,16 @@ func (f *File) addVMLObject(opts vmlOptions) error {
380456
}
381457
drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
382458
sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
459+
sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet)
460+
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
383461
if ws.LegacyDrawing != nil {
384462
// The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
385463
sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.Sheet, ws.LegacyDrawing.RID)
386464
vmlID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
387465
drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
388466
} else {
389467
// Add first VML drawing for given sheet.
390-
sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet)
391-
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
392468
rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
393-
if !opts.FormCtrl {
394-
sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml"
395-
f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
396-
}
397469
f.addSheetNameSpace(opts.Sheet, SourceRelationship)
398470
f.addSheetLegacyDrawing(opts.Sheet, rID)
399471
}
@@ -405,6 +477,10 @@ func (f *File) addVMLObject(opts vmlOptions) error {
405477
if err = f.addComment(commentsXML, opts); err != nil {
406478
return err
407479
}
480+
if sheetXMLPath, ok := f.getSheetXMLPath(opts.Sheet); ok && f.getSheetComments(filepath.Base(sheetXMLPath)) == "" {
481+
sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml"
482+
f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
483+
}
408484
}
409485
return f.addContentTypePart(vmlID, "comments")
410486
}
@@ -475,75 +551,87 @@ func formCtrlText(opts *vmlOptions) []vmlFont {
475551
return font
476552
}
477553

478-
var (
479-
formCtrlPresets = map[FormControlType]struct {
480-
objectType string
481-
filled string
482-
fillColor string
483-
stroked string
484-
strokeColor string
485-
strokeButton string
486-
fill *vFill
487-
textHAlign string
488-
textVAlign string
489-
noThreeD *string
490-
firstButton *string
491-
shadow *vShadow
492-
}{
493-
FormControlNote: {
494-
objectType: "Note",
495-
filled: "",
496-
fillColor: "#FBF6D6",
497-
stroked: "",
498-
strokeColor: "#EDEAA1",
499-
strokeButton: "",
500-
fill: &vFill{
501-
Color2: "#FBFE82",
502-
Angle: -180,
503-
Type: "gradient",
504-
Fill: &oFill{Ext: "view", Type: "gradientUnscaled"},
505-
},
506-
textHAlign: "",
507-
textVAlign: "",
508-
noThreeD: nil,
509-
firstButton: nil,
510-
shadow: &vShadow{On: "t", Color: "black", Obscured: "t"},
511-
},
512-
FormControlButton: {
513-
objectType: "Button",
514-
filled: "",
515-
fillColor: "buttonFace [67]",
516-
stroked: "",
517-
strokeColor: "windowText [64]",
518-
strokeButton: "t",
519-
fill: &vFill{
520-
Color2: "buttonFace [67]",
521-
Angle: -180,
522-
Type: "gradient",
523-
Fill: &oFill{Ext: "view", Type: "gradientUnscaled"},
524-
},
525-
textHAlign: "Center",
526-
textVAlign: "Center",
527-
noThreeD: nil,
528-
firstButton: nil,
529-
shadow: nil,
554+
var formCtrlPresets = map[FormControlType]struct {
555+
objectType string
556+
filled string
557+
fillColor string
558+
stroked string
559+
strokeColor string
560+
strokeButton string
561+
fill *vFill
562+
textHAlign string
563+
textVAlign string
564+
noThreeD *string
565+
firstButton *string
566+
shadow *vShadow
567+
}{
568+
FormControlNote: {
569+
objectType: "Note",
570+
filled: "",
571+
fillColor: "#FBF6D6",
572+
stroked: "",
573+
strokeColor: "#EDEAA1",
574+
strokeButton: "",
575+
fill: &vFill{
576+
Color2: "#FBFE82",
577+
Angle: -180,
578+
Type: "gradient",
579+
Fill: &oFill{Ext: "view", Type: "gradientUnscaled"},
530580
},
531-
FormControlRadio: {
532-
objectType: "Radio",
533-
filled: "f",
534-
fillColor: "window [65]",
535-
stroked: "f",
536-
strokeColor: "windowText [64]",
537-
strokeButton: "",
538-
fill: nil,
539-
textHAlign: "",
540-
textVAlign: "Center",
541-
noThreeD: stringPtr(""),
542-
firstButton: stringPtr(""),
543-
shadow: nil,
581+
textHAlign: "",
582+
textVAlign: "",
583+
noThreeD: nil,
584+
firstButton: nil,
585+
shadow: &vShadow{On: "t", Color: "black", Obscured: "t"},
586+
},
587+
FormControlButton: {
588+
objectType: "Button",
589+
filled: "",
590+
fillColor: "buttonFace [67]",
591+
stroked: "",
592+
strokeColor: "windowText [64]",
593+
strokeButton: "t",
594+
fill: &vFill{
595+
Color2: "buttonFace [67]",
596+
Angle: -180,
597+
Type: "gradient",
598+
Fill: &oFill{Ext: "view", Type: "gradientUnscaled"},
544599
},
545-
}
546-
)
600+
textHAlign: "Center",
601+
textVAlign: "Center",
602+
noThreeD: nil,
603+
firstButton: nil,
604+
shadow: nil,
605+
},
606+
FormControlCheckbox: {
607+
objectType: "Checkbox",
608+
filled: "f",
609+
fillColor: "window [65]",
610+
stroked: "f",
611+
strokeColor: "windowText [64]",
612+
strokeButton: "",
613+
fill: nil,
614+
textHAlign: "",
615+
textVAlign: "Center",
616+
noThreeD: stringPtr(""),
617+
firstButton: nil,
618+
shadow: nil,
619+
},
620+
FormControlRadio: {
621+
objectType: "Radio",
622+
filled: "f",
623+
fillColor: "window [65]",
624+
stroked: "f",
625+
strokeColor: "windowText [64]",
626+
strokeButton: "",
627+
fill: nil,
628+
textHAlign: "",
629+
textVAlign: "Center",
630+
noThreeD: stringPtr(""),
631+
firstButton: stringPtr(""),
632+
shadow: nil,
633+
},
634+
}
547635

548636
// addDrawingVML provides a function to create VML drawing XML as
549637
// xl/drawings/vmlDrawing%d.vml by given data ID, XML path and VML options. The
@@ -634,7 +722,7 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er
634722
if opts.FormCtrl {
635723
sp.ClientData.FmlaMacro = opts.Macro
636724
}
637-
if opts.Type == FormControlRadio && opts.Checked {
725+
if (opts.Type == FormControlCheckbox || opts.Type == FormControlRadio) && opts.Checked {
638726
sp.ClientData.Checked = stringPtr("1")
639727
}
640728
s, _ := xml.Marshal(sp)

vmlDrawing.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,20 @@ type decodeShape struct {
172172
Val string `xml:",innerxml"`
173173
}
174174

175+
// decodeShapeVal defines the structure used to parse the sub-element of the
176+
// shape in the file xl/drawings/vmlDrawing%d.vml.
177+
type decodeShapeVal struct {
178+
ClientData decodeVMLClientData `xml:"ClientData"`
179+
}
180+
181+
// decodeVMLClientData defines the structure used to parse the x:ClientData
182+
// element in the file xl/drawings/vmlDrawing%d.vml.
183+
type decodeVMLClientData struct {
184+
ObjectType string `xml:"ObjectType,attr"`
185+
Column int
186+
Row int
187+
}
188+
175189
// encodeShape defines the structure used to re-serialization shape element.
176190
type encodeShape struct {
177191
Fill *vFill `xml:"v:fill"`

vml_test.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func TestAddDrawingVML(t *testing.T) {
155155
assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8")
156156
}
157157

158-
func TestAddFormControl(t *testing.T) {
158+
func TestFormControl(t *testing.T) {
159159
f := NewFile()
160160
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
161161
Cell: "D1",
@@ -185,12 +185,23 @@ func TestAddFormControl(t *testing.T) {
185185
}))
186186
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
187187
Cell: "A5",
188+
Type: FormControlCheckbox,
189+
Text: "Check Box 1",
190+
Checked: true,
191+
}))
192+
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
193+
Cell: "A6",
194+
Type: FormControlCheckbox,
195+
Text: "Check Box 2",
196+
}))
197+
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
198+
Cell: "A7",
188199
Type: FormControlRadio,
189200
Text: "Option Button 1",
190201
Checked: true,
191202
}))
192203
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
193-
Cell: "A6",
204+
Cell: "A8",
194205
Type: FormControlRadio,
195206
Text: "Option Button 2",
196207
}))
@@ -221,4 +232,24 @@ func TestAddFormControl(t *testing.T) {
221232
Macro: "Button1_Click",
222233
}), newNoExistSheetError("SheetN"))
223234
assert.NoError(t, f.Close())
235+
// Test delete form control
236+
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
237+
assert.NoError(t, err)
238+
assert.NoError(t, f.DeleteFormControl("Sheet1", "D1"))
239+
assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
240+
// Test delete form control on not exists worksheet
241+
assert.Equal(t, f.DeleteFormControl("SheetN", "A1"), newNoExistSheetError("SheetN"))
242+
// Test delete form control on not exists worksheet
243+
assert.Equal(t, f.DeleteFormControl("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
244+
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteFormControl.xlsm")))
245+
assert.NoError(t, f.Close())
246+
// Test delete form control with expected element
247+
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
248+
assert.NoError(t, err)
249+
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
250+
assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8")
251+
assert.NoError(t, f.Close())
252+
// Test delete form control on a worksheet without form control
253+
f = NewFile()
254+
assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
224255
}

0 commit comments

Comments
 (0)