Completed
Push — master ( df8184...e3b994 )
by Aydin
02:31
created

CliMenuBuilder::end()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
c 0
b 0
f 0
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
1
<?php
2
3
namespace PhpSchool\CliMenu\Builder;
4
5
use PhpSchool\CliMenu\Action\ExitAction;
6
use PhpSchool\CliMenu\Action\GoBackAction;
7
use PhpSchool\CliMenu\MenuItem\AsciiArtItem;
8
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
9
use PhpSchool\CliMenu\MenuItem\SelectableItem;
10
use PhpSchool\CliMenu\MenuItem\SplitItem;
11
use PhpSchool\CliMenu\CliMenu;
12
use PhpSchool\CliMenu\MenuStyle;
13
use PhpSchool\CliMenu\Terminal\TerminalFactory;
14
use PhpSchool\CliMenu\Util\ColourUtil;
15
use PhpSchool\Terminal\Terminal;
16
use RuntimeException;
17
18
/**
19
 * @author Michael Woodward <[email protected]>
20
 * @author Aydin Hassan <[email protected]>
21
 */
22
class CliMenuBuilder implements Builder
23
{
24
    use BuilderUtils;
25
26
    /**
27
     * @var null|Builder
28
     */
29
    private $parent;
30
    
31
    /**
32
     * @var bool
33
     */
34
    private $isBuilt = false;
35
36
    /**
37
     * @var SplitItemBuilder[]
38
     */
39
    private $splitItemBuilders = [];
40
41
    /**
42
     * @var SplitItem[]
43
     */
44
    private $splitItems = [];
45
46
    /**
47
     * @var string
48
     */
49
    private $goBackButtonText = 'Go Back';
50
51
    /**
52
     * @var string
53
     */
54
    private $exitButtonText = 'Exit';
55
56
    /**
57
     * @var array
58
     */
59
    private $style;
60
61
    /**
62
     * @var Terminal
63
     */
64
    private $terminal;
65
66
    /**
67
     * @var string
68
     */
69
    private $menuTitle;
70
71
    /**
72
     * @var bool
73
     */
74
    private $disableDefaultItems = false;
75
76
    /**
77
     * @var bool
78
     */
79
    private $disabled = false;
80
81
    public function __construct(Builder $parent = null)
82
    {
83
        $this->parent   = $parent;
84
        $this->terminal = $this->parent !== null
85
            ? $this->parent->getTerminal()
86
            : TerminalFactory::fromSystem();
87
        $this->style = MenuStyle::getDefaultStyleValues();
88
    }
89
90
    public function setTitle(string $title) : self
91
    {
92
        $this->menuTitle = $title;
93
94
        return $this;
95
    }
96
97
    public function addMenuItem(MenuItemInterface $item) : self
98
    {
99
        $this->menuItems[] = $item;
100
101
        return $this;
102
    }
103
104
    public function addItems(array $items) : self
105
    {
106
        foreach ($items as $item) {
107
            $this->addItem(...$item);
0 ignored issues
show
Bug introduced by Aydin Hassan
The call to addItem() misses a required argument $itemCallable.

This check looks for function calls that miss required arguments.

Loading history...
108
        }
109
110
        return $this;
111
    }
112
113
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
114
    {
115
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
116
117
        return $this;
118
    }
119
120
    /**
121
     * Add a split item
122
     */
123
    public function addSplitItem() : SplitItemBuilder
124
    {
125
        $this->menuItems[] = [
126
            'type' => 'splititem-placeholder',
127
            'id'   => $id = uniqid('', true),
128
        ];
129
                
130
        $this->splitItemBuilders[$id] = new SplitItemBuilder($this);
131
        return $this->splitItemBuilders[$id];
132
    }
133
134
    /**
135
     * Disable a submenu
136
     *
137
     * @throws \InvalidArgumentException
138
     */
139
    public function disableMenu() : self
140
    {
141
        if (!$this->parent) {
142
            throw new \InvalidArgumentException(
143
                'You can\'t disable the root menu'
144
            );
145
        }
146
147
        $this->disabled = true;
148
149
        return $this;
150
    }
151
152
    public function isMenuDisabled() : bool
153
    {
154
        return $this->disabled;
155
    }
156
157
    public function setGoBackButtonText(string $goBackButtonTest) : self
158
    {
159
        $this->goBackButtonText = $goBackButtonTest;
160
161
        return $this;
162
    }
163
164
    public function setExitButtonText(string $exitButtonText) : self
165
    {
166
        $this->exitButtonText = $exitButtonText;
167
168
        return $this;
169
    }
170
171
    public function setBackgroundColour(string $colour, string $fallback = null) : self
172
    {
173
        $this->style['bg'] = ColourUtil::validateColour(
174
            $this->terminal,
175
            $colour,
176
            $fallback
177
        );
178
179
        return $this;
180
    }
181
182
    public function setForegroundColour(string $colour, string $fallback = null) : self
183
    {
184
        $this->style['fg'] = ColourUtil::validateColour(
185
            $this->terminal,
186
            $colour,
187
            $fallback
188
        );
189
190
        return $this;
191
    }
192
193
    public function setWidth(int $width) : self
194
    {
195
        $this->style['width'] = $width;
196
197
        return $this;
198
    }
199
200
    public function setPadding(int $topBottom, int $leftRight = null) : self
201
    {
202
        if ($leftRight === null) {
203
            $leftRight = $topBottom;
204
        }
205
206
        $this->setPaddingTopBottom($topBottom);
207
        $this->setPaddingLeftRight($leftRight);
208
209
        return $this;
210
    }
211
212
    public function setPaddingTopBottom(int $topBottom) : self
213
    {
214
        $this->style['paddingTopBottom'] = $topBottom;
215
216
        return $this;
217
    }
218
219
    public function setPaddingLeftRight(int $leftRight) : self
220
    {
221
        $this->style['paddingLeftRight'] = $leftRight;
222
223
        return $this;
224
    }
225
226
    public function setMarginAuto() : self
227
    {
228
        $this->style['marginAuto'] = true;
229
230
        return $this;
231
    }
232
233
    public function setMargin(int $margin) : self
234
    {
235
        $this->style['marginAuto'] = false;
236
        $this->style['margin'] = $margin;
237
238
        return $this;
239
    }
240
241
    public function setUnselectedMarker(string $marker) : self
242
    {
243
        $this->style['unselectedMarker'] = $marker;
244
245
        return $this;
246
    }
247
248
    public function setSelectedMarker(string $marker) : self
249
    {
250
        $this->style['selectedMarker'] = $marker;
251
252
        return $this;
253
    }
254
255
    public function setItemExtra(string $extra) : self
256
    {
257
        $this->style['itemExtra'] = $extra;
258
259
        return $this;
260
    }
261
262
    public function setTitleSeparator(string $separator) : self
263
    {
264
        $this->style['titleSeparator'] = $separator;
265
266
        return $this;
267
    }
268
269
    public function setBorder(
270
        int $topWidth,
271
        $rightWidth = null,
272
        $bottomWidth = null,
273
        $leftWidth = null,
274
        string $colour = null
275
    ) : self {
276
        if (!is_int($rightWidth)) {
277
            $colour = $rightWidth;
278
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
279
        } elseif (!is_int($bottomWidth)) {
280
            $colour = $bottomWidth;
281
            $bottomWidth = $topWidth;
282
            $leftWidth = $rightWidth;
283
        } elseif (!is_int($leftWidth)) {
284
            $colour = $leftWidth;
285
            $leftWidth = $rightWidth;
286
        }
287
288
        $this->style['borderTopWidth'] = $topWidth;
289
        $this->style['borderRightWidth'] = $rightWidth;
290
        $this->style['borderBottomWidth'] = $bottomWidth;
291
        $this->style['borderLeftWidth'] = $leftWidth;
292
293
        if (is_string($colour)) {
294
            $this->style['borderColour'] = $colour;
295
        } elseif ($colour !== null) {
296
            throw new \InvalidArgumentException('Invalid colour');
297
        }
298
299
        return $this;
300
    }
301
302
    public function setBorderTopWidth(int $width) : self
303
    {
304
        $this->style['borderTopWidth'] = $width;
305
        
306
        return $this;
307
    }
308
309
    public function setBorderRightWidth(int $width) : self
310
    {
311
        $this->style['borderRightWidth'] = $width;
312
313
        return $this;
314
    }
315
316
    public function setBorderBottomWidth(int $width) : self
317
    {
318
        $this->style['borderBottomWidth'] = $width;
319
320
        return $this;
321
    }
322
323
    public function setBorderLeftWidth(int $width) : self
324
    {
325
        $this->style['borderLeftWidth'] = $width;
326
327
        return $this;
328
    }
329
330
    public function setBorderColour(string $colour, $fallback = null) : self
331
    {
332
        $this->style['borderColour'] = $colour;
333
        $this->style['borderColourFallback'] = $fallback;
334
335
        return $this;
336
    }
337
338
    public function setTerminal(Terminal $terminal) : self
339
    {
340
        $this->terminal = $terminal;
341
        return $this;
342
    }
343
344
    public function getTerminal() : Terminal
345
    {
346
        return $this->terminal;
347
    }
348
349
    private function getDefaultItems() : array
350
    {
351
        $actions = [];
352
        if ($this->parent) {
353
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
354
        }
355
356
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
357
        return $actions;
358
    }
359
360
    public function disableDefaultItems() : self
361
    {
362
        $this->disableDefaultItems = true;
363
364
        return $this;
365
    }
366
367
    private function itemsHaveExtra(array $items) : bool
368
    {
369
        return !empty(array_filter($items, function (MenuItemInterface $item) {
370
            return $item->showsItemExtra();
371
        }));
372
    }
373
374
    /**
375
     * Recursively drop back to the parents menu style
376
     * when the current menu has a parent and has no changes
377
     */
378
    public function getMenuStyle() : MenuStyle
379
    {
380
        if (null === $this->parent) {
381
            return $this->buildStyle();
382
        }
383
384
        if ($this->style !== MenuStyle::getDefaultStyleValues()) {
385
            return $this->buildStyle();
386
        }
387
388
        return $this->parent->getMenuStyle();
389
    }
390
391
    private function buildStyle() : MenuStyle
392
    {
393
        $style = (new MenuStyle($this->terminal))
394
            ->setFg($this->style['fg'])
395
            ->setBg($this->style['bg'])
396
            ->setWidth($this->style['width'])
397
            ->setPaddingTopBottom($this->style['paddingTopBottom'])
398
            ->setPaddingLeftRight($this->style['paddingLeftRight'])
399
            ->setSelectedMarker($this->style['selectedMarker'])
400
            ->setUnselectedMarker($this->style['unselectedMarker'])
401
            ->setItemExtra($this->style['itemExtra'])
402
            ->setDisplaysExtra($this->style['displaysExtra'])
403
            ->setTitleSeparator($this->style['titleSeparator'])
404
            ->setBorderTopWidth($this->style['borderTopWidth'])
405
            ->setBorderRightWidth($this->style['borderRightWidth'])
406
            ->setBorderBottomWidth($this->style['borderBottomWidth'])
407
            ->setBorderLeftWidth($this->style['borderLeftWidth'])
408
            ->setBorderColour($this->style['borderColour'], $this->style['borderColourFallback']);
409
410
        $this->style['marginAuto'] ? $style->setMarginAuto() : $style->setMargin($this->style['margin']);
411
412
        return $style;
413
    }
414
415
    /**
416
     * @throws RuntimeException
417
     */
418
    public function getSubMenu(string $id) : CliMenu
419
    {
420
        if (false === $this->isBuilt) {
421
            throw new RuntimeException(sprintf('Menu: "%s" cannot be retrieved until menu has been built', $id));
422
        }
423
424
        return $this->subMenus[$id];
425
    }
426
    
427
    private function buildSplitItems(array $items) : array
428
    {
429
        return array_map(function ($item) {
430
            if (!is_array($item) || $item['type'] !== 'splititem-placeholder') {
431
                return $item;
432
            }
433
434
            $splitItemBuilder        = $this->splitItemBuilders[$item['id']];
435
            $this->splitItems[$item['id']] = $splitItemBuilder->build();
436
437
            return $this->splitItems[$item['id']];
438
        }, $items);
439
    }
440
441
    public function build() : CliMenu
442
    {
443
        $this->isBuilt = true;
444
445
        $mergedItems = $this->disableDefaultItems
446
            ? $this->menuItems
447
            : array_merge($this->menuItems, $this->getDefaultItems());
448
449
        
450
        $menuItems = $this->buildSplitItems($mergedItems);
451
        $menuItems = $this->buildSubMenus($menuItems);
452
453
        $this->style['displaysExtra'] = $this->itemsHaveExtra($menuItems);
454
455
        $menu = new CliMenu(
456
            $this->menuTitle,
457
            $menuItems,
458
            $this->terminal,
459
            $this->getMenuStyle()
460
        );
461
462
        foreach ($this->subMenus as $subMenu) {
463
            $subMenu->setParent($menu);
464
        }
465
        
466
        foreach ($this->splitItemBuilders as $splitItemBuilder) {
467
            $splitItemBuilder->setSubMenuParents($menu);
468
        }
469
470
        return $menu;
471
    }
472
473
    /**
474
     * Return to parent builder
475
     *
476
     * @return CliMenuBuilder|SplitItemBuilder
477
     */
478
    public function end() : ?Builder
479
    {
480
        if (null === $this->parent) {
481
            throw new RuntimeException('No parent builder to return to');
482
        }
483
484
        return $this->parent;
485
    }
486
}
487