diff --git a/phpstan.neon b/phpstan.neon index be37969a..9d52fd98 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,2 @@ parameters: checkMissingIterableValueType: false - ignoreErrors: - - - message: '#Function PhpSchool\\\\CliMenu\\\\Util\\\\mapWithKeys should return array but returns array|false#' - path: src/Util/ArrayUtils.php diff --git a/src/Builder/CliMenuBuilder.php b/src/Builder/CliMenuBuilder.php index 5454cdf8..867f7859 100644 --- a/src/Builder/CliMenuBuilder.php +++ b/src/Builder/CliMenuBuilder.php @@ -17,6 +17,7 @@ use PhpSchool\CliMenu\MenuItem\StaticItem; use PhpSchool\CliMenu\MenuStyle; use PhpSchool\CliMenu\Style\CheckboxStyle; +use PhpSchool\CliMenu\Style\DefaultStyle; use PhpSchool\CliMenu\Style\RadioStyle; use PhpSchool\CliMenu\Style\SelectableStyle; use PhpSchool\CliMenu\Terminal\TerminalFactory; @@ -397,7 +398,7 @@ public function setMargin(int $margin) : self public function setItemExtra(string $extra) : self { $this->style->setItemExtra($extra); - $this->menu->getSelectableStyle()->setItemExtra($extra); + $this->getSelectableStyle()->setItemExtra($extra); // if we customise item extra, it means we most likely want to display it $this->displayExtra(); @@ -490,7 +491,7 @@ public function disableDefaultItems() : self public function displayExtra() : self { $this->style->setDisplaysExtra(true); - $this->menu->getSelectableStyle()->setDisplaysExtra(true); + $this->getSelectableStyle()->setDisplaysExtra(true); return $this; } @@ -513,126 +514,93 @@ public function build() : CliMenu } if (!$this->subMenu) { - $this->propagateStyles($this->menu); + $this->menu->propagateStyles(); } return $this->menu; } - public function getCheckboxStyle() : CheckboxStyle + public function getDefaultStyle() : DefaultStyle { - return $this->menu->getCheckboxStyle(); + $style = $this->menu->getItemStyle(DefaultStyle::class); + assert($style instanceof DefaultStyle); + return $style; } - public function setCheckboxStyle(CheckboxStyle $style) : self + public function setDefaultStyle(DefaultStyle $style) : self { - $this->menu->setCheckboxStyle($style); + $this->menu->setItemStyle($style, DefaultStyle::class); return $this; } - public function modifyCheckboxStyle(callable $itemCallable) : self + public function modifyDefaultStyle(callable $itemCallable) : self { - $itemCallable($this->menu->getCheckboxStyle()); + $itemCallable($this->getDefaultStyle()); return $this; } - public function getRadioStyle() : RadioStyle + public function getSelectableStyle() : SelectableStyle { - return $this->menu->getRadioStyle(); + $style = $this->menu->getItemStyle(SelectableStyle::class); + assert($style instanceof SelectableStyle); + return $style; } - public function setRadioStyle(RadioStyle $style) : self + public function setSelectableStyle(SelectableStyle $style) : self { - $this->menu->setRadioStyle($style); + $this->menu->setItemStyle($style, SelectableStyle::class); return $this; } - public function modifyRadioStyle(callable $itemCallable) : self + public function modifySelectableStyle(callable $itemCallable) : self { - $itemCallable($this->menu->getRadioStyle()); + $itemCallable($this->getSelectableStyle()); return $this; } - public function getSelectableStyle() : SelectableStyle + public function getCheckboxStyle() : CheckboxStyle { - return $this->menu->getSelectableStyle(); + $style = $this->menu->getItemStyle(CheckboxStyle::class); + assert($style instanceof CheckboxStyle); + return $style; } - public function setSelectableStyle(SelectableStyle $style) : self + public function setCheckboxStyle(CheckboxStyle $style) : self { - $this->menu->setSelectableStyle($style); + $this->menu->setItemStyle($style, CheckboxStyle::class); return $this; } - public function modifySelectableStyle(callable $itemCallable) : self + public function modifyCheckboxStyle(callable $itemCallable) : self { - $itemCallable($this->menu->getSelectableStyle()); + $itemCallable($this->getCheckboxStyle()); return $this; } - /** - * Pass styles from current menu to sub-menu - * only if sub-menu style has not be customized - */ - private function propagateStyles(CliMenu $menu, array $items = []) : void - { - $currentItems = !empty($items) ? $items : $menu->getItems(); - - foreach ($currentItems as $item) { - if ($item instanceof CheckboxItem - && !$item->getStyle()->hasChangedFromDefaults() - ) { - $item->setStyle(clone $menu->getCheckboxStyle()); - } - - if ($item instanceof RadioItem - && !$item->getStyle()->hasChangedFromDefaults() - ) { - $item->setStyle(clone $menu->getRadioStyle()); - } - - if (($item instanceof MenuMenuItem - || $item instanceof SelectableItem - || $item instanceof StaticItem - ) - && !$item->getStyle()->hasChangedFromDefaults() - ) { - $item->setStyle(clone $menu->getSelectableStyle()); - } - - // Apply current style to children, if they are not customized - if ($item instanceof MenuMenuItem) { - $subMenu = $item->getSubMenu(); - - if (!$subMenu->getStyle()->hasChangedFromDefaults()) { - $subMenu->setStyle(clone $menu->getStyle()); - } - - if (!$subMenu->getCheckboxStyle()->hasChangedFromDefaults()) { - $subMenu->setCheckboxStyle(clone $menu->getCheckboxStyle()); - } + public function getRadioStyle() : RadioStyle + { + $style = $this->menu->getItemStyle(RadioStyle::class); + assert($style instanceof RadioStyle); + return $style; + } - if (!$subMenu->getRadioStyle()->hasChangedFromDefaults()) { - $subMenu->setRadioStyle(clone $menu->getRadioStyle()); - } + public function setRadioStyle(RadioStyle $style) : self + { + $this->menu->setItemStyle($style, RadioItem::class); - if (!$subMenu->getSelectableStyle()->hasChangedFromDefaults()) { - $subMenu->setSelectableStyle(clone $menu->getSelectableStyle()); - } + return $this; + } - $this->propagateStyles($subMenu); - } + public function modifyRadioStyle(callable $itemCallable) : self + { + $itemCallable($this->getRadioStyle()); - // Apply styles to SplitItem children using current $menu - if ($item instanceof SplitItem) { - $this->propagateStyles($menu, $item->getItems()); - } - } + return $this; } } diff --git a/src/CliMenu.php b/src/CliMenu.php index c57ca5ff..1a0bea7a 100644 --- a/src/CliMenu.php +++ b/src/CliMenu.php @@ -10,18 +10,19 @@ use PhpSchool\CliMenu\Input\Text; use PhpSchool\CliMenu\MenuItem\LineBreakItem; use PhpSchool\CliMenu\MenuItem\MenuItemInterface; +use PhpSchool\CliMenu\MenuItem\PropagatesStyles; use PhpSchool\CliMenu\MenuItem\SplitItem; use PhpSchool\CliMenu\MenuItem\StaticItem; use PhpSchool\CliMenu\Dialogue\Confirm; use PhpSchool\CliMenu\Dialogue\Flash; -use PhpSchool\CliMenu\Style\CheckboxStyle; -use PhpSchool\CliMenu\Style\RadioStyle; -use PhpSchool\CliMenu\Style\SelectableStyle; +use PhpSchool\CliMenu\Style\ItemStyle; +use PhpSchool\CliMenu\Style\Locator; use PhpSchool\CliMenu\Terminal\TerminalFactory; use PhpSchool\CliMenu\Util\StringUtil as s; use PhpSchool\Terminal\InputCharacter; use PhpSchool\Terminal\NonCanonicalReader; use PhpSchool\Terminal\Terminal; +use function PhpSchool\CliMenu\Util\each; /** * @author Michael Woodward @@ -39,19 +40,9 @@ class CliMenu protected $style; /** - * @var CheckboxStyle + * @var Locator */ - private $checkboxStyle; - - /** - * @var RadioStyle - */ - private $radioStyle; - - /** - * @var SelectableStyle - */ - private $selectableStyle; + private $itemStyleLocator; /** * @var ?string @@ -112,9 +103,8 @@ public function __construct( $this->items = $items; $this->terminal = $terminal ?: TerminalFactory::fromSystem(); $this->style = $style ?: new MenuStyle($this->terminal); - $this->checkboxStyle = new CheckboxStyle(); - $this->radioStyle = new RadioStyle(); - $this->selectableStyle = new SelectableStyle(); + + $this->itemStyleLocator = new Locator(); $this->selectFirstItem(); } @@ -661,40 +651,28 @@ public function setStyle(MenuStyle $style) : void $this->style = $style; } - public function getCheckboxStyle() : CheckboxStyle + public function setItemStyle(ItemStyle $style, string $styleClass) : void { - return $this->checkboxStyle; + $this->itemStyleLocator->setStyle($style, $styleClass); } - public function setCheckboxStyle(CheckboxStyle $style) : self + public function getItemStyle(string $styleClass) : ItemStyle { - $this->checkboxStyle = $style; - - return $this; + return $this->itemStyleLocator->getStyle($styleClass); } - public function getRadioStyle() : RadioStyle + public function getItemStyleForItem(MenuItemInterface $item) : ItemStyle { - return $this->radioStyle; + return $this->itemStyleLocator->getStyleForMenuItem($item); } - public function setRadioStyle(RadioStyle $style) : self + public function importStyles(CliMenu $menu) : void { - $this->radioStyle = $style; - - return $this; - } - - public function getSelectableStyle() : SelectableStyle - { - return $this->selectableStyle; - } - - public function setSelectableStyle(SelectableStyle $style) : self - { - $this->selectableStyle = $style; + if (!$this->style->hasChangedFromDefaults()) { + $this->style = $menu->style; + } - return $this; + $this->itemStyleLocator->importFrom($menu->itemStyleLocator); } public function getCurrentFrame() : Frame @@ -763,4 +741,25 @@ private function guardSingleLine(string $text) : void throw new \InvalidArgumentException; } } + + public function propagateStyles() : void + { + each( + array_filter($this->items, function (MenuItemInterface $item) { + return !$item->getStyle()->hasChangedFromDefaults(); + }), + function (int $index, $item) { + $item->setStyle(clone $this->getItemStyleForItem($item)); + } + ); + + each( + array_filter($this->items, function (MenuItemInterface $item) { + return $item instanceof PropagatesStyles; + }), + function (int $index, PropagatesStyles $item) { + $item->propagateStyles($this); + } + ); + } } diff --git a/src/MenuItem/AsciiArtItem.php b/src/MenuItem/AsciiArtItem.php index 7fa1b3b3..a3c70d97 100644 --- a/src/MenuItem/AsciiArtItem.php +++ b/src/MenuItem/AsciiArtItem.php @@ -4,6 +4,8 @@ use Assert\Assertion; use PhpSchool\CliMenu\MenuStyle; +use PhpSchool\CliMenu\Style\DefaultStyle; +use PhpSchool\CliMenu\Style\ItemStyle; /** * @author Michael Woodward @@ -37,6 +39,11 @@ class AsciiArtItem implements MenuItemInterface */ private $artLength; + /** + * @var DefaultStyle + */ + private $style; + public function __construct(string $text, string $position = self::POSITION_CENTER, string $alt = '') { Assertion::inArray($position, [self::POSITION_CENTER, self::POSITION_RIGHT, self::POSITION_LEFT]); @@ -44,6 +51,8 @@ public function __construct(string $text, string $position = self::POSITION_CENT $this->setText($text); $this->position = $position; $this->alternateText = $alt; + + $this->style = new DefaultStyle(); } /** @@ -161,4 +170,17 @@ public function hideItemExtra() : void { //noop } + + /** + * @return DefaultStyle + */ + public function getStyle() : ItemStyle + { + return $this->style; + } + + public function setStyle(DefaultStyle $style) : void + { + $this->style = $style; + } } diff --git a/src/MenuItem/CheckboxItem.php b/src/MenuItem/CheckboxItem.php index 7777355f..55f8de62 100644 --- a/src/MenuItem/CheckboxItem.php +++ b/src/MenuItem/CheckboxItem.php @@ -6,6 +6,7 @@ use PhpSchool\CliMenu\MenuStyle; use PhpSchool\CliMenu\Util\StringUtil; use PhpSchool\CliMenu\Style\CheckboxStyle; +use PhpSchool\CliMenu\Style\ItemStyle; class CheckboxItem implements MenuItemInterface { @@ -60,7 +61,7 @@ public function __construct( */ public function getRows(MenuStyle $style, bool $selected = false) : array { - $marker = sprintf("%s", $this->style->getMarker($this->checked)); + $marker = sprintf("%s", $this->style->getMarker($this, $selected)); $itemExtra = $this->style->getItemExtra(); @@ -183,15 +184,16 @@ public function toggle() : void $this->checked = !$this->checked; } - public function getStyle() : CheckboxStyle + /** + * @return CheckboxStyle + */ + public function getStyle() : ItemStyle { return $this->style; } - public function setStyle(CheckboxStyle $style) : self + public function setStyle(CheckboxStyle $style) : void { $this->style = $style; - - return $this; } } diff --git a/src/MenuItem/LineBreakItem.php b/src/MenuItem/LineBreakItem.php index 0b22a52c..ac07dedc 100644 --- a/src/MenuItem/LineBreakItem.php +++ b/src/MenuItem/LineBreakItem.php @@ -4,6 +4,8 @@ use Assert\Assertion; use PhpSchool\CliMenu\MenuStyle; +use PhpSchool\CliMenu\Style\DefaultStyle; +use PhpSchool\CliMenu\Style\ItemStyle; /** * @author Michael Woodward @@ -20,10 +22,17 @@ class LineBreakItem implements MenuItemInterface */ private $lines; + /** + * @var DefaultStyle + */ + private $style; + public function __construct(string $breakChar = ' ', int $lines = 1) { $this->breakChar = $breakChar; - $this->lines = $lines; + $this->lines = $lines; + + $this->style = new DefaultStyle(); } /** @@ -100,4 +109,17 @@ public function hideItemExtra() : void { //noop } + + /** + * @return DefaultStyle + */ + public function getStyle() : ItemStyle + { + return $this->style; + } + + public function setStyle(DefaultStyle $style) : void + { + $this->style = $style; + } } diff --git a/src/MenuItem/MenuItemInterface.php b/src/MenuItem/MenuItemInterface.php index 0561fce0..971c5d53 100644 --- a/src/MenuItem/MenuItemInterface.php +++ b/src/MenuItem/MenuItemInterface.php @@ -3,6 +3,7 @@ namespace PhpSchool\CliMenu\MenuItem; use PhpSchool\CliMenu\MenuStyle; +use PhpSchool\CliMenu\Style\ItemStyle; /** * @author Michael Woodward @@ -43,4 +44,11 @@ public function showItemExtra() : void; * Disable showing item extra */ public function hideItemExtra() : void; + + /** + * Get the items style object. Can and + * should be subclassed to provide bespoke + * behaviour. + */ + public function getStyle() : ItemStyle; } diff --git a/src/MenuItem/MenuMenuItem.php b/src/MenuItem/MenuMenuItem.php index faca885c..8d0c1bb7 100644 --- a/src/MenuItem/MenuMenuItem.php +++ b/src/MenuItem/MenuMenuItem.php @@ -5,13 +5,14 @@ use PhpSchool\CliMenu\CliMenu; use PhpSchool\CliMenu\MenuStyle; use PhpSchool\CliMenu\Util\StringUtil; +use PhpSchool\CliMenu\Style\ItemStyle; use PhpSchool\CliMenu\Style\SelectableStyle; use function PhpSchool\CliMenu\Util\mapWithKeys; /** * @author Michael Woodward */ -class MenuMenuItem implements MenuItemInterface +class MenuMenuItem implements MenuItemInterface, PropagatesStyles { /** * @var string @@ -55,7 +56,7 @@ public function __construct( */ public function getRows(MenuStyle $style, bool $selected = false) : array { - $marker = sprintf("%s", $this->style->getMarker($selected)); + $marker = sprintf("%s", $this->style->getMarker($this, $selected)); $length = $this->style->getDisplaysExtra() ? $style->getContentWidth() - (mb_strlen($this->style->getItemExtra()) + 2) @@ -163,15 +164,25 @@ public function hideItemExtra() : void $this->showItemExtra = false; } - public function getStyle() : SelectableStyle + /** + * @return SelectableStyle + */ + public function getStyle() : ItemStyle { return $this->style; } - public function setStyle(SelectableStyle $style) : self + public function setStyle(SelectableStyle $style) : void { $this->style = $style; + } - return $this; + /** + * @inheritDoc + */ + public function propagateStyles(CliMenu $parent): void + { + $this->getSubMenu()->importStyles($parent); + $this->getSubMenu()->propagateStyles(); } } diff --git a/src/MenuItem/PropagatesStyles.php b/src/MenuItem/PropagatesStyles.php new file mode 100644 index 00000000..5c412069 --- /dev/null +++ b/src/MenuItem/PropagatesStyles.php @@ -0,0 +1,16 @@ +style->getMarker($this->checked)); + $marker = sprintf("%s", $this->style->getMarker($this, $selected)); $itemExtra = $this->style->getItemExtra(); @@ -203,15 +204,16 @@ public function toggle() : void $this->checked = !$this->checked; } - public function getStyle() : RadioStyle + /** + * @return RadioStyle + */ + public function getStyle() : ItemStyle { return $this->style; } - public function setStyle(RadioStyle $style) : self + public function setStyle(RadioStyle $style) : void { $this->style = $style; - - return $this; } } diff --git a/src/MenuItem/SelectableItem.php b/src/MenuItem/SelectableItem.php index 509a1c1b..7e952ea0 100644 --- a/src/MenuItem/SelectableItem.php +++ b/src/MenuItem/SelectableItem.php @@ -3,6 +3,7 @@ namespace PhpSchool\CliMenu\MenuItem; use PhpSchool\CliMenu\MenuStyle; +use PhpSchool\CliMenu\Style\ItemStyle; use PhpSchool\CliMenu\Util\StringUtil; use PhpSchool\CliMenu\Style\SelectableStyle; use function PhpSchool\CliMenu\Util\mapWithKeys; @@ -56,7 +57,7 @@ public function __construct( */ public function getRows(MenuStyle $style, bool $selected = false) : array { - $marker = sprintf("%s", $this->style->getMarker($selected)); + $marker = sprintf("%s", $this->style->getMarker($this, $selected)); $length = $this->style->getDisplaysExtra() ? $style->getContentWidth() - (mb_strlen($this->style->getItemExtra()) + 2) @@ -145,15 +146,16 @@ public function hideItemExtra() : void $this->showItemExtra = false; } - public function getStyle() : SelectableStyle + /** + * @return SelectableStyle + */ + public function getStyle() : ItemStyle { return $this->style; } - public function setStyle(SelectableStyle $style) : self + public function setStyle(SelectableStyle $style) : void { $this->style = $style; - - return $this; } } diff --git a/src/MenuItem/SplitItem.php b/src/MenuItem/SplitItem.php index 7eec8b00..dbadbda9 100644 --- a/src/MenuItem/SplitItem.php +++ b/src/MenuItem/SplitItem.php @@ -3,14 +3,20 @@ namespace PhpSchool\CliMenu\MenuItem; use Assert\Assertion; +use PhpSchool\CliMenu\CliMenu; use PhpSchool\CliMenu\MenuStyle; +use PhpSchool\CliMenu\Style\DefaultStyle; +use PhpSchool\CliMenu\Style\ItemStyle; +use PhpSchool\CliMenu\Style\Selectable; use PhpSchool\CliMenu\Util\StringUtil; +use function PhpSchool\CliMenu\Util\each; use function PhpSchool\CliMenu\Util\mapWithKeys; +use function PhpSchool\CliMenu\Util\max; /** * @author Michael Woodward */ -class SplitItem implements MenuItemInterface +class SplitItem implements MenuItemInterface, PropagatesStyles { /** * @var array @@ -32,6 +38,11 @@ class SplitItem implements MenuItemInterface */ private $gutter = 2; + /** + * @var DefaultStyle + */ + private $style; + /** * @var array */ @@ -45,6 +56,8 @@ public function __construct(array $items = []) { $this->addItems($items); $this->setDefaultSelectedItem(); + + $this->style = new DefaultStyle(); } public function getGutter() : int @@ -133,17 +146,11 @@ public function getRows(MenuStyle $style, bool $selected = false) : array mapWithKeys($this->items, function (int $index, MenuItemInterface $item) use ($selected, $length, $style) { $isSelected = $selected && $index === $this->selectedItemIndex; - if ($item instanceof CheckboxItem || $item instanceof RadioItem) { - $markerType = $item->getStyle()->getMarker($item->getChecked()); - } else { - /** @var MenuMenuItem|SelectableItem|StaticItem $item */ - $markerType = $item->getStyle()->getMarker($isSelected); + $marker = ''; + if ($item->canSelect()) { + $marker = $item->getStyle()->getMarker($item, $isSelected); } - $marker = $item->canSelect() - ? sprintf('%s', $markerType) - : ''; - $itemExtra = ''; if ($item->getStyle()->getDisplaysExtra()) { $itemExtraVal = $item->getStyle()->getItemExtra(); @@ -332,21 +339,50 @@ public function getText() : string */ private function calculateItemExtra() : int { - $largestItemExtra = 0; + return max(array_map( + function (MenuItemInterface $item) { + return mb_strlen($item->getStyle()->getItemExtra()); + }, + array_filter($this->items, function (MenuItemInterface $item) { + return $item->getStyle()->getDisplaysExtra(); + }) + )); + } - /** @var CheckboxItem|RadioItem|MenuMenuItem|SelectableItem|StaticItem $item */ - foreach ($this->items as $item) { - if (!$item->getStyle()->getDisplaysExtra()) { - continue; - } + /** + * @return DefaultStyle + */ + public function getStyle(): ItemStyle + { + return $this->style; + } - if (mb_strlen($item->getStyle()->getItemExtra()) < $largestItemExtra) { - continue; - } + public function setStyle(DefaultStyle $style): void + { + $this->style = $style; + } - $largestItemExtra = mb_strlen($item->getStyle()->getItemExtra()); - } + /** + * @inheritDoc + */ + public function propagateStyles(CliMenu $parent): void + { + each( + array_filter($this->getItems(), function (MenuItemInterface $item) { + return !$item->getStyle()->hasChangedFromDefaults(); + }), + function ($index, $item) use ($parent) { + $item->setStyle(clone $parent->getItemStyleForItem($item)); + } + ); - return $largestItemExtra; + each( + array_filter($this->getItems(), function (MenuItemInterface $item) { + return $item instanceof PropagatesStyles; + }), + function ($index, PropagatesStyles $item) use ($parent) { + $item->propagateStyles($parent); + } + ); } } diff --git a/src/MenuItem/StaticItem.php b/src/MenuItem/StaticItem.php index efa326de..87c53f69 100644 --- a/src/MenuItem/StaticItem.php +++ b/src/MenuItem/StaticItem.php @@ -3,8 +3,9 @@ namespace PhpSchool\CliMenu\MenuItem; use PhpSchool\CliMenu\MenuStyle; +use PhpSchool\CliMenu\Style\DefaultStyle; +use PhpSchool\CliMenu\Style\ItemStyle; use PhpSchool\CliMenu\Util\StringUtil; -use PhpSchool\CliMenu\Style\SelectableStyle; /** * @author Michael Woodward @@ -17,7 +18,7 @@ class StaticItem implements MenuItemInterface private $text; /** - * @var SelectableStyle; + * @var DefaultStyle */ private $style; @@ -25,7 +26,7 @@ public function __construct(string $text) { $this->text = $text; - $this->style = new SelectableStyle(); + $this->style = new DefaultStyle(); } /** @@ -92,15 +93,16 @@ public function hideItemExtra() : void //noop } - public function getStyle() : SelectableStyle + /** + * @return DefaultStyle + */ + public function getStyle() : ItemStyle { return $this->style; } - public function setStyle(SelectableStyle $style) : self + public function setStyle(DefaultStyle $style) : void { $this->style = $style; - - return $this; } } diff --git a/src/Style/CheckboxStyle.php b/src/Style/CheckboxStyle.php index 36339eb5..ed9a6ae4 100644 --- a/src/Style/CheckboxStyle.php +++ b/src/Style/CheckboxStyle.php @@ -2,7 +2,10 @@ namespace PhpSchool\CliMenu\Style; -class CheckboxStyle +use PhpSchool\CliMenu\MenuItem\CheckboxItem; +use PhpSchool\CliMenu\MenuItem\MenuItemInterface; + +class CheckboxStyle implements ItemStyle { private const DEFAULT_STYLES = [ 'checkedMarker' => '[✔] ', @@ -51,9 +54,15 @@ public function hasChangedFromDefaults() : bool return $currentValues !== array_values(self::DEFAULT_STYLES); } - public function getMarker(bool $selected) : string + public function getMarker(MenuItemInterface $item, bool $selected) : string { - return $selected ? $this->checkedMarker : $this->uncheckedMarker; + if (!$item instanceof CheckboxItem) { + throw new \InvalidArgumentException( + sprintf('Expected an instance of: %s. Got: %s', CheckboxItem::class, get_class($item)) + ); + } + + return $item->getChecked() ? $this->checkedMarker : $this->uncheckedMarker; } public function getCheckedMarker() : string diff --git a/src/Style/DefaultStyle.php b/src/Style/DefaultStyle.php new file mode 100644 index 00000000..cd751a1c --- /dev/null +++ b/src/Style/DefaultStyle.php @@ -0,0 +1,33 @@ + DefaultStyle::class, + AsciiArtItem::class => DefaultStyle::class, + LineBreakItem::class => DefaultStyle::class, + SplitItem::class => DefaultStyle::class, + SelectableItem::class => SelectableStyle::class, + MenuMenuItem::class => SelectableStyle::class, + CheckboxItem::class => CheckboxStyle::class, + RadioItem::class => RadioStyle::class, + ]; + + /** + * @var array + */ + private $styles; + + public function __construct() + { + $this->styles = [ + DefaultStyle::class => new DefaultStyle(), + SelectableStyle::class => new SelectableStyle(), + CheckboxStyle::class => new CheckboxStyle(), + RadioStyle::class => new RadioStyle() + ]; + } + + /** + * For each of our unmodified item styles, we replace ours with the versions + * from the given style locator. + * + * @param Locator $other + */ + public function importFrom(self $other) : void + { + $this->styles = mapWithKeys( + $this->styles, + function ($styleClass, ItemStyle $instance) use ($other) { + return $instance->hasChangedFromDefaults() + ? $instance + : $other->getStyle($styleClass); + } + ); + } + + public function getStyle(string $styleClass) : ItemStyle + { + if (!isset($this->styles[$styleClass])) { + throw InvalidStyle::unregisteredStyle($styleClass); + } + + return $this->styles[$styleClass]; + } + + public function setStyle(ItemStyle $itemStyle, string $styleClass) : void + { + if (!isset($this->styles[$styleClass])) { + throw InvalidStyle::unregisteredStyle($styleClass); + } + + if (!$itemStyle instanceof $styleClass) { + throw InvalidStyle::notSubClassOf($styleClass); + } + + $this->styles[$styleClass] = $itemStyle; + } + + public function getStyleForMenuItem(MenuItemInterface $item) : ItemStyle + { + if (!isset($this->itemStyleMap[get_class($item)])) { + throw InvalidStyle::unregisteredItem(get_class($item)); + } + + $styleClass = $this->itemStyleMap[get_class($item)]; + + return $this->getStyle($styleClass); + } +} diff --git a/src/Style/RadioStyle.php b/src/Style/RadioStyle.php index f4a7a657..63cb592a 100644 --- a/src/Style/RadioStyle.php +++ b/src/Style/RadioStyle.php @@ -2,7 +2,10 @@ namespace PhpSchool\CliMenu\Style; -class RadioStyle +use PhpSchool\CliMenu\MenuItem\MenuItemInterface; +use PhpSchool\CliMenu\MenuItem\RadioItem; + +class RadioStyle implements ItemStyle { private const DEFAULT_STYLES = [ 'checkedMarker' => '[●] ', @@ -51,9 +54,15 @@ public function hasChangedFromDefaults() : bool return $currentValues !== array_values(self::DEFAULT_STYLES); } - public function getMarker(bool $selected) : string + public function getMarker(MenuItemInterface $item, bool $selected) : string { - return $selected ? $this->checkedMarker : $this->uncheckedMarker; + if (!$item instanceof RadioItem) { + throw new \InvalidArgumentException( + sprintf('Expected an instance of: %s. Got: %s', RadioItem::class, get_class($item)) + ); + } + + return $item->getChecked() ? $this->checkedMarker : $this->uncheckedMarker; } public function getCheckedMarker() : string diff --git a/src/Style/SelectableStyle.php b/src/Style/SelectableStyle.php index ad78d4d1..d0224186 100644 --- a/src/Style/SelectableStyle.php +++ b/src/Style/SelectableStyle.php @@ -2,7 +2,9 @@ namespace PhpSchool\CliMenu\Style; -class SelectableStyle +use PhpSchool\CliMenu\MenuItem\MenuItemInterface; + +class SelectableStyle implements ItemStyle { private const DEFAULT_STYLES = [ 'selectedMarker' => '● ', @@ -51,7 +53,7 @@ public function hasChangedFromDefaults() : bool return $currentValues !== array_values(self::DEFAULT_STYLES); } - public function getMarker(bool $selected) : string + public function getMarker(MenuItemInterface $item, bool $selected) : string { return $selected ? $this->selectedMarker : $this->unselectedMarker; } diff --git a/src/Util/ArrayUtils.php b/src/Util/ArrayUtils.php index 60aad3e8..f51cc90c 100644 --- a/src/Util/ArrayUtils.php +++ b/src/Util/ArrayUtils.php @@ -6,10 +6,14 @@ function mapWithKeys(array $array, callable $callback) : array { - return array_combine( + $arr = array_combine( array_keys($array), array_map($callback, array_keys($array), $array) ); + + assert(is_array($arr)); + + return $arr; } function each(array $array, callable $callback) : void @@ -18,3 +22,8 @@ function each(array $array, callable $callback) : void $callback($k, $v); } } + +function max(array $items) : int +{ + return count($items) > 0 ? \max($items) : 0; +} diff --git a/test/MenuItem/SplitItemTest.php b/test/MenuItem/SplitItemTest.php index b1748e01..2d21f448 100644 --- a/test/MenuItem/SplitItemTest.php +++ b/test/MenuItem/SplitItemTest.php @@ -198,14 +198,15 @@ public function testGetRowsWithOneItemSelected() : void ->setSelectedMarker('= ') ->setUnselectedMarker('* '); - $item = new SplitItem( - [ - (new SelectableItem('Item One', function () { - }))->setStyle($selectableStyle), - (new SelectableItem('Item Two', function () { - }))->setStyle($selectableStyle), - ] - ); + $cb = function () { + }; + + $item1 = new SelectableItem("Item One", $cb); + $item1->setStyle($selectableStyle); + $item2 = new SelectableItem("Item Two", $cb); + $item2->setStyle($selectableStyle); + + $item = new SplitItem([$item1, $item2]); $item->setSelectedItemIndex(0); @@ -244,14 +245,15 @@ public function testGetRowsWithMultipleLinesWithUnSelectedMarker() : void $selectableStyle = (new SelectableStyle()) ->setUnselectedMarker('* '); - $item = new SplitItem( - [ - (new SelectableItem("Item\nOne", function () { - }))->setStyle($selectableStyle), - (new SelectableItem("Item\nTwo", function () { - }))->setStyle($selectableStyle), - ] - ); + $cb = function () { + }; + + $item1 = new SelectableItem("Item\nOne", $cb); + $item1->setStyle($selectableStyle); + $item2 = new SelectableItem("Item\nTwo", $cb); + $item2->setStyle($selectableStyle); + + $item = new SplitItem([$item1, $item2]); self::assertEquals( [ @@ -275,14 +277,15 @@ public function testGetRowsWithMultipleLinesWithOneItemSelected() : void ->setSelectedMarker('= ') ->setUnselectedMarker('* '); - $item = new SplitItem( - [ - (new SelectableItem("Item\nOne", function () { - }))->setStyle($selectableStyle), - (new SelectableItem("Item\nTwo", function () { - }))->setStyle($selectableStyle), - ] - ); + $cb = function () { + }; + + $item1 = new SelectableItem("Item\nOne", $cb); + $item1->setStyle($selectableStyle); + $item2 = new SelectableItem("Item\nTwo", $cb); + $item2->setStyle($selectableStyle); + + $item = new SplitItem([$item1, $item2]); $item->setSelectedItemIndex(0); @@ -309,14 +312,15 @@ public function testGetRowsWithItemExtra() : void ->setDisplaysExtra(true) ->setUnselectedMarker('* '); - $item = new SplitItem( - [ - (new SelectableItem('Item 1', function () { - }, true))->setStyle($selectableStyle), - (new SelectableItem('Item 2', function () { - }, true))->setStyle($selectableStyle), - ] - ); + $cb = function () { + }; + + $item1 = new SelectableItem('Item 1', $cb, true); + $item1->setStyle($selectableStyle); + $item2 = new SelectableItem('Item 2', $cb, true); + $item2->setStyle($selectableStyle); + + $item = new SplitItem([$item1, $item2]); self::assertEquals(['* Item 1 [EXTRA] * Item 2 [EXTRA] '], $item->getRows($menuStyle)); } @@ -335,14 +339,15 @@ public function testGetRowsWithMultipleLinesWithItemExtra() : void ->setDisplaysExtra(true) ->setUnselectedMarker('* '); - $item = new SplitItem( - [ - (new SelectableItem("Item 1\nItem 1", function () { - }, true))->setStyle($selectableStyle), - (new SelectableItem("Item 2\nItem 2", function () { - }, true))->setStyle($selectableStyle), - ] - ); + $cb = function () { + }; + + $item1 = new SelectableItem("Item 1\nItem 1", $cb, true); + $item1->setStyle($selectableStyle); + $item2 = new SelectableItem("Item 2\nItem 2", $cb, true); + $item2->setStyle($selectableStyle); + + $item = new SplitItem([$item1, $item2]); self::assertEquals( [ @@ -367,14 +372,15 @@ public function testGetRowsWithMultipleLinesWithItemExtraOnOne() : void ->setDisplaysExtra(true) ->setUnselectedMarker('* '); - $item = new SplitItem( - [ - (new SelectableItem("Item 1\nItem 1", function () { - }))->setStyle($selectableStyle), - (new SelectableItem("Item 2\nItem 2", function () { - }, true))->setStyle($selectableStyle), - ] - ); + $cb = function () { + }; + + $item1 = new SelectableItem("Item 1\nItem 1", $cb); + $item1->setStyle($selectableStyle); + $item2 = new SelectableItem("Item 2\nItem 2", $cb, true); + $item2->setStyle($selectableStyle); + + $item = new SplitItem([$item1, $item2]); self::assertEquals( [ diff --git a/test/Style/CheckboxStyleTest.php b/test/Style/CheckboxStyleTest.php index 4321790e..ed8e2fbb 100644 --- a/test/Style/CheckboxStyleTest.php +++ b/test/Style/CheckboxStyleTest.php @@ -4,6 +4,7 @@ namespace PhpSchool\CliMenuTest\Style; +use PhpSchool\CliMenu\MenuItem\CheckboxItem; use PhpSchool\CliMenu\Style\CheckboxStyle; use PHPUnit\Framework\TestCase; @@ -16,10 +17,16 @@ public function testHasChangedFromDefaultsWhenNoStylesChanged() : void public function testGetMarker() : void { + $item = new CheckboxItem('My Checkbox', 'var_dump'); + $item->setChecked(); + $style = new CheckboxStyle; - self::assertSame('[✔] ', $style->getMarker(true)); - self::assertSame('[ ] ', $style->getMarker(false)); + self::assertSame('[✔] ', $style->getMarker($item, false)); + + $item->setUnchecked(); + + self::assertSame('[ ] ', $style->getMarker($item, false)); } public function testGetSetMarkerOn() : void diff --git a/test/Style/DefaultStyleTest.php b/test/Style/DefaultStyleTest.php new file mode 100644 index 00000000..7ec6b069 --- /dev/null +++ b/test/Style/DefaultStyleTest.php @@ -0,0 +1,41 @@ +hasChangedFromDefaults()); + } + + public function testGetMarker() : void + { + $item = new LineBreakItem('X'); + $style = new DefaultStyle; + + self::assertSame('', $style->getMarker($item, false)); + self::assertSame('', $style->getMarker($item, true)); + } + + public function testGetSetItemExtra() : void + { + $style = new DefaultStyle; + + self::assertSame('', $style->getItemExtra()); + } + + + public function testGetSetDisplayExtra() : void + { + $style = new DefaultStyle; + + self::assertFalse($style->getDisplaysExtra()); + } +} diff --git a/test/Style/LocatorTest.php b/test/Style/LocatorTest.php new file mode 100644 index 00000000..8d4d957a --- /dev/null +++ b/test/Style/LocatorTest.php @@ -0,0 +1,189 @@ +getStyle(DefaultStyle::class); + $selectableStyle = $locator->getStyle(SelectableStyle::class); + $checkboxStyle = $locator->getStyle(CheckboxStyle::class); + $radioStyle = $locator->getStyle(RadioStyle::class); + + $selectableStyle->setUnselectedMarker('[ ]'); + $selectableStyle->setSelectedMarker('[X]'); + + $checkboxStyle->setCheckedMarker('[*] '); + $radioStyle->setCheckedMarker('[*] '); + + $otherLocator = new Locator(); + + $locator->importFrom($otherLocator); + + self::assertSame($defaultStyle, $locator->getStyle(DefaultStyle::class)); + self::assertSame($selectableStyle, $locator->getStyle(SelectableStyle::class)); + self::assertSame($checkboxStyle, $locator->getStyle(CheckboxStyle::class)); + self::assertSame($radioStyle, $locator->getStyle(RadioStyle::class)); + } + + public function testImportStylesWhenOneStyleNotModified() : void + { + $locator = new Locator(); + + $defaultStyle = $locator->getStyle(DefaultStyle::class); + $selectableStyle = $locator->getStyle(SelectableStyle::class); + $checkboxStyle = $locator->getStyle(CheckboxStyle::class); + $radioStyle = $locator->getStyle(RadioStyle::class); + + $checkboxStyle->setCheckedMarker('[*] '); + $radioStyle->setCheckedMarker('[*] '); + + $otherLocator = new Locator(); + + $locator->importFrom($otherLocator); + + self::assertSame($defaultStyle, $locator->getStyle(DefaultStyle::class)); + self::assertSame($checkboxStyle, $locator->getStyle(CheckboxStyle::class)); + self::assertSame($radioStyle, $locator->getStyle(RadioStyle::class)); + + self::assertNotSame($selectableStyle, $locator->getStyle(SelectableStyle::class)); + self::assertSame($otherLocator->getStyle(SelectableStyle::class), $locator->getStyle(SelectableStyle::class)); + } + + public function testImportStylesWhenStyleNotModified() : void + { + $locator = new Locator(); + + $selectableStyle = $locator->getStyle(SelectableStyle::class); + $checkboxStyle = $locator->getStyle(CheckboxStyle::class); + $radioStyle = $locator->getStyle(RadioStyle::class); + + $otherLocator = new Locator(); + + $locator->importFrom($otherLocator); + + self::assertNotSame($selectableStyle, $locator->getStyle(SelectableStyle::class)); + self::assertNotSame($checkboxStyle, $locator->getStyle(CheckboxStyle::class)); + self::assertNotSame($radioStyle, $locator->getStyle(RadioStyle::class)); + + self::assertSame($otherLocator->getStyle(SelectableStyle::class), $locator->getStyle(SelectableStyle::class)); + self::assertSame($otherLocator->getStyle(CheckboxStyle::class), $locator->getStyle(CheckboxStyle::class)); + self::assertSame($otherLocator->getStyle(RadioStyle::class), $locator->getStyle(RadioStyle::class)); + } + + public function testGetStyleForMenuItemThrowsExceptionIfItemNotRegistered() : void + { + self::expectException(InvalidStyle::class); + + $myItem = new class extends LineBreakItem { + }; + + $locator = new Locator(); + $locator->getStyleForMenuItem($myItem); + } + + public function itemStyleProvider() : array + { + $action = function () { + }; + + return [ + [DefaultStyle::class, new LineBreakItem()], + [DefaultStyle::class, new StaticItem('*')], + [DefaultStyle::class, new AsciiArtItem('*')], + [SelectableStyle::class, new SelectableItem('1', $action)], + [SelectableStyle::class, new MenuMenuItem('2', new CliMenu('sub', []))], + [CheckboxStyle::class, new CheckboxItem('3', $action)], + [RadioStyle::class, new RadioItem('4', $action)], + ]; + } + + /** + * @dataProvider itemStyleProvider + */ + public function testGetStyleForMenuItem(string $expectedStyleClass, MenuItemInterface $menuItem) : void + { + $locator = new Locator(); + + self::assertInstanceOf($expectedStyleClass, $locator->getStyleForMenuItem($menuItem)); + } + + public function testGetStyleThrowsExceptionIfStyleClassNotRegistered() : void + { + self::expectException(InvalidStyle::class); + + $locator = new Locator(); + $locator->getStyle('NonExistingStyleClass'); + } + + public function styleProvider() : array + { + return [ + [DefaultStyle::class], + [SelectableStyle::class], + [SelectableStyle::class], + [CheckboxStyle::class], + [RadioStyle::class], + ]; + } + + /** + * @dataProvider styleProvider + */ + public function testGetStyle(string $styleClass) : void + { + $locator = new Locator(); + + self::assertInstanceOf($styleClass, $locator->getStyle($styleClass)); + } + + public function testSetStyleThrowsExceptionIfStyleClassNotRegistered() : void + { + self::expectException(InvalidStyle::class); + + $locator = new Locator(); + $locator->setStyle(new DefaultStyle(), 'NonExistingStyleClass'); + } + + public function testSetStyleThrowsExceptionIfStyleNotInstanceOfStyleClass() : void + { + self::expectException(InvalidStyle::class); + + $invalidStyle = new class extends SelectableStyle { + }; + + $locator = new Locator(); + $locator->setStyle($invalidStyle, DefaultStyle::class); + } + + public function testSetStyle() : void + { + $locator = new Locator(); + + $locator->setStyle($new = new DefaultStyle(), DefaultStyle::class); + + self::assertSame($new, $locator->getStyle(DefaultStyle::class)); + } +} diff --git a/test/Style/RadioStyleTest.php b/test/Style/RadioStyleTest.php index 9edf2804..4630c63b 100644 --- a/test/Style/RadioStyleTest.php +++ b/test/Style/RadioStyleTest.php @@ -4,6 +4,7 @@ namespace PhpSchool\CliMenuTest\Style; +use PhpSchool\CliMenu\MenuItem\RadioItem; use PhpSchool\CliMenu\Style\RadioStyle; use PHPUnit\Framework\TestCase; @@ -16,10 +17,16 @@ public function testHasChangedFromDefaultsWhenNoStylesChanged() : void public function testGetMarker() : void { + $item = new RadioItem('My Radio', 'var_dump'); + $item->setChecked(); + $style = new RadioStyle; - self::assertSame('[●] ', $style->getMarker(true)); - self::assertSame('[○] ', $style->getMarker(false)); + self::assertSame('[●] ', $style->getMarker($item, false)); + + $item->setUnchecked(); + + self::assertSame('[○] ', $style->getMarker($item, false)); } public function testGetSetMarkerOn() : void diff --git a/test/Style/SelectableStyleTest.php b/test/Style/SelectableStyleTest.php index 27c4c439..d8311b6f 100644 --- a/test/Style/SelectableStyleTest.php +++ b/test/Style/SelectableStyleTest.php @@ -4,6 +4,7 @@ namespace PhpSchool\CliMenuTest\Style; +use PhpSchool\CliMenu\MenuItem\SelectableItem; use PhpSchool\CliMenu\Style\SelectableStyle; use PHPUnit\Framework\TestCase; @@ -16,10 +17,11 @@ public function testHasChangedFromDefaultsWhenNoStylesChanged() : void public function testGetMarker() : void { + $item = new SelectableItem('My Item', 'var_dump'); $style = new SelectableStyle; - self::assertSame('● ', $style->getMarker(true)); - self::assertSame('○ ', $style->getMarker(false)); + self::assertSame('● ', $style->getMarker($item, true)); + self::assertSame('○ ', $style->getMarker($item, false)); } public function testGetSetMarkerOn() : void diff --git a/test/Util/ArrayUtilTest.php b/test/Util/ArrayUtilTest.php index 895f2bdf..34e349d1 100644 --- a/test/Util/ArrayUtilTest.php +++ b/test/Util/ArrayUtilTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use function PhpSchool\CliMenu\Util\each; use function PhpSchool\CliMenu\Util\mapWithKeys; +use function PhpSchool\CliMenu\Util\max; class ArrayUtilTest extends TestCase { @@ -47,4 +48,11 @@ public function testEach() : void each([1, 2, 3], $cb); self::assertEquals(3, $i); } + + public function testMax() : void + { + self::assertEquals(0, max([])); + self::assertEquals(3, max([1, 2, 3])); + self::assertEquals(6, max([1, 6, 3])); + } }