diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css index 84e0970..0932e81 100644 --- a/assets/webui/ofdgo.css +++ b/assets/webui/ofdgo.css @@ -2222,6 +2222,29 @@ button.outline-link:hover { } } +@media (min-width: 901px) and (max-width: 1600px) and (pointer: fine) { + .editor-group { + padding-inline: 5px; + column-gap: 3px; + } + + .editor-toolbar .button { + padding-inline: 7px; + } + + .editor-group:has(#textFont) { + grid-template-columns: 144px 32px; + } + + .editor-paragraph { + grid-template-columns: 54px 108px; + } + + .toolbar { + --tool-gap: 6px; + } +} + @media (max-width: 900px) { :root { --sidebar-width: min(78vw, 292px); diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 23cc9d4..eb18d43 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -1642,22 +1642,34 @@ function shapeStyle() { lineWidth: Number(el.shapeWidth.value) * 25.4 / 72 }; } -async function changeShapeStyle() { - const item = canvasEditor.selected; +function selectedPath(item) { + if (!item?.items) return item?.type === "PathObject" ? item : null; + if (!item.items.every(member => member.type === "PathObject")) return null; + const path = { ...item, type: "PathObject", shape: item.items.every(member => lineShape(member.shape)) ? "line" : "" }; + for (const key of ["fill", "stroke", "fillColor", "strokeColor", "lineWidth"]) { + const value = item.items[0][key]; + path[key] = item.items.every(member => member[key] === value) ? value : undefined; + } + return path; +} + +async function changeShapeStyle({ target }) { + const item = selectedPath(canvasEditor.selected); if (!state.editing || document.body.hasAttribute("aria-busy")) { return; } - if (!el.shapeWidth.checkValidity() || Number(el.shapeWidth.value) <= 0) { - el.shapeWidth.value = String(item?.type === "PathObject" ? displayPoints(item.lineWidth) : 1); + if (canvasEditor.selected && (!item || !canEditObject(item, "update"))) return; + if (target === el.shapeWidth && (!el.shapeWidth.checkValidity() || Number(el.shapeWidth.value) <= 0)) { + el.shapeWidth.value = item ? item.lineWidth === undefined ? "" : String(displayPoints(item.lineWidth)) : "1"; return; } - if (!el.shapeFill.checked && !el.shapeStroke.checked) { - el.shapeStroke.checked = true; - } - const style = shapeStyle(); - if (item?.type === "PathObject") { - await changeDocument("ofdgoUpdatePathStyle", item, style.fill, style.fillColor, style.stroke, style.strokeColor, - inputMillimeters(el.shapeWidth, item.lineWidth)); + if (item) { + const values = [el.shapeFill, el.shapeFillColor, el.shapeStroke, el.shapeStrokeColor, el.shapeWidth].map(input => { + if (input !== target) return null; + if (input === el.shapeFill || input === el.shapeStroke) return input.checked; + return input === el.shapeWidth ? inputMillimeters(input, item.lineWidth) : input.value; + }); + await changeDocument("ofdgoUpdatePathStyle", { ...item, id: (item.items || [item]).map(member => member.id) }, ...values); updateObjectControls(canvasEditor.selected, true); } else { el.shapeWidth.value = String(Number(el.shapeWidth.value)); @@ -1676,11 +1688,14 @@ function toggleDrawingTool(tool) { function updateDrawingControls() { const tool = canvasEditor.tool; + const path = selectedPath(canvasEditor.selected); el.insertTextButton.setAttribute("aria-pressed", String(tool === "text")); - const line = lineShape(tool) || !tool && lineShape(canvasEditor.selected?.shape); + const line = lineShape(tool) || !tool && lineShape(path?.shape); if (line) { el.shapeFill.checked = false; el.shapeStroke.checked = true; + } else if (!path && !el.shapeFill.checked && !el.shapeStroke.checked) { + el.shapeStroke.checked = true; } const disabled = !state.editing || !state.ready || state.exporting; el.arrowTool.disabled = disabled || !pageCan("insert"); @@ -1691,11 +1706,10 @@ function updateDrawingControls() { el.eraseButton.disabled = el.eraseMode.disabled = disabled; el.eraseButton.setAttribute("aria-pressed", String(tool.startsWith("erase-"))); el.selectObjectButton.setAttribute("aria-pressed", String(canvasEditor.enabled && !tool)); - const styleDisabled = disabled || (canvasEditor.selected?.type === "PathObject" - ? !canEditObject(canvasEditor.selected, "update") : !pageCan("insert")); + const styleDisabled = disabled || (canvasEditor.selected ? !path || !canEditObject(path, "update") : !pageCan("insert")); el.shapeFill.disabled = el.shapeStroke.disabled = styleDisabled || line; - el.shapeFillColor.disabled = styleDisabled || line || !el.shapeFill.checked; - el.shapeStrokeColor.disabled = el.shapeWidth.disabled = styleDisabled || !line && !el.shapeStroke.checked; + el.shapeFillColor.disabled = styleDisabled || line || !el.shapeFill.checked && !el.shapeFill.indeterminate; + el.shapeStrokeColor.disabled = el.shapeWidth.disabled = styleDisabled || !line && !el.shapeStroke.checked && !el.shapeStroke.indeterminate; } async function changeDocument(name, item, ...args) { @@ -5220,14 +5234,24 @@ function updateObjectControls(item, reset = false) { if (reset || el.textColor.disabled || document.activeElement !== el.textColor) { el.textColor.value = text.color || "#000000"; } - if (item?.type === "PathObject" && canEditObject(item, "update")) { - el.shapeFill.checked = item.fill; - el.shapeStroke.checked = item.stroke; - el.shapeFillColor.value = item.fillColor; - el.shapeStrokeColor.value = item.strokeColor; + const path = selectedPath(item); + for (const [input, key] of [[el.shapeFill, "fill"], [el.shapeStroke, "stroke"]]) { + input.indeterminate = Boolean(path?.items && path[key] === undefined); + if (path) input.checked = Boolean(path[key]); + } + for (const [input, key, label] of [[el.shapeFillColor, "fillColor", "填充颜色"], [el.shapeStrokeColor, "strokeColor", "描边颜色"]]) { + const mixed = Boolean(path?.items && path[key] === undefined); + input.classList.toggle("mixed-color", mixed); + input.title = mixed ? `${label}:混合` : label; + if (path && (reset || document.activeElement !== input)) input.value = path[key] || "#000000"; + } + el.shapeWidth.placeholder = path?.items && path.lineWidth === undefined ? "混合" : ""; + if (path && canEditObject(path, "update")) { if (reset || document.activeElement !== el.shapeWidth) { - el.shapeWidth.value = displayPoints(item.lineWidth); + el.shapeWidth.value = path.lineWidth === undefined ? "" : displayPoints(path.lineWidth); } + } else if (!item && !el.shapeWidth.value) { + el.shapeWidth.value = "1"; } updateDrawingControls(); const members = item?.items || (item ? [item] : []); diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 8f274e9..7e18059 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -1556,33 +1556,58 @@ func editorPathScale(object ofdgo.PathObject) float64 { } // setPathStyle 应用图形填充、描边和线宽 -// 入参: object 路径对象, args 填充开关及色值、描边开关及色值、线宽 +// 入参: object 路径对象, args 填充开关及色值、描边开关及色值、线宽,null保持原值 // 返回: error 错误信息 func setPathStyle(object *ofdgo.PathObject, args []js.Value) error { - fill, stroke, width := args[0].Bool(), args[2].Bool(), args[4].Float() - if kind, _ := object.Shape(); kind == ofdgo.ShapeLine { + fill, stroke := object.Fill != nil && *object.Fill, object.Stroke == nil || *object.Stroke + if !args[0].IsNull() { + fill = args[0].Bool() + } + if !args[2].IsNull() { + stroke = args[2].Bool() + } + if kind, _ := object.Shape(); kind == ofdgo.ShapeLine || kind == ofdgo.ShapeArrow || kind == ofdgo.ShapeDoubleArrow { fill, stroke = false, true } if !fill && !stroke { - return fmt.Errorf("fill or stroke must be enabled") + if args[0].IsNull() || args[2].IsNull() { + stroke = true + } else { + return fmt.Errorf("fill or stroke must be enabled") + } } - if width <= 0 || math.IsNaN(width) || math.IsInf(width, 0) { - return fmt.Errorf("line width must be positive and finite") + if !args[0].IsNull() || fill != (object.Fill != nil && *object.Fill) { + object.Fill = &fill } - object.Fill, object.Stroke = &fill, &stroke - if scale := editorPathScale(*object); width != object.LineWidth*scale { - object.LineWidth = width / scale + if !args[2].IsNull() || stroke != (object.Stroke == nil || *object.Stroke) { + object.Stroke = &stroke } - if object.FillColor == nil { - object.FillColor = &ofdgo.FillColor{} + if !args[4].IsNull() { + width := args[4].Float() + if width <= 0 || math.IsNaN(width) || math.IsInf(width, 0) { + return fmt.Errorf("line width must be positive and finite") + } + if scale := editorPathScale(*object); width != object.LineWidth*scale { + object.LineWidth = width / scale + } } - if object.StrokeColor == nil { - object.StrokeColor = &ofdgo.StrokeColor{} + if !args[1].IsNull() { + if object.FillColor == nil { + object.FillColor = &ofdgo.FillColor{} + } + if err := setEditorColor(object.FillColor, args[1].String()); err != nil { + return err + } } - if err := setEditorColor(object.FillColor, args[1].String()); err != nil { - return err + if !args[3].IsNull() { + if object.StrokeColor == nil { + object.StrokeColor = &ofdgo.StrokeColor{} + } + if err := setEditorColor((*ofdgo.FillColor)(object.StrokeColor), args[3].String()); err != nil { + return err + } } - return setEditorColor((*ofdgo.FillColor)(object.StrokeColor), args[3].String()) + return nil } // insertShape 创建标准路径图形 @@ -1605,32 +1630,28 @@ func insertShape(args []js.Value) (any, error) { return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations) } -// updatePathStyle 更新选中路径的绘制样式 -// 入参: args 页码、对象标识和绘制样式 +// updatePathStyle 原子更新选中路径的绘制样式,保留未修改属性 +// 入参: args 页码、对象标识列表和绘制样式 // 返回: any 文档信息, error 错误信息 func updatePathStyle(args []js.Value) (any, error) { - if currentEditor == nil { - return nil, fmt.Errorf("no document is being edited") - } - page, id := args[0].Int(), args[1].String() - object, err := currentEditor.Object(page, id) - if err != nil { - return nil, err - } - if object.Type != "PathObject" { - return nil, fmt.Errorf("object is not a path") - } - if err := setPathStyle(&object.PathObject, args[2:]); err != nil { - return nil, err - } - revision := currentEditor.Revision() - if err := currentEditor.UpdateObject(page, id, object); err != nil { - return nil, err - } - if currentEditor.Revision() == revision { - return editorSummary(), nil - } - return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations) + return changeObjects(func() error { + page, ids := args[0].Int(), stringsFromJS(args[1]) + objects := make([]ofdgo.GraphicObject, len(ids)) + for i, id := range ids { + object, err := currentEditor.Object(page, id) + if err != nil { + return err + } + if object.Type != "PathObject" { + return fmt.Errorf("object is not a path") + } + if err := setPathStyle(&object.PathObject, args[2:]); err != nil { + return err + } + objects[i] = object + } + return currentEditor.UpdateObjects(page, objects) + }) } // insertText 使用选定字体创建文字对象