Skip to content

Commit e2fb565

Browse files
authored
Merge pull request #127 from php-school/split-item-builder
Split item with builder
2 parents ef5e67d + 61c6940 commit e2fb565

19 files changed

+1581
-140
lines changed

examples/split-item.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
use PhpSchool\CliMenu\CliMenu;
4+
use PhpSchool\CliMenu\CliMenuBuilder;
5+
6+
require_once(__DIR__ . '/../vendor/autoload.php');
7+
8+
$itemCallable = function (CliMenu $menu) {
9+
echo $menu->getSelectedItem()->getText();
10+
};
11+
12+
$menu = (new CliMenuBuilder)
13+
->setWidth(150)
14+
->addSplitItem()
15+
->addSubMenu('Sub Menu on a split item')
16+
->setTitle('Behold the awesomeness')
17+
->addItem('This is awesome', function() { print 'Yes!'; })
18+
->addSplitItem()
19+
->addItem('Split Item 1', function() { print 'Item 1!'; })
20+
->addItem('Split Item 2', function() { print 'Item 2!'; })
21+
->addItem('Split Item 3', function() { print 'Item 3!'; })
22+
->addSubMenu('Split Item Nested Sub Menu')
23+
->addItem('One', function() { print 'One!'; })
24+
->addItem('Two', function() { print 'Two!'; })
25+
->addItem('Three', function() { print 'Three!'; })
26+
->end()
27+
->end()
28+
->end()
29+
->addItem('Item 2', $itemCallable)
30+
->addStaticItem('Item 3 - Static')
31+
->addItem('Item 4', $itemCallable)
32+
->end()
33+
->build();
34+
35+
$menu->open();

src/Builder.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace PhpSchool\CliMenu;
4+
5+
use PhpSchool\Terminal\Terminal;
6+
7+
/**
8+
* @author Aydin Hassan <[email protected]>
9+
*/
10+
interface Builder
11+
{
12+
public function getTerminal() : Terminal;
13+
14+
public function end() : ?Builder;
15+
16+
public function getMenuStyle() : MenuStyle;
17+
}

src/BuilderUtils.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
namespace PhpSchool\CliMenu;
4+
5+
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
6+
use PhpSchool\CliMenu\MenuItem\MenuMenuItem;
7+
use PhpSchool\CliMenu\MenuItem\SelectableItem;
8+
use PhpSchool\CliMenu\MenuItem\StaticItem;
9+
use RuntimeException;
10+
11+
/**
12+
* @author Aydin Hassan <[email protected]>
13+
*/
14+
trait BuilderUtils
15+
{
16+
/**
17+
* @var null|Builder
18+
*/
19+
private $parent;
20+
21+
/**
22+
* @var self[]
23+
*/
24+
private $subMenuBuilders = [];
25+
26+
/**
27+
* @var CliMenu[]
28+
*/
29+
private $subMenus = [];
30+
31+
/**
32+
* @var array
33+
*/
34+
private $menuItems = [];
35+
36+
public function addItem(
37+
string $text,
38+
callable $itemCallable,
39+
bool $showItemExtra = false,
40+
bool $disabled = false
41+
) : self {
42+
$this->menuItems[] = new SelectableItem($text, $itemCallable, $showItemExtra, $disabled);
43+
44+
return $this;
45+
}
46+
47+
public function addStaticItem(string $text) : self
48+
{
49+
$this->menuItems[] = new StaticItem($text);
50+
51+
return $this;
52+
}
53+
54+
public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self
55+
{
56+
$this->menuItems[] = new LineBreakItem($breakChar, $lines);
57+
58+
return $this;
59+
}
60+
61+
/**
62+
* Add a submenu with a name. The name will be displayed as the item text
63+
* in the parent menu.
64+
*/
65+
public function addSubMenu(string $name, CliMenuBuilder $subMenuBuilder = null) : Builder
66+
{
67+
$this->menuItems[] = $id = 'submenu-placeholder-' . $name;
68+
69+
if (null === $subMenuBuilder) {
70+
$this->subMenuBuilders[$id] = new CliMenuBuilder($this);
71+
return $this->subMenuBuilders[$id];
72+
}
73+
74+
$this->subMenuBuilders[$id] = $subMenuBuilder;
75+
return $this;
76+
}
77+
78+
private function buildSubMenus(array $items) : array
79+
{
80+
return array_map(function ($item) {
81+
if (!is_string($item) || 0 !== strpos($item, 'submenu-placeholder-')) {
82+
return $item;
83+
}
84+
85+
$menuBuilder = $this->subMenuBuilders[$item];
86+
$this->subMenus[$item] = $menuBuilder->build();
87+
88+
return new MenuMenuItem(
89+
substr($item, \strlen('submenu-placeholder-')),
90+
$this->subMenus[$item],
91+
$menuBuilder->isMenuDisabled()
92+
);
93+
}, $items);
94+
}
95+
96+
/**
97+
* Return to parent builder
98+
*
99+
* @throws RuntimeException
100+
*/
101+
public function end() : ?Builder
102+
{
103+
if (null === $this->parent) {
104+
throw new RuntimeException('No parent builder to return to');
105+
}
106+
107+
return $this->parent;
108+
}
109+
}

