From e1f5a9ee28cb9a82ab44c7307613b8338546910e Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Fri, 20 Dec 2019 02:20:26 -0600 Subject: [PATCH 1/4] Selectable item style --- README.md | 19 +++-- examples/custom-styles.php | 7 +- examples/item-extra-toggling.php | 6 +- src/Builder/CliMenuBuilder.php | 44 ++++++++-- src/CliMenu.php | 31 ++++++-- src/MenuItem/SelectableItem.php | 93 +++++++++++++++++++++- src/Style/SelectableStyle.php | 115 +++++++++++++++++++++++++++ test/MenuItem/SelectableItemTest.php | 30 +++---- 8 files changed, 304 insertions(+), 41 deletions(-) create mode 100644 src/Style/SelectableStyle.php diff --git a/README.md b/README.md index dc5e1ef4..6faff903 100644 --- a/README.md +++ b/README.md @@ -801,13 +801,17 @@ markers only display on *selectable* items, which are: `\PhpSchool\CliMenu\MenuI setUnselectedMarker('❅ ') - ->setSelectedMarker('✏ ') - - //disable unselected marker - ->setUnselectedMarker('') + ->modifySelectableStyle(function (SelectableStyle $style) { + $style->setUnselectedMarker('❅ ') + ->setSelectedMarker('✏ ') + + // disable unselected marker + ->setUnselectedMarker('') + ; + }) ->build(); ``` @@ -858,9 +862,12 @@ The third parameter to `addItem` is a boolean whether to show the item extra or use PhpSchool\CliMenu\Builder\CliMenuBuilder; use PhpSchool\CliMenu\CliMenu; +use PhpSchool\CliMenu\Style\SelectableStyle; $menu = (new CliMenuBuilder) - ->setItemExtra('✔') + ->modifySelectableStyle(function (SelectableStyle $style) { + $style->setItemExtra('✔'); + }) ->addItem('Exercise 1', function (CliMenu $menu) { echo 'I am complete!'; }, true) ->build(); ``` diff --git a/examples/custom-styles.php b/examples/custom-styles.php index 1e36b550..933fc94c 100644 --- a/examples/custom-styles.php +++ b/examples/custom-styles.php @@ -2,6 +2,7 @@ use PhpSchool\CliMenu\CliMenu; use PhpSchool\CliMenu\Builder\CliMenuBuilder; +use PhpSchool\CliMenu\Style\SelectableStyle; require_once(__DIR__ . '/../vendor/autoload.php'); @@ -20,9 +21,11 @@ ->setPadding(4) ->setMargin(4) ->setBorder(1, 2, 'red') - ->setUnselectedMarker(' ') - ->setSelectedMarker('>') ->setTitleSeparator('- ') + ->modifySelectableStyle(function (SelectableStyle $style) { + $style->setUnselectedMarker(' ') + ->setSelectedMarker('>'); + }) ->build(); $menu->open(); diff --git a/examples/item-extra-toggling.php b/examples/item-extra-toggling.php index 87a503ed..994b3765 100644 --- a/examples/item-extra-toggling.php +++ b/examples/item-extra-toggling.php @@ -2,6 +2,7 @@ use PhpSchool\CliMenu\CliMenu; use PhpSchool\CliMenu\Builder\CliMenuBuilder; +use PhpSchool\CliMenu\Style\SelectableStyle; require_once(__DIR__ . '/../vendor/autoload.php'); @@ -20,8 +21,9 @@ ->addItem('First Item', $itemCallable) ->addItem('Second Item', $itemCallable) ->addItem('Third Item', $itemCallable) - ->setItemExtra('[COMPLETE!]') - ->displayExtra() + ->modifySelectableStyle(function (SelectableStyle $style) { + $style->setItemExtra('[COMPLETE!]'); + }) ->addLineBreak('-') ->build(); diff --git a/src/Builder/CliMenuBuilder.php b/src/Builder/CliMenuBuilder.php index 1ea00682..59f7deb2 100644 --- a/src/Builder/CliMenuBuilder.php +++ b/src/Builder/CliMenuBuilder.php @@ -18,6 +18,7 @@ use PhpSchool\CliMenu\MenuStyle; use PhpSchool\CliMenu\Style\CheckboxStyle; use PhpSchool\CliMenu\Style\RadioStyle; +use PhpSchool\CliMenu\Style\SelectableStyle; use PhpSchool\CliMenu\Terminal\TerminalFactory; use PhpSchool\Terminal\Terminal; @@ -89,12 +90,12 @@ public function __construct(Terminal $terminal = null) $this->style = new MenuStyle($this->terminal); $this->menu = new CliMenu(null, [], $this->terminal, $this->style); } - + public static function newSubMenu(Terminal $terminal) : self { $instance = new self($terminal); $instance->subMenu = true; - + return $instance; } @@ -396,6 +397,7 @@ public function setMargin(int $margin) : self public function setUnselectedMarker(string $marker) : self { $this->style->setUnselectedMarker($marker); + $this->menu->getSelectableStyle()->setUnselectedMarker($marker); return $this; } @@ -403,6 +405,7 @@ public function setUnselectedMarker(string $marker) : self public function setSelectedMarker(string $marker) : self { $this->style->setSelectedMarker($marker); + $this->menu->getSelectableStyle()->setSelectedMarker($marker); return $this; } @@ -410,8 +413,9 @@ public function setSelectedMarker(string $marker) : self public function setItemExtra(string $extra) : self { $this->style->setItemExtra($extra); + $this->menu->getSelectableStyle()->setItemExtra($extra); - //if we customise item extra, it means we most likely want to display it + // if we customise item extra, it means we most likely want to display it $this->displayExtra(); return $this; @@ -434,7 +438,7 @@ public function setBorder(int $top, $right = null, $bottom = null, $left = null, public function setBorderTopWidth(int $width) : self { $this->style->setBorderTopWidth($width); - + return $this; } @@ -497,6 +501,7 @@ public function disableDefaultItems() : self public function displayExtra() : self { $this->style->setDisplaysExtra(true); + $this->menu->getSelectableStyle()->setDisplaysExtra(true); return $this; } @@ -507,7 +512,7 @@ private function itemsHaveExtra(array $items) : bool return $item->showsItemExtra(); })); } - + public function build() : CliMenu { if (!$this->disableDefaultItems) { @@ -563,6 +568,25 @@ public function modifyRadioStyle(callable $itemCallable) : self return $this; } + public function getSelectableStyle() : SelectableStyle + { + return $this->menu->getSelectableStyle(); + } + + public function setSelectableStyle(SelectableStyle $style) : self + { + $this->menu->setSelectableStyle($style); + + return $this; + } + + public function modifySelectableStyle(callable $itemCallable) : self + { + $itemCallable($this->menu->getSelectableStyle()); + + return $this; + } + /** * Pass styles from current menu to sub-menu * only if sub-menu style has not be customized @@ -584,6 +608,12 @@ private function propagateStyles(CliMenu $menu, array $items = []) $item->setStyle(clone $menu->getRadioStyle()); } + if ($item instanceof SelectableItem + && !$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(); @@ -600,6 +630,10 @@ private function propagateStyles(CliMenu $menu, array $items = []) $subMenu->setRadioStyle(clone $menu->getRadioStyle()); } + if (!$subMenu->getSelectableStyle()->hasChangedFromDefaults()) { + $subMenu->setSelectableStyle(clone $menu->getSelectableStyle()); + } + $this->propagateStyles($subMenu); } diff --git a/src/CliMenu.php b/src/CliMenu.php index 1a15bc08..82535a78 100644 --- a/src/CliMenu.php +++ b/src/CliMenu.php @@ -16,6 +16,7 @@ use PhpSchool\CliMenu\Dialogue\Flash; use PhpSchool\CliMenu\Style\CheckboxStyle; use PhpSchool\CliMenu\Style\RadioStyle; +use PhpSchool\CliMenu\Style\SelectableStyle; use PhpSchool\CliMenu\Terminal\TerminalFactory; use PhpSchool\CliMenu\Util\StringUtil as s; use PhpSchool\Terminal\InputCharacter; @@ -47,6 +48,11 @@ class CliMenu */ private $radioStyle; + /** + * @var SelectableStyle + */ + private $selectableStyle; + /** * @var ?string */ @@ -102,12 +108,13 @@ public function __construct( Terminal $terminal = null, MenuStyle $style = null ) { - $this->title = $title; - $this->items = $items; - $this->terminal = $terminal ?: TerminalFactory::fromSystem(); - $this->style = $style ?: new MenuStyle($this->terminal); - $this->checkboxStyle = new CheckboxStyle(); - $this->radioStyle = new RadioStyle(); + $this->title = $title; + $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->selectFirstItem(); } @@ -678,6 +685,18 @@ public function setRadioStyle(RadioStyle $style) : self return $this; } + public function getSelectableStyle() : SelectableStyle + { + return $this->selectableStyle; + } + + public function setSelectableStyle(SelectableStyle $style) : self + { + $this->selectableStyle = $style; + + return $this; + } + public function getCurrentFrame() : Frame { return $this->currentFrame; diff --git a/src/MenuItem/SelectableItem.php b/src/MenuItem/SelectableItem.php index 6b58f81c..edc97048 100644 --- a/src/MenuItem/SelectableItem.php +++ b/src/MenuItem/SelectableItem.php @@ -2,18 +2,31 @@ namespace PhpSchool\CliMenu\MenuItem; +use PhpSchool\CliMenu\MenuStyle; +use PhpSchool\CliMenu\Util\StringUtil; +use PhpSchool\CliMenu\Style\SelectableStyle; + /** * @author Michael Woodward */ class SelectableItem implements MenuItemInterface { - use SelectableTrait; - /** * @var callable */ private $selectAction; + private $text = ''; + + private $showItemExtra = false; + + private $disabled = false; + + /** + * @var SelectableStyle; + */ + private $style; + public function __construct( string $text, callable $selectAction, @@ -24,6 +37,41 @@ public function __construct( $this->selectAction = $selectAction; $this->showItemExtra = $showItemExtra; $this->disabled = $disabled; + + $this->style = new SelectableStyle(); + } + + /** + * The output text for the item + */ + public function getRows(MenuStyle $style, bool $selected = false) : array + { + $marker = sprintf("%s", $this->style->getMarker($selected)); + + $length = $this->style->getDisplaysExtra() + ? $style->getContentWidth() - (mb_strlen($this->style->getItemExtra()) + 2) + : $style->getContentWidth(); + + $rows = explode( + "\n", + StringUtil::wordwrap( + sprintf('%s%s', $marker, $this->text), + $length, + sprintf("\n%s", str_repeat(' ', mb_strlen($marker))) + ) + ); + + return array_map(function ($row, $key) use ($style, $length) { + $text = $this->disabled ? $style->getDisabledItemText($row) : $row; + + if ($key === 0) { + return $this->showItemExtra + ? sprintf('%s%s %s', $text, str_repeat(' ', $length - mb_strlen($row)), $this->style->getItemExtra()) + : $text; + } + + return $text; + }, $rows, array_keys($rows)); } /** @@ -34,6 +82,18 @@ public function getSelectAction() : ?callable return $this->selectAction; } + public function getStyle() : SelectableStyle + { + return $this->style; + } + + public function setStyle(SelectableStyle $style) : self + { + $this->style = $style; + + return $this; + } + /** * Return the raw string of text */ @@ -49,4 +109,33 @@ public function setText(string $text) : void { $this->text = $text; } + + /** + * Can the item be selected + */ + public function canSelect() : bool + { + return !$this->disabled; + } + + public function showsItemExtra() : bool + { + return $this->showItemExtra; + } + + /** + * Enable showing item extra + */ + public function showItemExtra() : void + { + $this->showItemExtra = true; + } + + /** + * Disable showing item extra + */ + public function hideItemExtra() : void + { + $this->showItemExtra = false; + } } diff --git a/src/Style/SelectableStyle.php b/src/Style/SelectableStyle.php new file mode 100644 index 00000000..f9cd0961 --- /dev/null +++ b/src/Style/SelectableStyle.php @@ -0,0 +1,115 @@ + '● ', + 'unselectedMarker' => '○ ', + 'itemExtra' => '✔', + 'displaysExtra' => false, + ]; + + /** + * @var string + */ + private $selectedMarker; + + /** + * @var string + */ + private $unselectedMarker; + + /** + * @var string + */ + private $itemExtra; + + /** + * @var bool + */ + private $displaysExtra; + + /** + * @var bool + */ + private $custom = false; + + public function __construct() + { + $this->selectedMarker = self::DEFAULT_STYLES['selectedMarker']; + $this->unselectedMarker = self::DEFAULT_STYLES['unselectedMarker']; + $this->itemExtra = self::DEFAULT_STYLES['itemExtra']; + $this->displaysExtra = self::DEFAULT_STYLES['displaysExtra']; + } + + public function hasChangedFromDefaults() : bool + { + return $this->custom; + } + + public function getMarker(bool $selected) : string + { + return $selected ? $this->selectedMarker : $this->unselectedMarker; + } + + public function getSelectedMarker() : string + { + return $this->selectedMarker; + } + + public function setSelectedMarker(string $marker) : self + { + $this->custom = true; + + $this->selectedMarker = $marker; + + return $this; + } + + public function getUnselectedMarker() : string + { + return $this->unselectedMarker; + } + + public function setUnselectedMarker(string $marker) : self + { + $this->custom = true; + + $this->unselectedMarker = $marker; + + return $this; + } + + public function getItemExtra() : string + { + return $this->itemExtra; + } + + public function setItemExtra(string $itemExtra) : self + { + $this->custom = true; + + $this->itemExtra = $itemExtra; + + // if we customise item extra, it means we most likely want to display it + $this->setDisplaysExtra(true); + + return $this; + } + + public function getDisplaysExtra() : bool + { + return $this->displaysExtra; + } + + public function setDisplaysExtra(bool $displaysExtra) : self + { + $this->custom = true; + + $this->displaysExtra = $displaysExtra; + + return $this; + } +} diff --git a/test/MenuItem/SelectableItemTest.php b/test/MenuItem/SelectableItemTest.php index c78e9fec..be9eda1d 100644 --- a/test/MenuItem/SelectableItemTest.php +++ b/test/MenuItem/SelectableItemTest.php @@ -87,14 +87,10 @@ public function testGetRowsWithUnSelectedMarker() : void ->method('getContentWidth') ->will($this->returnValue(10)); - $menuStyle - ->expects($this->exactly(2)) - ->method('getMarker') - ->with(false) - ->will($this->returnValue('* ')); - $item = new SelectableItem('Item', function () { }); + $item->getStyle() + ->setUnselectedMarker('* '); $this->assertEquals(['* Item'], $item->getRows($menuStyle)); $this->assertEquals(['* Item'], $item->getRows($menuStyle, false)); } @@ -108,14 +104,10 @@ public function testGetRowsWithSelectedMarker() : void ->method('getContentWidth') ->will($this->returnValue(10)); - $menuStyle - ->expects($this->once()) - ->method('getMarker') - ->with(true) - ->will($this->returnValue('= ')); - $item = new SelectableItem('Item', function () { }); + $item->getStyle() + ->setSelectedMarker('= '); $this->assertEquals(['= Item'], $item->getRows($menuStyle, true)); } @@ -127,12 +119,13 @@ public function testGetRowsWithItemExtra() : void $menuStyle = new MenuStyle($terminal); $menuStyle->setPaddingLeftRight(0); $menuStyle->setWidth(20); - $menuStyle->setItemExtra('[EXTRA]'); - $menuStyle->setDisplaysExtra(true); - $menuStyle->setUnselectedMarker('* '); $item = new SelectableItem('Item', function () { }, true); + $item->getStyle() + ->setItemExtra('[EXTRA]') + ->setDisplaysExtra(true) + ->setUnselectedMarker('* '); $this->assertEquals(['* Item [EXTRA]'], $item->getRows($menuStyle)); } @@ -144,12 +137,13 @@ public function testGetRowsWithMultipleLinesWithItemExtra() : void $menuStyle = new MenuStyle($terminal); $menuStyle->setPaddingLeftRight(0); $menuStyle->setWidth(20); - $menuStyle->setItemExtra('[EXTRA]'); - $menuStyle->setDisplaysExtra(true); - $menuStyle->setUnselectedMarker('* '); $item = new SelectableItem('LONG ITEM LINE', function () { }, true); + $item->getStyle() + ->setItemExtra('[EXTRA]') + ->setDisplaysExtra(true) + ->setUnselectedMarker('* '); $this->assertEquals( [ "* LONG ITEM [EXTRA]", From 2dc5e1c1d09f485c119e047d12ba2d7de4574ff4 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Fri, 20 Dec 2019 02:27:36 -0600 Subject: [PATCH 2/4] Tests and upstream changes for Selectable style --- src/Style/SelectableStyle.php | 29 ++++++----- test/Style/SelectableStyleTest.php | 81 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 test/Style/SelectableStyleTest.php diff --git a/src/Style/SelectableStyle.php b/src/Style/SelectableStyle.php index f9cd0961..e04cbbba 100644 --- a/src/Style/SelectableStyle.php +++ b/src/Style/SelectableStyle.php @@ -5,10 +5,10 @@ class SelectableStyle { private const DEFAULT_STYLES = [ - 'selectedMarker' => '● ', + 'selectedMarker' => '● ', 'unselectedMarker' => '○ ', - 'itemExtra' => '✔', - 'displaysExtra' => false, + 'itemExtra' => '✔', + 'displaysExtra' => false, ]; /** @@ -38,15 +38,22 @@ class SelectableStyle public function __construct() { - $this->selectedMarker = self::DEFAULT_STYLES['selectedMarker']; + $this->selectedMarker = self::DEFAULT_STYLES['selectedMarker']; $this->unselectedMarker = self::DEFAULT_STYLES['unselectedMarker']; - $this->itemExtra = self::DEFAULT_STYLES['itemExtra']; - $this->displaysExtra = self::DEFAULT_STYLES['displaysExtra']; + $this->itemExtra = self::DEFAULT_STYLES['itemExtra']; + $this->displaysExtra = self::DEFAULT_STYLES['displaysExtra']; } public function hasChangedFromDefaults() : bool { - return $this->custom; + $currentValues = [ + $this->selectedMarker, + $this->unselectedMarker, + $this->itemExtra, + $this->displaysExtra, + ]; + + return $currentValues !== array_values(self::DEFAULT_STYLES); } public function getMarker(bool $selected) : string @@ -61,8 +68,6 @@ public function getSelectedMarker() : string public function setSelectedMarker(string $marker) : self { - $this->custom = true; - $this->selectedMarker = $marker; return $this; @@ -75,8 +80,6 @@ public function getUnselectedMarker() : string public function setUnselectedMarker(string $marker) : self { - $this->custom = true; - $this->unselectedMarker = $marker; return $this; @@ -89,8 +92,6 @@ public function getItemExtra() : string public function setItemExtra(string $itemExtra) : self { - $this->custom = true; - $this->itemExtra = $itemExtra; // if we customise item extra, it means we most likely want to display it @@ -106,8 +107,6 @@ public function getDisplaysExtra() : bool public function setDisplaysExtra(bool $displaysExtra) : self { - $this->custom = true; - $this->displaysExtra = $displaysExtra; return $this; diff --git a/test/Style/SelectableStyleTest.php b/test/Style/SelectableStyleTest.php new file mode 100644 index 00000000..27c4c439 --- /dev/null +++ b/test/Style/SelectableStyleTest.php @@ -0,0 +1,81 @@ +hasChangedFromDefaults()); + } + + public function testGetMarker() : void + { + $style = new SelectableStyle; + + self::assertSame('● ', $style->getMarker(true)); + self::assertSame('○ ', $style->getMarker(false)); + } + + public function testGetSetMarkerOn() : void + { + $style = new SelectableStyle; + + self::assertSame('● ', $style->getSelectedMarker()); + + $style->setSelectedMarker('x '); + + self::assertSame('x ', $style->getSelectedMarker()); + self::assertTrue($style->hasChangedFromDefaults()); + } + + public function testGetSetMarkerOff() : void + { + $style = new SelectableStyle; + + self::assertSame('○ ', $style->getUnselectedMarker()); + + $style->setUnselectedMarker('( ) '); + + self::assertSame('( ) ', $style->getUnselectedMarker()); + self::assertTrue($style->hasChangedFromDefaults()); + } + + public function testGetSetItemExtra() : void + { + $style = new SelectableStyle; + + self::assertSame('✔', $style->getItemExtra()); + + $style->setItemExtra('[!EXTRA]!'); + + self::assertSame('[!EXTRA]!', $style->getItemExtra()); + self::assertTrue($style->hasChangedFromDefaults()); + } + + public function testModifyingItemExtraForcesExtraToBeDisplayedWhenNoItemsDisplayExtra() : void + { + $style = new SelectableStyle; + self::assertFalse($style->getDisplaysExtra()); + + $style->setItemExtra('[!EXTRA]!'); + self::assertTrue($style->getDisplaysExtra()); + } + + public function testGetSetDisplayExtra() : void + { + $style = new SelectableStyle; + + self::assertFalse($style->getDisplaysExtra()); + + $style->setDisplaysExtra(true); + + self::assertTrue($style->getDisplaysExtra()); + self::assertTrue($style->hasChangedFromDefaults()); + } +} From c459f5caf346dddc05e178e86a859edb85095c94 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Fri, 20 Dec 2019 02:30:33 -0600 Subject: [PATCH 3/4] custom no longer needed --- src/Style/RadioStyle.php | 5 ----- src/Style/SelectableStyle.php | 5 ----- 2 files changed, 10 deletions(-) diff --git a/src/Style/RadioStyle.php b/src/Style/RadioStyle.php index d1b78477..f4a7a657 100644 --- a/src/Style/RadioStyle.php +++ b/src/Style/RadioStyle.php @@ -31,11 +31,6 @@ class RadioStyle */ private $displaysExtra; - /** - * @var bool - */ - protected $custom = false; - public function __construct() { $this->checkedMarker = self::DEFAULT_STYLES['checkedMarker']; diff --git a/src/Style/SelectableStyle.php b/src/Style/SelectableStyle.php index e04cbbba..ad78d4d1 100644 --- a/src/Style/SelectableStyle.php +++ b/src/Style/SelectableStyle.php @@ -31,11 +31,6 @@ class SelectableStyle */ private $displaysExtra; - /** - * @var bool - */ - private $custom = false; - public function __construct() { $this->selectedMarker = self::DEFAULT_STYLES['selectedMarker']; From 09b42948d52b603698dffdb71ac8276b26aadbb4 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Fri, 20 Dec 2019 02:35:45 -0600 Subject: [PATCH 4/4] CS fix --- src/MenuItem/SelectableItem.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/MenuItem/SelectableItem.php b/src/MenuItem/SelectableItem.php index edc97048..50bfcd5b 100644 --- a/src/MenuItem/SelectableItem.php +++ b/src/MenuItem/SelectableItem.php @@ -66,7 +66,12 @@ public function getRows(MenuStyle $style, bool $selected = false) : array if ($key === 0) { return $this->showItemExtra - ? sprintf('%s%s %s', $text, str_repeat(' ', $length - mb_strlen($row)), $this->style->getItemExtra()) + ? sprintf( + '%s%s %s', + $text, + str_repeat(' ', $length - mb_strlen($row)), + $this->style->getItemExtra() + ) : $text; }