From 044bd9ebece3f330730dd0011bb4555eb40864fb Mon Sep 17 00:00:00 2001 From: Heiko Joerg Schick Date: Thu, 13 Mar 2025 00:30:31 +0100 Subject: [PATCH] Initial commit of grid line function --- README.md | 5 + docs/USER_GUIDE.md | 34 ++ src/taskpane/components/App.tsx | 9 + .../components/GridGuidelineManager.tsx | 561 ++++++++++++++++++ 4 files changed, 609 insertions(+) create mode 100644 src/taskpane/components/GridGuidelineManager.tsx diff --git a/README.md b/README.md index 23709803..5cc77d91 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,11 @@ A productivity toolkit that enhances PowerPoint with specialized formatting and - **Horizontal Alignment**: Align selected objects to the left, center, or right of the slide. - **Vertical Alignment**: Align selected objects to the top, middle, or bottom of the slide. +### Grid & Guidelines Tools + +- **Custom Grid**: Create a customizable grid with adjustable spacing, opacity, and color. +- **Guidelines**: Add horizontal or vertical guidelines at specific positions with customizable colors. + ## Installation ### For End Users diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index c3878048..8dc1a403 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -134,6 +134,40 @@ These tools align selected objects horizontally or vertically on the slide. - **Middle** - Centers all shapes vertically - **Bottom** - Aligns all shapes to the bottom edge +## Grid & Guidelines Tools + +These tools help you align and position elements precisely on slides using grids and guidelines. + +### Custom Grid + +Creates a customizable grid overlay on the current slide to help with precise positioning and alignment. + +**How to use:** +1. Navigate to the slide where you want to add a grid +2. In the Grid & Guidelines section, configure the grid options: + - **Spacing** - Set the distance between grid lines (in points) + - **Opacity** - Adjust how visible the grid appears + - **Color** - Choose from blue, red, yellow, or green +3. Click "Create Grid" to add the grid to the current slide +4. Click "Remove Grid" to clear the grid from the slide + +**Note:** The grid appears only on the current slide and is for visual reference only - it won't appear in presentation mode or exports. + +### Guidelines + +Add precise horizontal or vertical guidelines at specific positions on your slide. + +**How to use:** +1. Navigate to the slide where you want to add guidelines +2. In the Grid & Guidelines section: + - **Position** - Set the position of the guideline (in points from top/left) + - **Type** - Choose horizontal or vertical + - **Color** - Select a color for the guideline +3. Click "Add Guideline" to add the specified guideline +4. Click "Remove All" to clear all guidelines from the slide + +**Tip:** Use different colors for different types of guidelines (e.g., blue for margins, red for key alignment points). + ## Tips and Best Practices ### Selection Order Matters diff --git a/src/taskpane/components/App.tsx b/src/taskpane/components/App.tsx index 9e401f9f..ecae7403 100644 --- a/src/taskpane/components/App.tsx +++ b/src/taskpane/components/App.tsx @@ -9,6 +9,7 @@ import ConfidentialButtons from "./ConfidentialButtons"; import DraftButtons from "./DraftButtons"; import ProgressBarButtons from "./ProgressBarButtons"; import AlignmentButtons from "./AlignmentButtons"; +import GridGuidelineManager from "./GridGuidelineManager"; import { makeStyles, Text, Subtitle1, tokens, Theme, Spinner } from "@fluentui/react-components"; import { ShapeUnionRegular, SquareRegular, InfoRegular } from "@fluentui/react-icons"; @@ -46,6 +47,7 @@ const useStyles = makeStyles({ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif", backgroundColor: tokens.colorNeutralBackground1, overflow: "auto", + maxHeight: "100vh", }, statusContainer: { marginTop: "4px", @@ -194,6 +196,13 @@ const App: React.FC = () => { +
+ + Grid & Guidelines + + +
+ {/* Status message area at the bottom */} {isProcessing && (
diff --git a/src/taskpane/components/GridGuidelineManager.tsx b/src/taskpane/components/GridGuidelineManager.tsx new file mode 100644 index 00000000..909bfc21 --- /dev/null +++ b/src/taskpane/components/GridGuidelineManager.tsx @@ -0,0 +1,561 @@ +import * as React from "react"; +import { + Button, + makeStyles, + Label, + Slider, + SpinButton, + Divider, + ToggleButton, + tokens, + Input +} from "@fluentui/react-components"; +import { + GridRegular, + DismissRegular, + AddRegular, + LineHorizontal3Regular, + SplitVerticalRegular, + GridDotsRegular, + ArrowResetRegular +} from "@fluentui/react-icons"; +import { useStatusContext } from "./App"; +import { useCommonStyles } from "./commonStyles"; + +const useStyles = makeStyles({ + buttonGrid: { + display: "grid", + gridTemplateColumns: "repeat(2, 1fr)", + gap: "8px", + width: "100%", + marginBottom: "8px" + }, + gridButton: { + width: "100%", + minWidth: 0, + padding: "8px 4px", + "& span": { + justifyContent: "center" + } + }, + controlRow: { + display: "flex", + alignItems: "center", + gap: "8px", + marginBottom: "8px" + }, + controlLabel: { + minWidth: "80px", + marginBottom: "0" + }, + controlInput: { + flex: 1 + }, + guidesContainer: { + marginTop: "16px" + }, + guideButtonGrid: { + display: "grid", + gridTemplateColumns: "repeat(2, 1fr)", + gap: "8px", + width: "100%", + marginTop: "8px" + }, + colorButton: { + width: "24px", + height: "24px", + minWidth: "24px", + padding: "0", + border: "1px solid " + tokens.colorNeutralStroke1, + marginLeft: "8px" + }, + colorButtonSelected: { + border: "2px solid " + tokens.colorBrandForeground1 + } +}); + +type GridColor = "blue" | "red" | "yellow" | "green"; + +const GRID_PREFIX = "edison_grid_"; +const GUIDE_PREFIX = "edison_guide_"; + +const colorValues = { + blue: "#4472C4", + red: "#C00000", + yellow: "#FFC000", + green: "#70AD47" +}; + +export const GridGuidelineManager: React.FC = () => { + const styles = useStyles(); + const commonStyles = useCommonStyles(); + const { + statusMessage, setStatusMessage, + statusType, setStatusType, + isProcessing, setIsProcessing + } = useStatusContext(); + + // Grid state + const [gridSpacing, setGridSpacing] = React.useState(50); + const [gridColor, setGridColor] = React.useState("blue"); + const [gridOpacity, setGridOpacity] = React.useState(30); + + // Guidelines state + const [guidePosition, setGuidePosition] = React.useState(240); + const [guideDirection, setGuideDirection] = React.useState<"horizontal" | "vertical">("horizontal"); + const [guideColor, setGuideColor] = React.useState("red"); + + // Helper function to create a line shape + const createLineShape = ( + slide: PowerPoint.Slide, + startX: number, + startY: number, + endX: number, + endY: number + ) => { + // Create a line using alternative approach with a rectangle shape + const line = slide.shapes.addGeometricShape("Rectangle"); + + if (startX === endX) { + // Vertical line + line.left = startX; + line.top = startY; + line.height = endY - startY; + line.width = 1; // Make it 1pt wide + } else { + // Horizontal line + line.left = startX; + line.top = startY; + line.width = endX - startX; + line.height = 1; // Make it 1pt tall + } + + return line; + }; + + const createGrid = async () => { + setIsProcessing(true); + try { + await PowerPoint.run(async (context) => { + // Remove any existing grid first + await removeGrid(false); + + // Get the current slide using the selected slides collection + const selectedSlides = context.presentation.getSelectedSlides(); + selectedSlides.load("items"); + await context.sync(); + + if (selectedSlides.items.length === 0) { + throw new Error("No slide selected"); + } + + const currentSlide = selectedSlides.items[0]; + + // Standard dimensions for PowerPoint slides + const slideWidth = 960; // Width in points + const slideHeight = 540; // Height in points + + // Calculate number of lines needed + const numHorizontalLines = Math.floor(slideHeight / gridSpacing); + const numVerticalLines = Math.floor(slideWidth / gridSpacing); + + // Create horizontal grid lines + for (let i = 1; i <= numHorizontalLines; i++) { + const yPosition = i * gridSpacing; + + // Skip if we're at the edge of the slide + if (yPosition >= slideHeight) continue; + + // Create horizontal line + const line = createLineShape(currentSlide, 0, yPosition, slideWidth, yPosition); + line.name = `${GRID_PREFIX}h_${i}`; + + // Set line properties + line.fill.setSolidColor(colorValues[gridColor]); + line.lineFormat.weight = 0.75; // Thin line + line.lineFormat.visible = false; // No outline, just fill + + // Set transparency (opacity is inverse of transparency) + line.fill.transparency = (100 - gridOpacity) / 100; + } + + // Create vertical grid lines + for (let i = 1; i <= numVerticalLines; i++) { + const xPosition = i * gridSpacing; + + // Skip if we're at the edge of the slide + if (xPosition >= slideWidth) continue; + + // Create vertical line + const line = createLineShape(currentSlide, xPosition, 0, xPosition, slideHeight); + line.name = `${GRID_PREFIX}v_${i}`; + + // Set line properties + line.fill.setSolidColor(colorValues[gridColor]); + line.lineFormat.weight = 0.75; // Thin line + line.lineFormat.visible = false; // No outline, just fill + + // Set transparency (opacity is inverse of transparency) + line.fill.transparency = (100 - gridOpacity) / 100; + } + + setStatusMessage("Grid created on current slide"); + setStatusType("success"); + }); + } catch (error) { + setStatusMessage(`Error: ${error.message}`); + setStatusType("error"); + console.error("Create grid error:", error); + } finally { + setIsProcessing(false); + } + }; + + const removeGrid = async (showStatus = true) => { + if (showStatus) { + setIsProcessing(true); + } + + try { + await PowerPoint.run(async (context) => { + // Get the current slide using the selected slides collection + const selectedSlides = context.presentation.getSelectedSlides(); + selectedSlides.load("items"); + await context.sync(); + + if (selectedSlides.items.length === 0) { + throw new Error("No slide selected"); + } + + const currentSlide = selectedSlides.items[0]; + + // Load all shapes on this slide + const shapes = currentSlide.shapes; + shapes.load("items"); + await context.sync(); + + // Find shapes that are part of our grid + let removedCount = 0; + for (let i = 0; i < shapes.items.length; i++) { + const shape = shapes.items[i]; + shape.load("name"); + await context.sync(); + + // Check if this shape is part of our grid + if (shape.name && shape.name.startsWith(GRID_PREFIX)) { + shape.delete(); + removedCount++; + } + } + + await context.sync(); + + if (showStatus) { + if (removedCount > 0) { + setStatusMessage(`Removed grid from slide`); + } else { + setStatusMessage("No grid found to remove"); + } + setStatusType("success"); + } + }); + } catch (error) { + if (showStatus) { + setStatusMessage(`Error: ${error.message}`); + setStatusType("error"); + } + console.error("Remove grid error:", error); + } finally { + if (showStatus) { + setIsProcessing(false); + } + } + }; + + const addGuideline = async () => { + setIsProcessing(true); + try { + await PowerPoint.run(async (context) => { + // Get the current slide using the selected slides collection + const selectedSlides = context.presentation.getSelectedSlides(); + selectedSlides.load("items"); + await context.sync(); + + if (selectedSlides.items.length === 0) { + throw new Error("No slide selected"); + } + + const currentSlide = selectedSlides.items[0]; + + // Standard dimensions for PowerPoint slides + const slideWidth = 960; // Width in points + const slideHeight = 540; // Height in points + + // Create a unique identifier for this guide + const timestamp = new Date().getTime(); + const guideId = `${GUIDE_PREFIX}${guideDirection}_${timestamp}`; + + // Add the guide line + let line; + if (guideDirection === "horizontal") { + // Create horizontal guideline + line = createLineShape(currentSlide, 0, guidePosition, slideWidth, guidePosition); + } else { + // Create vertical guideline + line = createLineShape(currentSlide, guidePosition, 0, guidePosition, slideHeight); + } + + line.name = guideId; + + // Set line properties + line.fill.setSolidColor(colorValues[guideColor]); + line.lineFormat.visible = false; // No outline, just fill + + // We want solid lines for guidelines + line.fill.transparency = 0; + + setStatusMessage(`Added ${guideDirection} guideline`); + setStatusType("success"); + }); + } catch (error) { + setStatusMessage(`Error: ${error.message}`); + setStatusType("error"); + console.error("Add guideline error:", error); + } finally { + setIsProcessing(false); + } + }; + + const removeGuidelines = async () => { + setIsProcessing(true); + try { + await PowerPoint.run(async (context) => { + // Get the current slide using the selected slides collection + const selectedSlides = context.presentation.getSelectedSlides(); + selectedSlides.load("items"); + await context.sync(); + + if (selectedSlides.items.length === 0) { + throw new Error("No slide selected"); + } + + const currentSlide = selectedSlides.items[0]; + + // Load all shapes on this slide + const shapes = currentSlide.shapes; + shapes.load("items"); + await context.sync(); + + // Find shapes that are guidelines + let removedCount = 0; + for (let i = 0; i < shapes.items.length; i++) { + const shape = shapes.items[i]; + shape.load("name"); + await context.sync(); + + // Check if this shape is a guideline + if (shape.name && shape.name.startsWith(GUIDE_PREFIX)) { + shape.delete(); + removedCount++; + } + } + + await context.sync(); + + if (removedCount > 0) { + setStatusMessage(`Removed ${removedCount} guidelines`); + } else { + setStatusMessage("No guidelines found to remove"); + } + setStatusType("success"); + }); + } catch (error) { + setStatusMessage(`Error: ${error.message}`); + setStatusType("error"); + console.error("Remove guidelines error:", error); + } finally { + setIsProcessing(false); + } + }; + + // Color picker button component + const ColorButton = ({ color, selectedColor, onClick }: + { color: GridColor, selectedColor: GridColor, onClick: (color: GridColor) => void }) => { + return ( + + +
+ +
+ + { + const value = parseInt(data.value); + if (!isNaN(value) && value >= 10 && value <= 200) { + setGridSpacing(value); + } + }} + /> +
+ +
+ +
+ { + const value = parseInt(data.value); + if (!isNaN(value) && value >= 10 && value <= 100) { + setGridOpacity(value); + } + }} + /> + setGridOpacity(data.value)} + /> +
+
+ +
+ +
+ + + + +
+
+ + {/* Guidelines Section */} +
+ + + +
+ + { + const value = parseInt(data.value); + const maxVal = guideDirection === "horizontal" ? 540 : 960; + if (!isNaN(value) && value >= 0 && value <= maxVal) { + setGuidePosition(value); + } + }} + /> +
+ +
+ +
+ setGuideDirection("horizontal")} + icon={} + style={{ minWidth: "40px", padding: "2px 8px" }} + > + Horizontal + + setGuideDirection("vertical")} + icon={} + style={{ minWidth: "40px", padding: "2px 8px" }} + > + Vertical + +
+
+ +
+ +
+ + + + +
+
+ +
+ + +
+
+ + ); +}; + +export default GridGuidelineManager; \ No newline at end of file