src/CliMenu.php

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PhpSchool\CliMenu\Input\Text;
1313
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
1414
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
15+
use PhpSchool\CliMenu\MenuItem\SplitItem;
1516
use PhpSchool\CliMenu\MenuItem\StaticItem;
1617
use PhpSchool\CliMenu\Dialogue\Confirm;
1718
use PhpSchool\CliMenu\Dialogue\Flash;
@@ -256,7 +257,12 @@ private function display() : void
256257
switch ($char->getControl()) {
257258
case InputCharacter::UP:
258259
case InputCharacter::DOWN:
259-
$this->moveSelection($char->getControl());
260+
$this->moveSelectionVertically($char->getControl());
261+
$this->draw();
262+
break;
263+
case InputCharacter::LEFT:
264+
case InputCharacter::RIGHT:
265+
$this->moveSelectionHorizontally($char->getControl());
260266
$this->draw();
261267
break;
262268
case InputCharacter::ENTER:
@@ -269,11 +275,11 @@ private function display() : void
269275
/**
270276
* Move the selection in a given direction, up / down
271277
*/
272-
protected function moveSelection(string $direction) : void
278+
protected function moveSelectionVertically(string $direction) : void
273279
{
274-
do {
275-
$itemKeys = array_keys($this->items);
280+
$itemKeys = array_keys($this->items);
276281

282+
do {
277283
$direction === 'UP'
278284
? $this->selectedItem--
279285
: $this->selectedItem++;
@@ -282,15 +288,65 @@ protected function moveSelection(string $direction) : void
282288
$this->selectedItem = $direction === 'UP'
283289
? end($itemKeys)
284290
: reset($itemKeys);
285-
} elseif ($this->getSelectedItem()->canSelect()) {
286-
return;
287291
}
288-
} while (!$this->getSelectedItem()->canSelect());
292+
} while (!$this->canSelect());
289293
}
290294

295+
/**
296+
* Move the selection in a given direction, left / right
297+
*/
298+
protected function moveSelectionHorizontally(string $direction) : void
299+
{
300+
if (!$this->items[$this->selectedItem] instanceof SplitItem) {
301+
return;
302+
}
303+
304+
/** @var SplitItem $item */
305+
$item = $this->items[$this->selectedItem];
306+
$itemKeys = array_keys($item->getItems());
307+
$selectedItemIndex = $item->getSelectedItemIndex();
308+
309+
do {
310+
$direction === 'LEFT'
311+
? $selectedItemIndex--
312+
: $selectedItemIndex++;
313+
314+
if (!array_key_exists($selectedItemIndex, $item->getItems())) {
315+
$selectedItemIndex = $direction === 'LEFT'
316+
? end($itemKeys)
317+
: reset($itemKeys);
318+
}
319+
} while (!$item->canSelectIndex($selectedItemIndex));
320+
321+
$item->setSelectedItemIndex($selectedItemIndex);
322+
}
323+
324+
/**
325+
* Can the currently selected item actually be selected?
326+
*
327+
* For example:
328+
* selectable item -> yes
329+
* static item -> no
330+
* split item with only static items -> no
331+
* split item with at least one selectable item -> yes
332+
*
333+
* @return bool
334+
*/
335+
private function canSelect() : bool
336+
{
337+
return $this->items[$this->selectedItem]->canSelect();
338+
}
339+
340+
/**
341+
* Retrieve the item the user actually selected
342+
*
343+
*/
291344
public function getSelectedItem() : MenuItemInterface
292345
{
293-
return $this->items[$this->selectedItem];
346+
$item = $this->items[$this->selectedItem];
347+
return $item instanceof SplitItem
348+
? $item->getSelectedItem()
349+
: $item;
294350
}
295351

296352
/**
@@ -385,6 +441,10 @@ protected function draw() : void
385441
protected function drawMenuItem(MenuItemInterface $item, bool $selected = false) : array
386442
{
387443
$rows = $item->getRows($this->style, $selected);
444+
445+
if ($item instanceof SplitItem) {
446+
$selected = false;
447+
}
388448

389449
$invertedColoursSetCode = $selected
390450
? $this->style->getInvertedColoursSetCode()

0 commit comments

Comments
 (0)