diff --git a/src/taskpane/components/MatchProperties.tsx b/src/taskpane/components/MatchProperties.tsx index 30ecd455..72b53f7b 100644 --- a/src/taskpane/components/MatchProperties.tsx +++ b/src/taskpane/components/MatchProperties.tsx @@ -1,231 +1,340 @@ import * as React from "react"; -import { - Button -} from "@fluentui/react-components"; -import { - ColorRegular -} from "@fluentui/react-icons"; +import { ColorRegular } from "@fluentui/react-icons"; import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; +import ActionButton from "./ActionButton"; +import { getErrorMessage } from "../types/office-types"; + +// Property types that can be matched +enum PropertyType { + Line = "line", + Fill = "fill", + Text = "text" +} + +// Shape property loading configuration +const SHAPE_PROPERTIES = { + line: [ + "lineFormat/weight", + "lineFormat/dashStyle", + "lineFormat/color" + ], + fill: [ + "fill/transparency", + "fill/foregroundColor" + ], + text: [ + "textFrame" + ] +}; + +// Font property loading configuration +const FONT_PROPERTIES = [ + "font/name", + "font/size", + "font/bold", + "font/italic", + "font/underline", + "font/color" +]; export const MatchProperties: React.FC = () => { const styles = useCommonStyles(); - const { - statusMessage, setStatusMessage, - statusType, setStatusType, - isProcessing, setIsProcessing - } = useStatusContext(); + const { setStatusMessage, setStatusType } = useStatusContext(); - const matchPropertiesToFirstSelected = async () => { - setIsProcessing(true); - try { - await PowerPoint.run(async (context) => { - // Get the selected shapes - const shapes = context.presentation.getSelectedShapes(); - shapes.load("items"); - await context.sync(); - - // Check if shapes are selected - if (shapes.items.length === 0) { - setStatusMessage("No shapes are selected. Please select shapes first."); - setStatusType("warning"); - return; - } - - // Check if there is more than one shape selected - if (shapes.items.length === 1) { - setStatusMessage("Please select multiple shapes to copy properties."); - setStatusType("warning"); - return; - } - - // Get the first shape to use as template - const firstShape = shapes.items[0]; - - // Load all necessary properties from the first shape - firstShape.load([ - "lineFormat/weight", - "lineFormat/dashStyle", - "lineFormat/color", - "fill/transparency", - "fill/foregroundColor", - "textFrame" - ]); - - await context.sync(); - - // Check if the shape has text and load text properties - let hasText = false; - if (firstShape.textFrame) { - firstShape.textFrame.load("textRange"); - await context.sync(); - - if (firstShape.textFrame.textRange) { - firstShape.textFrame.textRange.load([ - "font/name", - "font/size", - "font/bold", - "font/italic", - "font/underline", - "font/color" - ]); - await context.sync(); - hasText = true; - } - } - - // First, load all text frames in a batch - if (hasText) { - // Pre-load textFrame for all target shapes - for (let i = 1; i < shapes.items.length; i++) { - shapes.items[i].load("textFrame"); - } - await context.sync(); - - // For shapes that have textFrames, load their textRanges - for (let i = 1; i < shapes.items.length; i++) { - if (shapes.items[i].textFrame) { - shapes.items[i].textFrame.load("textRange"); - } - } - await context.sync(); - } - - // Now apply properties to all shapes - let successCount = 0; - let textStyleCount = 0; - - for (let i = 1; i < shapes.items.length; i++) { - try { - const targetShape = shapes.items[i]; - let propertiesApplied = false; - - // Apply line formatting properties - try { - // Line weight - if (firstShape.lineFormat.weight !== undefined) { - targetShape.lineFormat.weight = firstShape.lineFormat.weight; - } - - // Line style - if (firstShape.lineFormat.dashStyle !== undefined) { - targetShape.lineFormat.dashStyle = firstShape.lineFormat.dashStyle; - } - - // Line color - if (firstShape.lineFormat.color !== undefined) { - targetShape.lineFormat.color = firstShape.lineFormat.color; - } - - propertiesApplied = true; - } catch (err) { - console.error(`Error applying line format to shape ${i}:`, err); - } - - // Apply fill properties - try { - // Fill transparency - if (firstShape.fill.transparency !== undefined) { - targetShape.fill.transparency = firstShape.fill.transparency; - } - - if (firstShape.fill.foregroundColor !== undefined) { - targetShape.fill.foregroundColor = firstShape.fill.foregroundColor; - } - - propertiesApplied = true; - } catch (err) { - console.error(`Error applying fill format to shape ${i}:`, err); - } - - // Apply text properties if the source has text - if (hasText && targetShape.textFrame && targetShape.textFrame.textRange) { - try { - const sourceFont = firstShape.textFrame.textRange.font; - const targetFont = targetShape.textFrame.textRange.font; - - // Apply font properties in batch - if (sourceFont.name !== undefined) { - targetFont.name = sourceFont.name; - } - - if (sourceFont.size !== undefined) { - targetFont.size = sourceFont.size; - } - - if (sourceFont.bold !== undefined) { - targetFont.bold = sourceFont.bold; - } - - if (sourceFont.italic !== undefined) { - targetFont.italic = sourceFont.italic; - } - - if (sourceFont.underline !== undefined) { - targetFont.underline = sourceFont.underline; - } - - if (sourceFont.color !== undefined) { - targetFont.color = sourceFont.color; - } - - textStyleCount++; - } catch (err) { - console.error(`Error applying text format to shape ${i}:`, err); - } - } - - if (propertiesApplied) { - successCount++; - } - - } catch (err) { - console.error(`Error updating shape ${i}:`, err); - } - } - - // Single sync after all property changes - await context.sync(); - - // Final status message based on what was applied - if (successCount > 0) { - let message = `Applied properties to ${successCount} shapes`; - if (textStyleCount > 0) { - message += ` (including text styling on ${textStyleCount})`; - } - setStatusMessage(message); - setStatusType("success"); - } else { - setStatusMessage("Couldn't apply properties. Try selecting different shapes."); - setStatusType("error"); - } - - // Timeout is handled in App.tsx now - }); - } catch (error) { - setStatusMessage(`Error: ${error.message}`); - setStatusType("error"); - console.error("Main error:", error); - } finally { - setIsProcessing(false); + /** + * Validates that multiple shapes are selected + * @param shapes The collection of selected shapes + * @returns True if validation passes, false otherwise + */ + const validateShapeSelection = (shapes: PowerPoint.ShapeScopedCollection): boolean => { + if (shapes.items.length === 0) { + setStatusMessage("No shapes are selected. Please select shapes first."); + setStatusType("warning"); + return false; } + + if (shapes.items.length === 1) { + setStatusMessage("Please select multiple shapes to copy properties."); + setStatusType("warning"); + return false; + } + + return true; + }; + + /** + * Loads all necessary properties from the source shape + * @param sourceShape The shape to copy properties from + * @param context The PowerPoint request context + * @returns Object containing loaded property information + */ + const loadSourceProperties = async ( + sourceShape: PowerPoint.Shape, + context: PowerPoint.RequestContext + ): Promise<{ hasText: boolean }> => { + // Load basic shape properties + const propertiesToLoad = [ + ...SHAPE_PROPERTIES.line, + ...SHAPE_PROPERTIES.fill, + ...SHAPE_PROPERTIES.text + ]; + + sourceShape.load(propertiesToLoad); + await context.sync(); + + // Check if the shape has text and load text properties if it does + let hasText = false; + if (sourceShape.textFrame) { + sourceShape.textFrame.load("textRange"); + await context.sync(); + + if (sourceShape.textFrame.textRange) { + sourceShape.textFrame.textRange.load(FONT_PROPERTIES); + await context.sync(); + hasText = true; + } + } + + return { hasText }; + }; + + /** + * Preloads text properties for all target shapes in batches + * @param shapes The collection of shapes + * @param hasText Whether the source shape has text + * @param context The PowerPoint request context + */ + const preloadTargetTextProperties = async ( + shapes: PowerPoint.ShapeScopedCollection, + hasText: boolean, + context: PowerPoint.RequestContext + ): Promise => { + if (!hasText) return; + + // Batch 1: Load textFrame for all target shapes + for (let i = 1; i < shapes.items.length; i++) { + shapes.items[i].load("textFrame"); + } + await context.sync(); + + // Batch 2: For shapes that have textFrames, load their textRanges + for (let i = 1; i < shapes.items.length; i++) { + if (shapes.items[i].textFrame) { + shapes.items[i].textFrame.load("textRange"); + } + } + await context.sync(); + }; + + /** + * Applies line properties from source to target shape + * @param sourceShape The shape to copy properties from + * @param targetShape The shape to apply properties to + * @returns True if any properties were applied + */ + const applyLineProperties = ( + sourceShape: PowerPoint.Shape, + targetShape: PowerPoint.Shape + ): boolean => { + let propertiesApplied = false; + + try { + // Line weight + if (sourceShape.lineFormat.weight !== undefined) { + targetShape.lineFormat.weight = sourceShape.lineFormat.weight; + propertiesApplied = true; + } + + // Line style + if (sourceShape.lineFormat.dashStyle !== undefined) { + targetShape.lineFormat.dashStyle = sourceShape.lineFormat.dashStyle; + propertiesApplied = true; + } + + // Line color + if (sourceShape.lineFormat.color !== undefined) { + targetShape.lineFormat.color = sourceShape.lineFormat.color; + propertiesApplied = true; + } + } catch (err) { + console.error("Error applying line format:", getErrorMessage(err)); + } + + return propertiesApplied; + }; + + /** + * Applies fill properties from source to target shape + * @param sourceShape The shape to copy properties from + * @param targetShape The shape to apply properties to + * @returns True if any properties were applied + */ + const applyFillProperties = ( + sourceShape: PowerPoint.Shape, + targetShape: PowerPoint.Shape + ): boolean => { + let propertiesApplied = false; + + try { + // Fill transparency + if (sourceShape.fill.transparency !== undefined) { + targetShape.fill.transparency = sourceShape.fill.transparency; + propertiesApplied = true; + } + + // Fill color + if (sourceShape.fill.foregroundColor !== undefined) { + targetShape.fill.foregroundColor = sourceShape.fill.foregroundColor; + propertiesApplied = true; + } + } catch (err) { + console.error("Error applying fill format:", getErrorMessage(err)); + } + + return propertiesApplied; + }; + + + /** + * Applies text properties from source to target shape + * @param sourceShape The shape to copy properties from + * @param targetShape The shape to apply properties to + * @returns True if text properties were applied + */ + const applyTextProperties = ( + sourceShape: PowerPoint.Shape, + targetShape: PowerPoint.Shape + ): boolean => { + try { + if (sourceShape.textFrame?.textRange && targetShape.textFrame?.textRange) { + const sourceFont = sourceShape.textFrame.textRange.font; + const targetFont = targetShape.textFrame.textRange.font; + + // Apply font properties in batch + if (sourceFont.name !== undefined) { + targetFont.name = sourceFont.name; + } + + if (sourceFont.size !== undefined) { + targetFont.size = sourceFont.size; + } + + if (sourceFont.bold !== undefined) { + targetFont.bold = sourceFont.bold; + } + + if (sourceFont.italic !== undefined) { + targetFont.italic = sourceFont.italic; + } + + if (sourceFont.underline !== undefined) { + targetFont.underline = sourceFont.underline; + } + + if (sourceFont.color !== undefined) { + targetFont.color = sourceFont.color; + } + + return true; + } + } catch (err) { + console.error("Error applying text format:", getErrorMessage(err)); + } + + return false; + }; + + /** + * Main function to match properties from the first selected shape to others + */ + const matchPropertiesToFirstSelected = async (): Promise => { + await PowerPoint.run(async (context) => { + // Get the selected shapes + const shapes = context.presentation.getSelectedShapes(); + shapes.load("items"); + await context.sync(); + + // Validate shape selection + if (!validateShapeSelection(shapes)) { + return; + } + + // Get the first shape to use as template + const sourceShape = shapes.items[0]; + + // Load all necessary properties from the source shape + const { hasText } = await loadSourceProperties(sourceShape, context); + + // Preload text properties for target shapes if needed + await preloadTargetTextProperties(shapes, hasText, context); + + // Apply properties to all target shapes + let successCount = 0; + let textStyleCount = 0; + + for (let i = 1; i < shapes.items.length; i++) { + const targetShape = shapes.items[i]; + let shapeUpdated = false; + + // Apply different property types + const lineApplied = applyLineProperties(sourceShape, targetShape); + const fillApplied = applyFillProperties(sourceShape, targetShape); + + // Apply text properties if available + if (hasText) { + const textApplied = applyTextProperties(sourceShape, targetShape); + if (textApplied) { + textStyleCount++; + } + } + + // Count successful shape updates + if (lineApplied || fillApplied) { + shapeUpdated = true; + } + + if (shapeUpdated) { + successCount++; + } + } + + // Single sync after all property changes + await context.sync(); + + // Generate appropriate status message + if (successCount > 0) { + const propertyTypes = []; + + // Build a detailed status message + if (sourceShape.lineFormat) propertyTypes.push("line"); + if (sourceShape.fill) propertyTypes.push("fill"); + + let message = `Applied ${propertyTypes.join(", ")} properties to ${successCount} shapes`; + if (textStyleCount > 0) { + message += ` (including text styling on ${textStyleCount})`; + } + + setStatusMessage(message); + setStatusType("success"); + } else { + setStatusMessage("Couldn't apply properties. Try selecting different shapes."); + setStatusType("error"); + } + }); }; return (
- + onClick={matchPropertiesToFirstSelected} + />
); }; -export default MatchProperties; \ No newline at end of file +export default MatchProperties;