CliMenuBuilder::setBorderRightWidth()   A
last analyzed

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

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

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