Completed
Push — master ( 35f7bb...68920e )
by Aydin
02:04
created

CliMenuBuilder::getMenuStyle()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 0
1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Action\ExitAction;
6
use PhpSchool\CliMenu\Action\GoBackAction;
7
use PhpSchool\CliMenu\MenuItem\AsciiArtItem;
8
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
9
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
10
use PhpSchool\CliMenu\MenuItem\MenuMenuItem;
11
use PhpSchool\CliMenu\MenuItem\SelectableItem;
12
use PhpSchool\CliMenu\MenuItem\StaticItem;
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
23
{
24
    /**
25
     * @var bool
26
     */
27
    private $isBuilt = false;
28
29
    /**
30
     * @var null|self
31
     */
32
    private $parent;
33
34
    /**
35
     * @var self[]
36
     */
37
    private $subMenuBuilders = [];
38
39
    /**
40
     * @var CliMenu[]
41
     */
42
    private $subMenus = [];
43
44
    /**
45
     * @var string
46
     */
47
    private $goBackButtonText = 'Go Back';
48
49
    /**
50
     * @var string
51
     */
52
    private $exitButtonText = 'Exit';
53
54
    /**
55
     * @var array
56
     */
57
    private $menuItems = [];
58
59
    /**
60
     * @var array
61
     */
62
    private $style;
63
64
    /**
65
     * @var Terminal
66
     */
67
    private $terminal;
68
69
    /**
70
     * @var string
71
     */
72
    private $menuTitle = null;
73
74
    /**
75
     * @var bool
76
     */
77
    private $disableDefaultItems = false;
78
79
    /**
80
     * @var bool
81
     */
82
    private $disabled = false;
83
84
    public function __construct(CliMenuBuilder $parent = null)
85
    {
86
        $this->parent   = $parent;
87
        $this->terminal = $this->parent !== null
88
            ? $this->parent->getTerminal()
89
            : TerminalFactory::fromSystem();
90
        $this->style    = MenuStyle::getDefaultStyleValues();
91
    }
92
93
    public function setTitle(string $title) : self
94
    {
95
        $this->menuTitle = $title;
96
97
        return $this;
98
    }
99
100
    public function addMenuItem(MenuItemInterface $item) : self
101
    {
102
        $this->menuItems[] = $item;
103
104
        return $this;
105
    }
106
107
    public function addItem(
108
        string $text,
109
        callable $itemCallable,
110
        bool $showItemExtra = false,
111
        bool $disabled = false
112
    ) : self {
113
        $this->addMenuItem(new SelectableItem($text, $itemCallable, $showItemExtra, $disabled));
114
115
        return $this;
116
    }
117
118
    public function addItems(array $items) : self
119
    {
120
        foreach ($items as $item) {
121
            $this->addItem(...$item);
0 ignored issues
show
Bug introduced by
The call to addItem() misses a required argument $itemCallable.

This check looks for function calls that miss required arguments.

Loading history...
122
        }
123
124
        return $this;
125
    }
126
127
    public function addStaticItem(string $text) : self
128
    {
129
        $this->addMenuItem(new StaticItem($text));
130
131
        return $this;
132
    }
133
134
    public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self
135
    {
136
        $this->addMenuItem(new LineBreakItem($breakChar, $lines));
137
138
        return $this;
139
    }
140
141
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
142
    {
143
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
144
145
        return $this;
146
    }
147
148
    /**
149
     * Add a submenu with a string identifier
150
     */
151
    public function addSubMenu(string $id, CliMenuBuilder $subMenuBuilder = null) : CliMenuBuilder
152
    {
153
        $this->menuItems[]  = $id;
154
155
        if (null === $subMenuBuilder) {
156
            $this->subMenuBuilders[$id] = new static($this);
157
            return $this->subMenuBuilders[$id];
158
        }
159
160
        $this->subMenuBuilders[$id] = $subMenuBuilder;
161
        return $this;
162
    }
163
164
    /**
165
     * Disable a submenu
166
     *
167
     * @throws \InvalidArgumentException
168
     */
169
    public function disableMenu() : self
170
    {
171
        if (!$this->parent) {
172
            throw new \InvalidArgumentException(
173
                'You can\'t disable the root menu'
174
            );
175
        }
176
177
        $this->disabled = true;
178
179
        return $this;
180
    }
181
182
    public function isMenuDisabled() : bool
183
    {
184
        return $this->disabled;
185
    }
186
187
    public function setGoBackButtonText(string $goBackButtonTest) : self
188
    {
189
        $this->goBackButtonText = $goBackButtonTest;
190
191
        return $this;
192
    }
193
194
    public function setExitButtonText(string $exitButtonText) : self
195
    {
196
        $this->exitButtonText = $exitButtonText;
197
198
        return $this;
199
    }
200
201
    public function setBackgroundColour(string $colour, string $fallback = null) : self
202
    {
203
        $this->style['bg'] = ColourUtil::validateColour(
204
            $this->terminal,
205
            $colour,
206
            $fallback
207
        );
208
209
        return $this;
210
    }
211
212 View Code Duplication
    public function setForegroundColour(string $colour, string $fallback = null) : self
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
213
    {
214
        $this->style['fg'] = ColourUtil::validateColour(
215
            $this->terminal,
216
            $colour,
217
            $fallback
218
        );
219
220
        return $this;
221
    }
222
223
    public function setWidth(int $width) : self
224
    {
225
        $this->style['width'] = $width;
226
227
        return $this;
228
    }
229
230 View Code Duplication
    public function setPadding(int $topBottom, int $leftRight = null) : self
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
231
    {
232
        if ($leftRight === null) {
233
            $leftRight = $topBottom;
234
        }
235
236
        $this->setPaddingTopBottom($topBottom);
237
        $this->setPaddingLeftRight($leftRight);
238
239
        return $this;
240
    }
241
242
    public function setPaddingTopBottom(int $topBottom) : self
243
    {
244
        $this->style['paddingTopBottom'] = $topBottom;
245
246
        return $this;
247
    }
248
249
    public function setPaddingLeftRight(int $leftRight) : self
250
    {
251
        $this->style['paddingLeftRight'] = $leftRight;
252
253
        return $this;
254
    }
255
256
    public function setMarginAuto() : self
257
    {
258
        $this->style['marginAuto'] = true;
259
260
        return $this;
261
    }
262
263
    public function setMargin(int $margin) : self
264
    {
265
        $this->style['marginAuto'] = false;
266
        $this->style['margin'] = $margin;
267
268
        return $this;
269
    }
270
271
    public function setUnselectedMarker(string $marker) : self
272
    {
273
        $this->style['unselectedMarker'] = $marker;
274
275
        return $this;
276
    }
277
278
    public function setSelectedMarker(string $marker) : self
279
    {
280
        $this->style['selectedMarker'] = $marker;
281
282
        return $this;
283
    }
284
285
    public function setItemExtra(string $extra) : self
286
    {
287
        $this->style['itemExtra'] = $extra;
288
289
        return $this;
290
    }
291
292
    public function setTitleSeparator(string $separator) : self
293
    {
294
        $this->style['titleSeparator'] = $separator;
295
296
        return $this;
297
    }
298
299
    public function setBorder(
300
        int $topWidth,
301
        $rightWidth = null,
302
        $bottomWidth = null,
303
        $leftWidth = null,
304
        string $colour = null
305
    ) : self {
306 View Code Duplication
        if (!is_int($rightWidth)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
307
            $colour = $rightWidth;
308
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
309
        } elseif (!is_int($bottomWidth)) {
310
            $colour = $bottomWidth;
311
            $bottomWidth = $topWidth;
312
            $leftWidth = $rightWidth;
313
        } elseif (!is_int($leftWidth)) {
314
            $colour = $leftWidth;
315
            $leftWidth = $rightWidth;
316
        }
317
318
        $this->style['borderTopWidth'] = $topWidth;
319
        $this->style['borderRightWidth'] = $rightWidth;
320
        $this->style['borderBottomWidth'] = $bottomWidth;
321
        $this->style['borderLeftWidth'] = $leftWidth;
322
323
        if (is_string($colour)) {
324
            $this->style['borderColour'] = $colour;
325
        } elseif ($colour !== null) {
326
            throw new \InvalidArgumentException('Invalid colour');
327
        }
328
329
        return $this;
330
    }
331
332
    public function setBorderTopWidth(int $width) : self
333
    {
334
        $this->style['borderTopWidth'] = $width;
335
        
336
        return $this;
337
    }
338
339
    public function setBorderRightWidth(int $width) : self
340
    {
341
        $this->style['borderRightWidth'] = $width;
342
343
        return $this;
344
    }
345
346
    public function setBorderBottomWidth(int $width) : self
347
    {
348
        $this->style['borderBottomWidth'] = $width;
349
350
        return $this;
351
    }
352
353
    public function setBorderLeftWidth(int $width) : self
354
    {
355
        $this->style['borderLeftWidth'] = $width;
356
357
        return $this;
358
    }
359
360
    public function setBorderColour(string $colour, $fallback = null) : self
361
    {
362
        $this->style['borderColour'] = $colour;
363
        $this->style['borderColourFallback'] = $fallback;
364
365
        return $this;
366
    }
367
368
    public function setTerminal(Terminal $terminal) : self
369
    {
370
        $this->terminal = $terminal;
371
        return $this;
372
    }
373
374
    public function getTerminal() : Terminal
375
    {
376
        return $this->terminal;
377
    }
378
379
    private function getDefaultItems() : array
380
    {
381
        $actions = [];
382
        if ($this->parent) {
383
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
384
        }
385
386
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
387
        return $actions;
388
    }
389
390
    public function disableDefaultItems() : self
391
    {
392
        $this->disableDefaultItems = true;
393
394
        return $this;
395
    }
396
397
    private function itemsHaveExtra(array $items) : bool
398
    {
399
        return !empty(array_filter($items, function (MenuItemInterface $item) {
400
            return $item->showsItemExtra();
401
        }));
402
    }
403
404
    /**
405
     * Recursively drop back to the parents menu style
406
     * when the current menu has a parent and has no changes
407
     */
408
    private function getMenuStyle() : MenuStyle
409
    {
410
        if (null === $this->parent) {
411
            return $this->buildStyle();
412
        }
413
414
        if ($this->style !== MenuStyle::getDefaultStyleValues()) {
415
            return $this->buildStyle();
416
        }
417
418
        return $this->parent->getMenuStyle();
419
    }
420
421
    private function buildStyle() : MenuStyle
422
    {
423
        $style = (new MenuStyle($this->terminal))
424
            ->setFg($this->style['fg'])
425
            ->setBg($this->style['bg'])
426
            ->setWidth($this->style['width'])
427
            ->setPaddingTopBottom($this->style['paddingTopBottom'])
428
            ->setPaddingLeftRight($this->style['paddingLeftRight'])
429
            ->setSelectedMarker($this->style['selectedMarker'])
430
            ->setUnselectedMarker($this->style['unselectedMarker'])
431
            ->setItemExtra($this->style['itemExtra'])
432
            ->setDisplaysExtra($this->style['displaysExtra'])
433
            ->setTitleSeparator($this->style['titleSeparator'])
434
            ->setBorderTopWidth($this->style['borderTopWidth'])
435
            ->setBorderRightWidth($this->style['borderRightWidth'])
436
            ->setBorderBottomWidth($this->style['borderBottomWidth'])
437
            ->setBorderLeftWidth($this->style['borderLeftWidth'])
438
            ->setBorderColour($this->style['borderColour'], $this->style['borderColourFallback']);
439
440
        $this->style['marginAuto'] ? $style->setMarginAuto() : $style->setMargin($this->style['margin']);
441
442
        return $style;
443
    }
444
445
    /**
446
     * Return to parent builder
447
     *
448
     * @throws RuntimeException
449
     */
450
    public function end() : CliMenuBuilder
451
    {
452
        if (null === $this->parent) {
453
            throw new RuntimeException('No parent builder to return to');
454
        }
455
456
        return $this->parent;
457
    }
458
459
    /**
460
     * @throws RuntimeException
461
     */
462
    public function getSubMenu(string $id) : CliMenu
463
    {
464
        if (false === $this->isBuilt) {
465
            throw new RuntimeException(sprintf('Menu: "%s" cannot be retrieved until menu has been built', $id));
466
        }
467
468
        return $this->subMenus[$id];
469
    }
470
471
    private function buildSubMenus(array $items) : array
472
    {
473
        return array_map(function ($item) {
474
            if (!is_string($item)) {
475
                return $item;
476
            }
477
478
            $menuBuilder           = $this->subMenuBuilders[$item];
479
            $this->subMenus[$item] = $menuBuilder->build();
480
481
            return new MenuMenuItem($item, $this->subMenus[$item], $menuBuilder->isMenuDisabled());
482
        }, $items);
483
    }
484
485
    public function build() : CliMenu
486
    {
487
        $this->isBuilt = true;
488
489
        $mergedItems = $this->disableDefaultItems
490
            ? $this->menuItems
491
            : array_merge($this->menuItems, $this->getDefaultItems());
492
493
        $menuItems = $this->buildSubMenus($mergedItems);
494
495
        $this->style['displaysExtra'] = $this->itemsHaveExtra($menuItems);
496
497
        $menu = new CliMenu(
498
            $this->menuTitle,
499
            $menuItems,
500
            $this->terminal,
501
            $this->getMenuStyle()
502
        );
503
504
        foreach ($this->subMenus as $subMenu) {
505
            $subMenu->setParent($menu);
506
        }
507
508
        return $menu;
509
    }
510
}
511