Completed
Push — master ( a9e6a3...c72b58 )
by Aydin
01:55
created

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

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

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