Passed
Pull Request — master (#216)
by
unknown
02:26
created

CliMenuBuilder::setSelectedMarker()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 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\Exception\InvalidShortcutException;
8
use PhpSchool\CliMenu\MenuItem\AsciiArtItem;
9
use PhpSchool\CliMenu\MenuItem\CheckboxItem;
10
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
11
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
12
use PhpSchool\CliMenu\MenuItem\MenuMenuItem;
13
use PhpSchool\CliMenu\MenuItem\RadioItem;
14
use PhpSchool\CliMenu\MenuItem\SelectableItem;
15
use PhpSchool\CliMenu\CliMenu;
16
use PhpSchool\CliMenu\MenuItem\SelectableStyleInterface;
17
use PhpSchool\CliMenu\MenuItem\SplitItem;
18
use PhpSchool\CliMenu\MenuItem\StaticItem;
19
use PhpSchool\CliMenu\MenuStyle;
20
use PhpSchool\CliMenu\Style\CheckboxStyle;
21
use PhpSchool\CliMenu\Style\RadioStyle;
22
use PhpSchool\CliMenu\Style\SelectableStyle;
23
use PhpSchool\CliMenu\Terminal\TerminalFactory;
24
use PhpSchool\Terminal\Terminal;
25
26
/**
27
 * @author Michael Woodward <[email protected]>
28
 * @author Aydin Hassan <[email protected]>
29
 */
30
class CliMenuBuilder
31
{
32
    /**
33
     * @var CliMenu
34
     */
35
    private $menu;
36
37
    /**
38
     * @var string
39
     */
40
    private $goBackButtonText = 'Go Back';
41
42
    /**
43
     * @var string
44
     */
45
    private $exitButtonText = 'Exit';
46
47
    /**
48
     * @var MenuStyle
49
     */
50
    private $style;
51
52
    /**
53
     * @var Terminal
54
     */
55
    private $terminal;
56
57
    /**
58
     * @var bool
59
     */
60
    private $disableDefaultItems = false;
61
62
    /**
63
     * @var bool
64
     */
65
    private $disabled = false;
66
67
    /**
68
     * Whether or not to auto create keyboard shortcuts for items
69
     * when they contain square brackets. Eg: [M]y item
70
     *
71
     * @var bool
72
     */
73
    private $autoShortcuts = false;
74
75
    /**
76
     * Regex to auto match for shortcuts defaults to looking
77
     * for a single character encased in square brackets
78
     *
79
     * @var string
80
     */
81
    private $autoShortcutsRegex = '/\[(.)\]/';
82
83
    /**
84
     * @var bool
85
     */
86
    private $subMenu = false;
87
88
    public function __construct(Terminal $terminal = null)
89
    {
90
        $this->terminal = $terminal ?? TerminalFactory::fromSystem();
91
        $this->style    = new MenuStyle($this->terminal);
92
        $this->menu     = new CliMenu(null, [], $this->terminal, $this->style);
93
    }
94
95
    public static function newSubMenu(Terminal $terminal) : self
96
    {
97
        $instance = new self($terminal);
98
        $instance->subMenu = true;
99
100
        return $instance;
101
    }
102
103
    public function setTitle(string $title) : self
104
    {
105
        $this->menu->setTitle($title);
106
107
        return $this;
108
    }
109
110
    public function addMenuItem(MenuItemInterface $item) : self
111
    {
112
        $this->menu->addItem($item);
113
114
        $this->processItemShortcut($item);
115
116
        return $this;
117
    }
118
119
    public function addItem(
120
        string $text,
121
        callable $itemCallable,
122
        bool $showItemExtra = false,
123
        bool $disabled = false
124
    ) : self {
125
        $this->addMenuItem(new SelectableItem($text, $itemCallable, $showItemExtra, $disabled));
126
127
        return $this;
128
    }
129
130
    public function addItems(array $items) : self
131
    {
132
        foreach ($items as $item) {
133
            $this->addItem(...$item);
0 ignored issues
show
Bug introduced by
The call to PhpSchool\CliMenu\Builde...iMenuBuilder::addItem() has too few arguments starting with itemCallable. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

133
            $this->/** @scrutinizer ignore-call */ 
134
                   addItem(...$item);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
134
        }
135
136
        return $this;
137
    }
138
139
    public function addCheckboxItem(
140
        string $text,
141
        callable $itemCallable,
142
        bool $showItemExtra = false,
143
        bool $disabled = false
144
    ) : self {
145
        $this->addMenuItem(new CheckboxItem($text, $itemCallable, $showItemExtra, $disabled));
146
147
        return $this;
148
    }
149
150
    public function addRadioItem(
151
        string $text,
152
        callable $itemCallable,
153
        bool $showItemExtra = false,
154
        bool $disabled = false
155
    ) : self {
156
        $this->addMenuItem(new RadioItem($text, $itemCallable, $showItemExtra, $disabled));
157
158
        return $this;
159
    }
160
161
    public function addStaticItem(string $text) : self
162
    {
163
        $this->addMenuItem(new StaticItem($text));
164
165
        return $this;
166
    }
167
168
    public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self
169
    {
170
        $this->addMenuItem(new LineBreakItem($breakChar, $lines));
171
172
        return $this;
173
    }
174
175
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
176
    {
177
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
178
179
        return $this;
180
    }
181
182
    public function addSubMenu(string $text, \Closure $callback) : self
183
    {
184
        $builder = self::newSubMenu($this->terminal);
185
186
        if ($this->autoShortcuts) {
187
            $builder->enableAutoShortcuts($this->autoShortcutsRegex);
188
        }
189
190
        $callback($builder);
191
192
        $menu = $builder->build();
193
        $menu->setParent($this->menu);
194
195
        $this->menu->addItem($item = new MenuMenuItem(
196
            $text,
197
            $menu,
198
            $builder->isMenuDisabled()
199
        ));
200
201
        $this->processItemShortcut($item);
202
203
        return $this;
204
    }
205
206
    public function addSubMenuFromBuilder(string $text, CliMenuBuilder $builder) : self
207
    {
208
        $menu = $builder->build();
209
        $menu->setParent($this->menu);
210
211
        $this->menu->addItem($item = new MenuMenuItem(
212
            $text,
213
            $menu,
214
            $builder->isMenuDisabled()
215
        ));
216
217
        $this->processItemShortcut($item);
218
219
        return $this;
220
    }
221
222
    public function enableAutoShortcuts(string $regex = null) : self
223
    {
224
        $this->autoShortcuts = true;
225
226
        if (null !== $regex) {
227
            $this->autoShortcutsRegex = $regex;
228
        }
229
230
        return $this;
231
    }
232
233
    private function extractShortcut(string $title) : ?string
234
    {
235
        preg_match($this->autoShortcutsRegex, $title, $match);
236
237
        if (!isset($match[1])) {
238
            return null;
239
        }
240
241
        if (mb_strlen($match[1]) > 1) {
242
            throw InvalidShortcutException::fromShortcut($match[1]);
243
        }
244
245
        return isset($match[1]) ? strtolower($match[1]) : null;
246
    }
247
248
    private function processItemShortcut(MenuItemInterface $item) : void
249
    {
250
        $this->processIndividualShortcut($item, function (CliMenu $menu) use ($item) {
251
            $menu->executeAsSelected($item);
252
        });
253
    }
254
255
    private function processSplitItemShortcuts(SplitItem $splitItem) : void
256
    {
257
        foreach ($splitItem->getItems() as $item) {
258
            $this->processIndividualShortcut($item, function (CliMenu $menu) use ($splitItem, $item) {
259
                $current = $splitItem->getSelectedItemIndex();
260
261
                $splitItem->setSelectedItemIndex(
262
                    array_search($item, $splitItem->getItems(), true)
0 ignored issues
show
Bug introduced by
It seems like array_search($item, $splitItem->getItems(), true) can also be of type false and string; however, parameter $index of PhpSchool\CliMenu\MenuIt...:setSelectedItemIndex() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

262
                    /** @scrutinizer ignore-type */ array_search($item, $splitItem->getItems(), true)
Loading history...
263
                );
264
265
                $menu->executeAsSelected($splitItem);
266
267
                if ($current !== null) {
268
                    $splitItem->setSelectedItemIndex($current);
269
                }
270
            });
271
        }
272
    }
273
274
    private function processIndividualShortcut(MenuItemInterface $item, callable $callback) : void
275
    {
276
        if (!$this->autoShortcuts) {
277
            return;
278
        }
279
280
        if ($shortcut = $this->extractShortcut($item->getText())) {
281
            $this->menu->addCustomControlMapping(
282
                $shortcut,
283
                $callback
284
            );
285
        }
286
    }
287
288
    public function addSplitItem(\Closure $callback) : self
289
    {
290
        $builder = new SplitItemBuilder($this->menu);
291
292
        if ($this->autoShortcuts) {
293
            $builder->enableAutoShortcuts($this->autoShortcutsRegex);
294
        }
295
296
        $callback($builder);
297
298
        $this->menu->addItem($splitItem = $builder->build());
299
300
        $this->processSplitItemShortcuts($splitItem);
301
302
        return $this;
303
    }
304
305
    /**
306
     * Disable a submenu
307
     *
308
     * @throws \InvalidArgumentException
309
     */
310
    public function disableMenu() : self
311
    {
312
        if (!$this->subMenu) {
313
            throw new \InvalidArgumentException(
314
                'You can\'t disable the root menu'
315
            );
316
        }
317
318
        $this->disabled = true;
319
320
        return $this;
321
    }
322
323
    public function isMenuDisabled() : bool
324
    {
325
        return $this->disabled;
326
    }
327
328
    public function setGoBackButtonText(string $goBackButtonTest) : self
329
    {
330
        $this->goBackButtonText = $goBackButtonTest;
331
332
        return $this;
333
    }
334
335
    public function setExitButtonText(string $exitButtonText) : self
336
    {
337
        $this->exitButtonText = $exitButtonText;
338
339
        return $this;
340
    }
341
342
    public function setBackgroundColour(string $colour, string $fallback = null) : self
343
    {
344
        $this->style->setBg($colour, $fallback);
345
346
        return $this;
347
    }
348
349
    public function setForegroundColour(string $colour, string $fallback = null) : self
350
    {
351
        $this->style->setFg($colour, $fallback);
352
353
        return $this;
354
    }
355
356
    public function setWidth(int $width) : self
357
    {
358
        $this->style->setWidth($width);
359
360
        return $this;
361
    }
362
363
    public function setPadding(int $topBottom, int $leftRight = null) : self
364
    {
365
        $this->style->setPadding($topBottom, $leftRight);
366
367
        return $this;
368
    }
369
370
    public function setPaddingTopBottom(int $topBottom) : self
371
    {
372
        $this->style->setPaddingTopBottom($topBottom);
373
374
        return $this;
375
    }
376
377
    public function setPaddingLeftRight(int $leftRight) : self
378
    {
379
        $this->style->setPaddingLeftRight($leftRight);
380
381
        return $this;
382
    }
383
384
    public function setMarginAuto() : self
385
    {
386
        $this->style->setMarginAuto();
387
388
        return $this;
389
    }
390
391
    public function setMargin(int $margin) : self
392
    {
393
        $this->style->setMargin($margin);
394
395
        return $this;
396
    }
397
398
    public function setItemExtra(string $extra) : self
399
    {
400
        $this->style->setItemExtra($extra);
401
        $this->menu->getSelectableStyle()->setItemExtra($extra);
402
403
        // if we customise item extra, it means we most likely want to display it
404
        $this->displayExtra();
405
406
        return $this;
407
    }
408
409
    public function setTitleSeparator(string $separator) : self
410
    {
411
        $this->style->setTitleSeparator($separator);
412
413
        return $this;
414
    }
415
416
    public function setBorder(int $top, $right = null, $bottom = null, $left = null, string $colour = null) : self
417
    {
418
        $this->style->setBorder($top, $right, $bottom, $left, $colour);
419
420
        return $this;
421
    }
422
423
    public function setBorderTopWidth(int $width) : self
424
    {
425
        $this->style->setBorderTopWidth($width);
426
427
        return $this;
428
    }
429
430
    public function setBorderRightWidth(int $width) : self
431
    {
432
        $this->style->setBorderRightWidth($width);
433
434
        return $this;
435
    }
436
437
    public function setBorderBottomWidth(int $width) : self
438
    {
439
        $this->style->setBorderBottomWidth($width);
440
441
        return $this;
442
    }
443
444
    public function setBorderLeftWidth(int $width) : self
445
    {
446
        $this->style->setBorderLeftWidth($width);
447
448
        return $this;
449
    }
450
451
    public function setBorderColour(string $colour, $fallback = null) : self
452
    {
453
        $this->style->setBorderColour($colour, $fallback);
454
455
        return $this;
456
    }
457
458
    public function getStyle() : MenuStyle
459
    {
460
        return $this->style;
461
    }
462
463
    public function getTerminal() : Terminal
464
    {
465
        return $this->terminal;
466
    }
467
468
    private function getDefaultItems() : array
469
    {
470
        $actions = [];
471
        if ($this->subMenu) {
472
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
473
        }
474
475
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
476
        return $actions;
477
    }
478
479
    public function disableDefaultItems() : self
480
    {
481
        $this->disableDefaultItems = true;
482
483
        return $this;
484
    }
485
486
    public function displayExtra() : self
487
    {
488
        $this->style->setDisplaysExtra(true);
489
        $this->menu->getSelectableStyle()->setDisplaysExtra(true);
490
491
        return $this;
492
    }
493
494
    private function itemsHaveExtra(array $items) : bool
495
    {
496
        return !empty(array_filter($items, function (MenuItemInterface $item) {
497
            return $item->showsItemExtra();
498
        }));
499
    }
500
501
    public function build() : CliMenu
502
    {
503
        if (!$this->disableDefaultItems) {
504
            $this->menu->addItems($this->getDefaultItems());
505
        }
506
507
        if (!$this->style->getDisplaysExtra()) {
508
            $this->style->setDisplaysExtra($this->itemsHaveExtra($this->menu->getItems()));
509
        }
510
511
        if (!$this->subMenu) {
512
            $this->propagateStyles($this->menu);
513
        }
514
515
        return $this->menu;
516
    }
517
518
    public function getCheckboxStyle() : CheckboxStyle
519
    {
520
        return $this->menu->getCheckboxStyle();
521
    }
522
523
    public function setCheckboxStyle(CheckboxStyle $style) : self
524
    {
525
        $this->menu->setCheckboxStyle($style);
526
527
        return $this;
528
    }
529
530
    public function modifyCheckboxStyle(callable $itemCallable) : self
531
    {
532
        $itemCallable($this->menu->getCheckboxStyle());
533
534
        return $this;
535
    }
536
537
    public function getRadioStyle() : RadioStyle
538
    {
539
        return $this->menu->getRadioStyle();
540
    }
541
542
    public function setRadioStyle(RadioStyle $style) : self
543
    {
544
        $this->menu->setRadioStyle($style);
545
546
        return $this;
547
    }
548
549
    public function modifyRadioStyle(callable $itemCallable) : self
550
    {
551
        $itemCallable($this->menu->getRadioStyle());
552
553
        return $this;
554
    }
555
556
    public function getSelectableStyle() : SelectableStyle
557
    {
558
        return $this->menu->getSelectableStyle();
559
    }
560
561
    public function setSelectableStyle(SelectableStyle $style) : self
562
    {
563
        $this->menu->setSelectableStyle($style);
564
565
        return $this;
566
    }
567
568
    public function modifySelectableStyle(callable $itemCallable) : self
569
    {
570
        $itemCallable($this->menu->getSelectableStyle());
571
572
        return $this;
573
    }
574
575
    /**
576
     * Pass styles from current menu to sub-menu
577
     * only if sub-menu style has not be customized
578
     */
579
    private function propagateStyles(CliMenu $menu, array $items = [])
580
    {
581
        $currentItems = !empty($items) ? $items : $menu->getItems();
582
583
        foreach ($currentItems as $item) {
584
            if ($item instanceof CheckboxItem
585
                && !$item->getStyle()->hasChangedFromDefaults()
586
            ) {
587
                $item->setStyle(clone $menu->getCheckboxStyle());
588
            }
589
590
            if ($item instanceof RadioItem
591
                && !$item->getStyle()->hasChangedFromDefaults()
592
            ) {
593
                $item->setStyle(clone $menu->getRadioStyle());
594
            }
595
596
            if ($item instanceof SelectableStyleInterface
597
                && !$item->getStyle()->hasChangedFromDefaults()
598
            ) {
599
                $item->setStyle(clone $menu->getSelectableStyle());
600
            }
601
602
            // Apply current style to children, if they are not customized
603
            if ($item instanceof MenuMenuItem) {
604
                $subMenu = $item->getSubMenu();
605
606
                if (!$subMenu->getStyle()->hasChangedFromDefaults()) {
607
                    $subMenu->setStyle(clone $menu->getStyle());
608
                }
609
610
                if (!$subMenu->getCheckboxStyle()->hasChangedFromDefaults()) {
611
                    $subMenu->setCheckboxStyle(clone $menu->getCheckboxStyle());
612
                }
613
614
                if (!$subMenu->getRadioStyle()->hasChangedFromDefaults()) {
615
                    $subMenu->setRadioStyle(clone $menu->getRadioStyle());
616
                }
617
618
                if (!$subMenu->getSelectableStyle()->hasChangedFromDefaults()) {
619
                    $subMenu->setSelectableStyle(clone $menu->getSelectableStyle());
620
                }
621
622
                $this->propagateStyles($subMenu);
623
            }
624
625
            // Apply styles to SplitItem children using current $menu
626
            if ($item instanceof SplitItem) {
627
                $this->propagateStyles($menu, $item->getItems());
628
            }
629
        }
630
    }
631
}
632