From 263a4a49347db1c42b20035752b51bd865778cbe Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 22 Jun 2023 12:41:53 -0700 Subject: [PATCH 1/2] Avoid NULL in String Function Call (#3617) The overall problem is described in issue #3613. This PR represents only a partial solution. Very complicated formulas are resulting in calls to string functions using null arguments, which is deprecated in recent Php releases. In fact, there are many such deprecations for the spreadsheet in question. Eliminating the deprecations is easy. However, the result of the calculation is, for many cells, 0 rather than what Excel determines it should be. This can be overlooked to a certain extent, because Excel will recalculate when the spreadsheet is opened, so loading and saving the spreadsheet in question will result in a spreadsheet which looks okay when it is opened in Excel. Resolving the incorrect calculation in PhpSpreadsheet would be nice, so I'm leaving the issue open, but that looks too complicated for me to get a toehold. --- src/PhpSpreadsheet/Calculation/Functions.php | 2 +- .../Reader/Xlsx/Issue3613Test.php | 25 ++++++++++++++++++ tests/data/Reader/XLSX/issue.3613.xlsx | Bin 0 -> 52578 bytes 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3613Test.php create mode 100644 tests/data/Reader/XLSX/issue.3613.xlsx diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 0ec396f6e3..a645d790a4 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -153,7 +153,7 @@ public static function ifCondition($condition): string { $condition = self::flattenSingleValue($condition); - if ($condition === '') { + if ($condition === '' || $condition === null) { return '=""'; } if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='], true)) { diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3613Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3613Test.php new file mode 100644 index 0000000000..71899039e4 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3613Test.php @@ -0,0 +1,25 @@ +load(self::$testbook); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('=ROUND(MAX((O4-P4)*{0.03;0.1;0.2;0.25;0.3;0.35;0.45}-{0;2520;16920;31920;52920;85920;181920},0)-Q4,2)', $sheet->getCell('N4')->getValue()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.3613.xlsx b/tests/data/Reader/XLSX/issue.3613.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4b1296fee7fa8e7f27639ecb7a250fbf7ea95ab7 GIT binary patch literal 52578 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 literal 0 HcmV?d00001 From 616b9de3f02c9b43ac4d20d86d3b8aea77f86d8c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 28 Jun 2023 07:13:20 -0700 Subject: [PATCH 2/2] Split Screens (#3622) * Split Screens Fix #3601. Split screens are a feature that affects the display of the spreadsheet to the end user; they do not affect the data. They are conceptually similar to "freeze panes". The differences are explained in the issue. As will be explained, support is fairly full for Xlsx, and less full for Xml; no attempt is yet made to support Xls or Ods. For freeze or split, the window can be divided into 2 horizontal panes, or 2 vertical panes, or 4 horizontal+vertical panes. In Excel, you can split or freeze on cell A1, which causes 4 panes centered at the middle of the screen. PhpSpreadsheet will not duplicate that functionality, and code is added to ignore an attempt to freeze at A1. This breaks one existing nonsensical test, which is changed to something sensible for this PR. In the spreadsheet xml, both 'freeze' and 'split' use attributes 'xSplit' and 'ySplit' to indicate the position. Unfortunately, the attributes have different meanings for 'freeze' (*columns* from top/left) than for 'split' (*distance* from top/left). For that reason, it is difficult to change between 'freeze' and 'split', so PhpSpreadsheet will not yet support doing so. There are 3 possible states when freeze/split is used - 'frozen', 'split', or 'frozenSplit'. All will be maintained during read/write, and the user can set them. In Excel, you get 'frozenSplit' by first splitting, then by changing to freezing. In that case, when you unfreeze, you revert to split. PhpSpreadsheet will not duplicate that functionality - when you unfreeze, the panes will go away regardless of whether you were 'frozen' or 'frozenSplit'. Changing the selected cell will cause the 'active pane' to update when the panes are frozen. PhpSpreadsheet will not yet update the active pane when the panes are split. The programmer will, of course, have the opportunity to explicitly change the active pane in this circumstance. I have not yet been able to figure out how Xml spreadsheets map their panes, or how to determine selected cell for the sheet as a whole or for individual panes when freeze/split is in effect. The programmer will have the opportunity to specify these explicitly. However, loading an Xml spreadsheet in PhpSpreadsheet will not fill in these values. Each pane has its own selected cells, and you can navigate between these with the F6 key in Excel (this may work only with split but not with freeze, but Excel maintains the values for freeze and so will PhpSpreadsheet). For Xlsx, PhpSpreadsheet loads and saves these values. * Eliminate Some Dead Code Pointed out by Scrutinizer. --- src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php | 54 +++- src/PhpSpreadsheet/Reader/Xml.php | 39 ++- src/PhpSpreadsheet/Worksheet/Pane.php | 48 +++ src/PhpSpreadsheet/Worksheet/Validations.php | 2 +- src/PhpSpreadsheet/Worksheet/Worksheet.php | 172 ++++++++++- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 77 +++-- .../Functional/FreezePaneTest.php | 139 +++++++++ .../Reader/Xlsx/SplitsTest.php | 76 +++++ .../Reader/Xml/SplitsTest.php | 74 +++++ .../Worksheet/Worksheet2Test.php | 29 +- tests/data/Reader/XLSX/splits.xlsx | Bin 0 -> 13543 bytes tests/data/Reader/Xml/splits.xml | 288 ++++++++++++++++++ 12 files changed, 949 insertions(+), 49 deletions(-) create mode 100644 src/PhpSpreadsheet/Worksheet/Pane.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/SplitsTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xml/SplitsTest.php create mode 100644 tests/data/Reader/XLSX/splits.xlsx create mode 100644 tests/data/Reader/Xml/splits.xml diff --git a/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php b/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php index b2bc99f052..50467fe745 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php @@ -4,6 +4,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; +use PhpOffice\PhpSpreadsheet\Worksheet\Pane; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; @@ -18,6 +19,8 @@ class SheetViews extends BaseParserClass /** @var Worksheet */ private $worksheet; + private string $activePane = ''; + public function __construct(SimpleXMLElement $sheetViewXml, Worksheet $workSheet) { $this->sheetViewXml = $sheetViewXml; @@ -35,11 +38,15 @@ public function load(): void $this->direction(); $this->showZeros(); + $usesPanes = false; if (isset($this->sheetViewXml->pane)) { $this->pane(); + $usesPanes = true; } - if (isset($this->sheetViewXml->selection, $this->sheetViewXml->selection->attributes()->sqref)) { - $this->selection(); + if (isset($this->sheetViewXml->selection)) { + foreach ($this->sheetViewXml->selection as $selection) { + $this->selection($selection, $usesPanes); + } } } @@ -127,30 +134,55 @@ private function pane(): void if (isset($paneAttributes->xSplit)) { $xSplit = (int) ($paneAttributes->xSplit); + $this->worksheet->setXSplit($xSplit); } if (isset($paneAttributes->ySplit)) { $ySplit = (int) ($paneAttributes->ySplit); + $this->worksheet->setYSplit($ySplit); } - + $paneState = isset($paneAttributes->state) ? ((string) $paneAttributes->state) : ''; + $this->worksheet->setPaneState($paneState); if (isset($paneAttributes->topLeftCell)) { $topLeftCell = (string) $paneAttributes->topLeftCell; + $this->worksheet->setPaneTopLeftCell($topLeftCell); + if ($paneState === Worksheet::PANE_FROZEN) { + $this->worksheet->setTopLeftCell($topLeftCell); + } + } + $activePane = isset($paneAttributes->activePane) ? ((string) $paneAttributes->activePane) : 'topLeft'; + $this->worksheet->setActivePane($activePane); + $this->activePane = $activePane; + if ($paneState === Worksheet::PANE_FROZEN || $paneState === Worksheet::PANE_FROZENSPLIT) { + $this->worksheet->freezePane( + Coordinate::stringFromColumnIndex($xSplit + 1) . ($ySplit + 1), + $topLeftCell, + $paneState === Worksheet::PANE_FROZENSPLIT + ); } - - $this->worksheet->freezePane( - Coordinate::stringFromColumnIndex($xSplit + 1) . ($ySplit + 1), - $topLeftCell - ); } - private function selection(): void + private function selection(?SimpleXMLElement $selection, bool $usesPanes): void { - $attributes = $this->sheetViewXml->selection->attributes(); + $attributes = ($selection === null) ? null : $selection->attributes(); if ($attributes !== null) { + $position = (string) $attributes->pane; + if ($usesPanes && $position === '') { + $position = 'topLeft'; + } + $activeCell = (string) $attributes->activeCell; $sqref = (string) $attributes->sqref; $sqref = explode(' ', $sqref); $sqref = $sqref[0]; - $this->worksheet->setSelectedCells($sqref); + if ($position === '') { + $this->worksheet->setSelectedCells($sqref); + } else { + $pane = new Pane($position, $sqref, $activeCell); + $this->worksheet->setPane($position, $pane); + if ($position === $this->activePane && $sqref !== '') { + $this->worksheet->setSelectedCells($sqref); + } + } } } } diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 6be26fc2c6..a1ad0c0ab9 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -525,7 +525,44 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo if (isset($xmlX->WorksheetOptions->SplitVertical)) { $freezeColumn = (int) $xmlX->WorksheetOptions->SplitVertical + 1; } - $spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow); + $leftTopRow = (string) $xmlX->WorksheetOptions->TopRowBottomPane; + $leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnRightPane; + if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) { + $leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1); + $spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow, $leftTopCoordinate, !isset($xmlX->WorksheetOptions->FrozenNoSplit)); + } else { + $spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow, null, !isset($xmlX->WorksheetOptions->FrozenNoSplit)); + } + } elseif (isset($xmlX->WorksheetOptions->SplitVertical) || isset($xmlX->WorksheetOptions->SplitHorizontal)) { + if (isset($xmlX->WorksheetOptions->SplitHorizontal)) { + $ySplit = (int) $xmlX->WorksheetOptions->SplitHorizontal; + $spreadsheet->getActiveSheet()->setYSplit($ySplit); + } + if (isset($xmlX->WorksheetOptions->SplitVertical)) { + $xSplit = (int) $xmlX->WorksheetOptions->SplitVertical; + $spreadsheet->getActiveSheet()->setXSplit($xSplit); + } + if (isset($xmlX->WorksheetOptions->LeftColumnVisible) || isset($xmlX->WorksheetOptions->TopRowVisible)) { + $leftTopColumn = $leftTopRow = 1; + if (isset($xmlX->WorksheetOptions->LeftColumnVisible)) { + $leftTopColumn = 1 + (int) $xmlX->WorksheetOptions->LeftColumnVisible; + } + if (isset($xmlX->WorksheetOptions->TopRowVisible)) { + $leftTopRow = 1 + (int) $xmlX->WorksheetOptions->TopRowVisible; + } + $leftTopCoordinate = Coordinate::stringFromColumnIndex($leftTopColumn) . "$leftTopRow"; + $spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate); + } + + $leftTopColumn = $leftTopRow = 1; + if (isset($xmlX->WorksheetOptions->LeftColumnRightPane)) { + $leftTopColumn = 1 + (int) $xmlX->WorksheetOptions->LeftColumnRightPane; + } + if (isset($xmlX->WorksheetOptions->TopRowBottomPane)) { + $leftTopRow = 1 + (int) $xmlX->WorksheetOptions->TopRowBottomPane; + } + $leftTopCoordinate = Coordinate::stringFromColumnIndex($leftTopColumn) . "$leftTopRow"; + $spreadsheet->getActiveSheet()->setPaneTopLeftCell($leftTopCoordinate); } (new PageSettings($xmlX))->loadPageSettings($spreadsheet); if (isset($xmlX->WorksheetOptions->TopRowVisible, $xmlX->WorksheetOptions->LeftColumnVisible)) { diff --git a/src/PhpSpreadsheet/Worksheet/Pane.php b/src/PhpSpreadsheet/Worksheet/Pane.php new file mode 100644 index 0000000000..8244b1e63e --- /dev/null +++ b/src/PhpSpreadsheet/Worksheet/Pane.php @@ -0,0 +1,48 @@ +sqref = $sqref; + $this->activeCell = $activeCell; + $this->position = $position; + } + + public function getPosition(): string + { + return $this->position; + } + + public function getSqref(): string + { + return $this->sqref; + } + + public function setSqref(string $sqref): self + { + $this->sqref = $sqref; + + return $this; + } + + public function getActiveCell(): string + { + return $this->activeCell; + } + + public function setActiveCell(string $activeCell): self + { + $this->activeCell = $activeCell; + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php index 42ba566c6c..db4535d3a1 100644 --- a/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -73,7 +73,7 @@ public static function validateCellRange($cellRange): string $addressRange = (string) preg_replace( ['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'], [self::SETMAXROW, self::SETMAXCOL], - $addressRange + $addressRange ?? '' ); return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange); diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 29221e9910..9c836c63f3 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -240,6 +240,28 @@ class Worksheet implements IComparable */ private $topLeftCell; + private string $paneTopLeftCell = ''; + + private string $activePane = ''; + + private int $xSplit = 0; + + private int $ySplit = 0; + + private string $paneState = ''; + + /** + * Properties of the 4 panes. + * + * @var (null|Pane)[] + */ + private $panes = [ + 'bottomRight' => null, + 'bottomLeft' => null, + 'topRight' => null, + 'topLeft' => null, + ]; + /** * Show gridlines? * @@ -2416,8 +2438,14 @@ public function getFreezePane() * * @return $this */ - public function freezePane($coordinate, $topLeftCell = null) + public function freezePane($coordinate, $topLeftCell = null, bool $frozenSplit = false) { + $this->panes = [ + 'bottomRight' => null, + 'bottomLeft' => null, + 'topRight' => null, + 'topLeft' => null, + ]; $cellAddress = ($coordinate !== null) ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)) : null; @@ -2433,8 +2461,28 @@ public function freezePane($coordinate, $topLeftCell = null) $topLeftCell = $coordinate[0] . $coordinate[1]; } + $topLeftCell = "$topLeftCell"; + $this->paneTopLeftCell = $topLeftCell; + $this->freezePane = $cellAddress; $this->topLeftCell = $topLeftCell; + if ($cellAddress === null) { + $this->paneState = ''; + $this->xSplit = $this->ySplit = 0; + $this->activePane = ''; + } else { + $coordinates = Coordinate::indexesFromString($cellAddress); + $this->xSplit = $coordinates[0] - 1; + $this->ySplit = $coordinates[1] - 1; + if ($this->xSplit > 0 || $this->ySplit > 0) { + $this->paneState = $frozenSplit ? self::PANE_FROZENSPLIT : self::PANE_FROZEN; + $this->setSelectedCellsActivePane(); + } else { + $this->paneState = ''; + $this->freezePane = null; + $this->activePane = ''; + } + } return $this; } @@ -2484,6 +2532,108 @@ public function getTopLeftCell() return $this->topLeftCell; } + public function getPaneTopLeftCell(): string + { + return $this->paneTopLeftCell; + } + + public function setPaneTopLeftCell(string $paneTopLeftCell): self + { + $this->paneTopLeftCell = $paneTopLeftCell; + + return $this; + } + + public function usesPanes(): bool + { + return $this->xSplit > 0 || $this->ySplit > 0; + } + + public function getPane(string $position): ?Pane + { + return $this->panes[$position] ?? null; + } + + public function setPane(string $position, ?Pane $pane): self + { + if (array_key_exists($position, $this->panes)) { + $this->panes[$position] = $pane; + } + + return $this; + } + + /** @return (null|Pane)[] */ + public function getPanes() + { + return $this->panes; + } + + public function getActivePane(): string + { + return $this->activePane; + } + + public function setActivePane(string $activePane): self + { + $this->activePane = array_key_exists($activePane, $this->panes) ? $activePane : ''; + + return $this; + } + + public function getXSplit(): int + { + return $this->xSplit; + } + + public function setXSplit(int $xSplit): self + { + $this->xSplit = $xSplit; + if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) { + $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT); + } + + return $this; + } + + public function getYSplit(): int + { + return $this->ySplit; + } + + public function setYSplit(int $ySplit): self + { + $this->ySplit = $ySplit; + if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) { + $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT); + } + + return $this; + } + + public function getPaneState(): string + { + return $this->paneState; + } + + public const PANE_FROZEN = 'frozen'; + public const PANE_FROZENSPLIT = 'frozenSplit'; + public const PANE_SPLIT = 'split'; + private const VALIDPANESTATE = [self::PANE_FROZEN, self::PANE_SPLIT, self::PANE_FROZENSPLIT]; + private const VALIDFROZENSTATE = [self::PANE_FROZEN, self::PANE_FROZENSPLIT]; + + public function setPaneState(string $paneState): self + { + $this->paneState = in_array($paneState, self::VALIDPANESTATE, true) ? $paneState : ''; + if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) { + $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT); + } else { + $this->freezePane = null; + } + + return $this; + } + /** * Insert a new row, updating all possible related data. * @@ -2938,10 +3088,30 @@ public function setSelectedCells($coordinate) $this->activeCell = $coordinate; } $this->selectedCells = $coordinate; + $this->setSelectedCellsActivePane(); return $this; } + private function setSelectedCellsActivePane(): void + { + if (!empty($this->freezePane)) { + $coordinateC = Coordinate::indexesFromString($this->freezePane); + $coordinateT = Coordinate::indexesFromString($this->activeCell); + if ($coordinateC[0] === 1) { + $activePane = ($coordinateT[1] <= $coordinateC[1]) ? 'topLeft' : 'bottomLeft'; + } elseif ($coordinateC[1] === 1) { + $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight'; + } elseif ($coordinateT[1] <= $coordinateC[1]) { + $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight'; + } else { + $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'bottomLeft' : 'bottomRight'; + } + $this->setActivePane($activePane); + $this->panes[$activePane] = new Pane($activePane, $this->selectedCells, $this->activeCell); + } + } + /** * Selected cell by using numeric cell coordinates. * diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 5e453b3d86..19b1054f03 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -297,55 +297,68 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $ } $topLeftCell = $worksheet->getTopLeftCell(); + if (!empty($topLeftCell) && $worksheet->getPaneState() !== PhpspreadsheetWorksheet::PANE_FROZEN && $worksheet->getPaneState() !== PhpspreadsheetWorksheet::PANE_FROZENSPLIT) { + $objWriter->writeAttribute('topLeftCell', $topLeftCell); + } $activeCell = $worksheet->getActiveCell(); $sqref = $worksheet->getSelectedCells(); // Pane - $pane = ''; - if ($worksheet->getFreezePane()) { - [$xSplit, $ySplit] = Coordinate::coordinateFromString($worksheet->getFreezePane()); - $xSplit = Coordinate::columnIndexFromString($xSplit); - --$xSplit; - --$ySplit; - - // pane - $pane = 'topRight'; + if ($worksheet->usesPanes()) { $objWriter->startElement('pane'); + $xSplit = $worksheet->getXSplit(); + $ySplit = $worksheet->getYSplit(); + $pane = $worksheet->getActivePane(); + $paneTopLeftCell = $worksheet->getPaneTopLeftCell(); if ($xSplit > 0) { $objWriter->writeAttribute('xSplit', "$xSplit"); } if ($ySplit > 0) { - $objWriter->writeAttribute('ySplit', $ySplit); - $pane = ($xSplit > 0) ? 'bottomRight' : 'bottomLeft'; + $objWriter->writeAttribute('ySplit', "$ySplit"); } - self::writeAttributeNotNull($objWriter, 'topLeftCell', $topLeftCell); - $objWriter->writeAttribute('activePane', $pane); - $objWriter->writeAttribute('state', 'frozen'); - $objWriter->endElement(); - - if (($xSplit > 0) && ($ySplit > 0)) { - // Write additional selections if more than two panes (ie both an X and a Y split) - $objWriter->startElement('selection'); - $objWriter->writeAttribute('pane', 'topRight'); - $objWriter->endElement(); - $objWriter->startElement('selection'); - $objWriter->writeAttribute('pane', 'bottomLeft'); - $objWriter->endElement(); + if ($pane !== '') { + $objWriter->writeAttribute('activePane', $pane); + } + $paneState = $worksheet->getPaneState(); + if ($paneState !== '') { + $objWriter->writeAttribute('state', $paneState); + } + if ($paneTopLeftCell !== '') { + $objWriter->writeAttribute('topLeftCell', $paneTopLeftCell); + } + $objWriter->endElement(); // pane + + foreach ($worksheet->getPanes() as $panex) { + if ($panex !== null) { + $sqref = $activeCell = ''; + $objWriter->startElement('selection'); + $objWriter->writeAttribute('pane', $panex->getPosition()); + $activeCellPane = $panex->getActiveCell(); + if ($activeCellPane !== '') { + $objWriter->writeAttribute('activeCell', $activeCellPane); + } + $sqrefPane = $panex->getSqref(); + if ($sqrefPane !== '') { + $objWriter->writeAttribute('sqref', $sqrefPane); + } + $objWriter->endElement(); // selection + } } - } else { - self::writeAttributeNotNull($objWriter, 'topLeftCell', $topLeftCell); } // Selection // Only need to write selection element if we have a split pane // We cheat a little by over-riding the active cell selection, setting it to the split cell - $objWriter->startElement('selection'); - if ($pane != '') { - $objWriter->writeAttribute('pane', $pane); + if (!empty($sqref) || !empty($activeCell)) { + $objWriter->startElement('selection'); + if (!empty($activeCell)) { + $objWriter->writeAttribute('activeCell', $activeCell); + } + if (!empty($sqref)) { + $objWriter->writeAttribute('sqref', $sqref); + } + $objWriter->endElement(); // selection } - $objWriter->writeAttribute('activeCell', $activeCell); - $objWriter->writeAttribute('sqref', $sqref); - $objWriter->endElement(); $objWriter->endElement(); diff --git a/tests/PhpSpreadsheetTests/Functional/FreezePaneTest.php b/tests/PhpSpreadsheetTests/Functional/FreezePaneTest.php index ca387587b0..cee46bfa01 100644 --- a/tests/PhpSpreadsheetTests/Functional/FreezePaneTest.php +++ b/tests/PhpSpreadsheetTests/Functional/FreezePaneTest.php @@ -3,6 +3,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Functional; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Pane; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class FreezePaneTest extends AbstractFunctional { @@ -28,6 +30,7 @@ public function testFreezePane($format): void $spreadsheet->getActiveSheet()->freezePane($cellSplit, $topLeftCell); $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); + $spreadsheet->disconnectWorksheets(); // Read written file $reloadedActive = $reloadedSpreadsheet->getActiveSheet(); @@ -36,6 +39,7 @@ public function testFreezePane($format): void self::assertSame($cellSplit, $actualCellSplit, 'should be able to set freeze pane'); self::assertSame($topLeftCell, $actualTopLeftCell, 'should be able to set the top left cell'); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -55,6 +59,7 @@ public function testFreezePaneWithInvalidSelectedCells($format): void $worksheet->setSelectedCells('F5'); $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); + $spreadsheet->disconnectWorksheets(); // Read written file $reloadedActive = $reloadedSpreadsheet->getActiveSheet(); @@ -64,6 +69,7 @@ public function testFreezePaneWithInvalidSelectedCells($format): void self::assertSame($cellSplit, $actualCellSplit, 'should be able to set freeze pane'); self::assertSame($topLeftCell, $actualTopLeftCell, 'should be able to set the top left cell'); self::assertSame('F5', $reloadedActive->getSelectedCells()); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -88,11 +94,13 @@ public function testFreezePaneUserSelectedCell($format): void $worksheet->setSelectedCells('C3'); $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); + $spreadsheet->disconnectWorksheets(); // Read written file $reloadedActive = $reloadedSpreadsheet->getActiveSheet(); $expected = 'C3'; self::assertSame($expected, $reloadedActive->getSelectedCells()); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -117,10 +125,141 @@ public function testNoFreezePaneUserSelectedCell($format): void $worksheet->setSelectedCells('C3'); $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); + $spreadsheet->disconnectWorksheets(); // Read written file $reloadedActive = $reloadedSpreadsheet->getActiveSheet(); $expected = 'C3'; self::assertSame($expected, $reloadedActive->getSelectedCells()); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testFreezePaneWithSelectedCells(): void + { + $spreadsheet = new Spreadsheet(); + $cellSplit = 'C4'; + $sheet = $spreadsheet->getActiveSheet(); + $sheet->freezePane($cellSplit); + $sheet->setSelectedCells('A2'); + self::assertSame('topLeft', $sheet->getActivePane()); + $sheet->setSelectedCells('D3'); + self::assertSame('topRight', $sheet->getActivePane()); + $sheet->setSelectedCells('B5'); + self::assertSame('bottomLeft', $sheet->getActivePane()); + $sheet->setSelectedCells('F7'); + self::assertSame('bottomRight', $sheet->getActivePane()); + $expected = [ + 'topLeft' => new Pane('topLeft', 'A2', 'A2'), + 'topRight' => new Pane('topRight', 'D3', 'D3'), + 'bottomLeft' => new Pane('bottomLeft', 'B5', 'B5'), + 'bottomRight' => new Pane('bottomRight', 'F7', 'F7'), + ]; + self::assertEquals($expected, $sheet->getPanes()); + self::assertSame('F7', $sheet->getSelectedCells()); + + $sheet = $spreadsheet->createSheet(); + $cellSplit = 'A2'; + $sheet->freezePane($cellSplit); + $sheet->setSelectedCells('B1'); + self::assertSame('topLeft', $sheet->getActivePane()); + $sheet->setSelectedCells('C7'); + self::assertSame('bottomLeft', $sheet->getActivePane()); + $expected = [ + 'topLeft' => new Pane('topLeft', 'B1', 'B1'), + 'topRight' => null, + 'bottomLeft' => new Pane('bottomLeft', 'C7', 'C7'), + 'bottomRight' => null, + ]; + self::assertEquals($expected, $sheet->getPanes()); + self::assertSame('C7', $sheet->getSelectedCells()); + + $sheet = $spreadsheet->createSheet(); + $cellSplit = 'D1'; + $sheet->freezePane($cellSplit); + $sheet->setSelectedCells('B1'); + self::assertSame('topLeft', $sheet->getActivePane()); + $sheet->setSelectedCells('G3'); + self::assertSame('topRight', $sheet->getActivePane()); + $expected = [ + 'topRight' => new Pane('topRight', 'G3', 'G3'), + 'bottomRight' => null, + 'topLeft' => new Pane('topLeft', 'B1', 'B1'), + 'bottomLeft' => null, + ]; + self::assertEquals($expected, $sheet->getPanes()); + self::assertSame('G3', $sheet->getSelectedCells()); + + $sheet = $spreadsheet->createSheet(); + $sheet->setSelectedCells('D7'); + self::assertEmpty($sheet->getActivePane()); + $expected = [ + 'topRight' => null, + 'bottomRight' => null, + 'topLeft' => null, + 'bottomLeft' => null, + ]; + self::assertEquals($expected, $sheet->getPanes()); + self::assertSame('D7', $sheet->getSelectedCells()); + + $spreadsheet->disconnectWorksheets(); + } + + public function testFreezePaneViaPaneState(): void + { + $spreadsheet = new Spreadsheet(); + $cellSplit = ['C4', 2, 3]; + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setXSplit($cellSplit[1]) + ->setYSplit($cellSplit[2]) + ->setPaneState(Worksheet::PANE_SPLIT); + self::assertNull($sheet->getFreezePane()); + + $sheet = $spreadsheet->createSheet(); + $sheet->setXSplit($cellSplit[1]) + ->setYSplit($cellSplit[2]) + ->setPaneState(Worksheet::PANE_FROZEN); + self::assertSame($cellSplit[0], $sheet->getFreezePane()); + $sheet->setSelectedCells('A2'); + self::assertSame('topLeft', $sheet->getActivePane()); + $sheet->setSelectedCells('D3'); + self::assertSame('topRight', $sheet->getActivePane()); + $sheet->setSelectedCells('B5'); + self::assertSame('bottomLeft', $sheet->getActivePane()); + $sheet->setSelectedCells('F7'); + self::assertSame('bottomRight', $sheet->getActivePane()); + $expected = [ + 'topLeft' => new Pane('topLeft', 'A2', 'A2'), + 'topRight' => new Pane('topRight', 'D3', 'D3'), + 'bottomLeft' => new Pane('bottomLeft', 'B5', 'B5'), + 'bottomRight' => new Pane('bottomRight', 'F7', 'F7'), + ]; + self::assertEquals($expected, $sheet->getPanes()); + self::assertSame('F7', $sheet->getSelectedCells()); + + $cellSplit = ['B4', 1, 3]; + $sheet->setXSplit($cellSplit[1]); + self::assertSame($cellSplit[0], $sheet->getFreezePane()); + self::assertSame('F7', $sheet->getSelectedCells()); + $expected = [ + 'topLeft' => null, + 'topRight' => null, + 'bottomLeft' => null, + 'bottomRight' => new Pane('bottomRight', 'F7', 'F7'), + ]; + self::assertEquals($expected, $sheet->getPanes()); + + $cellSplit = ['B8', 1, 7]; + $sheet->setYSplit($cellSplit[2]); + self::assertSame($cellSplit[0], $sheet->getFreezePane()); + self::assertSame('F7', $sheet->getSelectedCells()); + $expected = [ + 'topLeft' => null, + 'bottomRight' => null, + 'bottomLeft' => null, + 'topRight' => new Pane('topRight', 'F7', 'F7'), + ]; + self::assertEquals($expected, $sheet->getPanes()); + + $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/SplitsTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/SplitsTest.php new file mode 100644 index 0000000000..424353a7a3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/SplitsTest.php @@ -0,0 +1,76 @@ +load(self::$testbook); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getSheetByNameOrThrow('Freeze'); + self::assertSame('E7', $sheet->getFreezePane()); + self::assertSame('frozen', $sheet->getPaneState()); + self::assertSame('L7', $sheet->getPaneTopLeftCell()); + self::assertSame('L7', $sheet->getTopLeftCell()); + self::assertSame('L7', $sheet->getSelectedCells()); + + $sheet = $reloadedSpreadsheet->getSheetByNameOrThrow('SplitVertical'); + self::assertNull($sheet->getFreezePane()); + self::assertSame('G1', $sheet->getTopLeftCell()); + self::assertSame('E1', $sheet->getPaneTopLeftCell()); + self::assertSame('E1', $sheet->getSelectedCells()); + self::assertNotEquals(0, $sheet->getXSplit()); + self::assertEquals(0, $sheet->getYSplit()); + self::assertNotNull($sheet->getPane('topRight')); + + $sheet = $reloadedSpreadsheet->getSheetByNameOrThrow('SplitHorizontal'); + self::assertNull($sheet->getFreezePane()); + self::assertSame('A3', $sheet->getTopLeftCell()); + self::assertSame('A6', $sheet->getPaneTopLeftCell()); + self::assertSame('A7', $sheet->getSelectedCells()); + self::assertEquals(0, $sheet->getXSplit()); + self::assertNotEquals(0, $sheet->getYSplit()); + self::assertNotNull($sheet->getPane('bottomLeft')); + + $sheet = $reloadedSpreadsheet->getSheetByNameOrThrow('SplitBoth'); + self::assertNull($sheet->getFreezePane()); + self::assertSame('H3', $sheet->getTopLeftCell()); + self::assertSame('E19', $sheet->getPaneTopLeftCell()); + self::assertSame('E20', $sheet->getSelectedCells()); + self::assertNotEquals(0, $sheet->getXSplit()); + self::assertNotEquals(0, $sheet->getYSplit()); + self::assertNotNull($sheet->getPane('bottomLeft')); + self::assertNotNull($sheet->getPane('bottomRight')); + self::assertNotNull($sheet->getPane('topRight')); + + $sheet = $reloadedSpreadsheet->getSheetByNameOrThrow('NoFreezeNorSplit'); + self::assertNull($sheet->getFreezePane()); + self::assertSame('D3', $sheet->getTopLeftCell()); + self::assertSame('', $sheet->getPaneTopLeftCell()); + self::assertSame('D5', $sheet->getSelectedCells()); + self::assertNull($sheet->getPane('bottomLeft')); + self::assertNull($sheet->getPane('bottomRight')); + self::assertNull($sheet->getPane('topRight')); + + $sheet = $reloadedSpreadsheet->getSheetByNameOrThrow('FrozenSplit'); + self::assertSame('B4', $sheet->getFreezePane()); + self::assertSame('frozenSplit', $sheet->getPaneState()); + self::assertSame('B4', $sheet->getPaneTopLeftCell()); + self::assertSame('B4', $sheet->getTopLeftCell()); + self::assertSame('B4', $sheet->getSelectedCells()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/SplitsTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/SplitsTest.php new file mode 100644 index 0000000000..0fd49ca916 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xml/SplitsTest.php @@ -0,0 +1,74 @@ +load(self::$testbook); + + $sheet = $spreadsheet->getSheetByNameOrThrow('Freeze'); + self::assertSame('frozen', $sheet->getPaneState()); + self::assertSame('E7', $sheet->getFreezePane()); + self::assertSame('L7', $sheet->getPaneTopLeftCell()); + self::assertSame('L7', $sheet->getTopLeftCell()); + //self::assertSame('L7', $sheet->getSelectedCells()); + + $sheet = $spreadsheet->getSheetByNameOrThrow('SplitVertical'); + self::assertNull($sheet->getFreezePane()); + self::assertSame('G1', $sheet->getTopLeftCell()); + self::assertSame('E1', $sheet->getPaneTopLeftCell()); + self::assertNotEquals(0, $sheet->getXSplit()); + self::assertEquals(0, $sheet->getYSplit()); + //self::assertSame('E1', $sheet->getSelectedCells()); + //self::assertNotNull($sheet->getPane('topRight')); + + $sheet = $spreadsheet->getSheetByNameOrThrow('SplitHorizontal'); + self::assertNull($sheet->getFreezePane()); + self::assertSame('A3', $sheet->getTopLeftCell()); + self::assertSame('A6', $sheet->getPaneTopLeftCell()); + self::assertEquals(0, $sheet->getXSplit()); + self::assertNotEquals(0, $sheet->getYSplit()); + //self::assertSame('A7', $sheet->getSelectedCells()); + //self::assertNotNull($sheet->getPane('bottomLeft')); + + $sheet = $spreadsheet->getSheetByNameOrThrow('SplitBoth'); + self::assertNull($sheet->getFreezePane()); + self::assertSame('H3', $sheet->getTopLeftCell()); + self::assertSame('E19', $sheet->getPaneTopLeftCell()); + self::assertNotEquals(0, $sheet->getXSplit()); + self::assertNotEquals(0, $sheet->getYSplit()); + //self::assertSame('E20', $sheet->getSelectedCells()); + //self::assertNotNull($sheet->getPane('bottomLeft')); + //self::assertNotNull($sheet->getPane('bottomRight')); + //self::assertNotNull($sheet->getPane('topRight')); + + $sheet = $spreadsheet->getSheetByNameOrThrow('NoFreezeNorSplit'); + self::assertNull($sheet->getFreezePane()); + self::assertSame('D3', $sheet->getTopLeftCell()); + self::assertSame('', $sheet->getPaneTopLeftCell()); + self::assertSame('D5', $sheet->getSelectedCells()); + self::assertNull($sheet->getPane('bottomLeft')); + self::assertNull($sheet->getPane('bottomRight')); + self::assertNull($sheet->getPane('topRight')); + + $sheet = $spreadsheet->getSheetByNameOrThrow('FrozenSplit'); + self::assertSame('B4', $sheet->getFreezePane()); + self::assertSame('frozenSplit', $sheet->getPaneState()); + self::assertSame('B4', $sheet->getPaneTopLeftCell()); + self::assertSame('B4', $sheet->getTopLeftCell()); + //self::assertSame('B4', $sheet->getSelectedCells()); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/Worksheet2Test.php b/tests/PhpSpreadsheetTests/Worksheet/Worksheet2Test.php index 519852eb6f..2b1a2b5d82 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Worksheet2Test.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Worksheet2Test.php @@ -71,13 +71,36 @@ private function getPane(Worksheet $sheet): ?string public function testFreeze(): void { - $worksheet = new Worksheet(); - $worksheet->freezePane('A1'); + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $pane = $worksheet->getActivePane(); + self::assertEmpty($pane); + $worksheet->setSelectedCells('D3'); + $worksheet->freezePane('A2'); $freeze = $this->getPane($worksheet); - self::assertSame('A1', $freeze); + $pane = $worksheet->getActivePane(); + $selected = $worksheet->getSelectedCells(); + self::assertSame('A2', $freeze); + self::assertSame('D3', $selected); + self::assertSame('bottomLeft', $pane); $worksheet->unfreezePane(); // Scrutinizer is an idiot. If it still complains, I give up. self::assertNull($this->getPane($worksheet)); + $freeze = $this->getPane($worksheet); + $pane = $worksheet->getActivePane(); + $selected = $worksheet->getSelectedCells(); + self::assertEmpty($freeze); + self::assertEquals('', $pane); + self::assertSame('D3', $selected); + $spreadsheet->disconnectWorksheets(); + } + + public function testFreezeA1(): void + { + $worksheet = new Worksheet(); + $worksheet->freezePane('A1'); + $freeze = $this->getPane($worksheet); + self::assertNull($freeze); } public function testInsertBeforeRowOne(): void diff --git a/tests/data/Reader/XLSX/splits.xlsx b/tests/data/Reader/XLSX/splits.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..babe83c22257c955dbfafb7631560a77098d8709 GIT binary patch literal 13543 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 RJKiW@E1>MVVEO*r{{vg9=0E@d literal 0 HcmV?d00001 diff --git a/tests/data/Reader/Xml/splits.xml b/tests/data/Reader/Xml/splits.xml new file mode 100644 index 0000000000..434b77bac0 --- /dev/null +++ b/tests/data/Reader/Xml/splits.xml @@ -0,0 +1,288 @@ + + + + + Owen Leibman + Owen Leibman + 2023-06-16T06:25:47Z + 2023-06-17T21:18:13Z + 16.00 + + + true + 2023-06-16T06:33:59Z + Standard + defa4170-0d19-0005-0004-bc88714345d2 + f9465cb1-7889-4d9a-b552-fdd0addf0eb1 + 249f96c6-562d-4909-af9a-fa41f1d18cce + 0 + + + + + + 6820 + 19200 + 32767 + 32767 + 4 + False + False + + + + + + + + + 0 + 1 + + + 2 + 3 + +
+ + +
+