Completed
Push — master ( 389ef3...499201 )
by Aydin
24s queued 11s
created

CliMenuBuilder::modifySelectableStyle()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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