Completed
Pull Request — master (#205)
by Aydin
02:09
created

CliMenuBuilder::processItemShortcut()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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