Passed
Pull Request — master (#205)
by Aydin
02:04
created

CliMenuBuilder::setMargin()   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\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()->getIsCustom()) {
206
            $menu->checkboxStyle(function (CheckboxStyle $style) {
207
                $style->fromArray($this->menu->getCheckboxStyle()->toArray());
208
            });
209
        }
210
211
        if (!$menu->getRadioStyle()->getIsCustom()) {
212
            $menu->radioStyle(function (RadioStyle $style) {
213
                $style->fromArray($this->menu->getRadioStyle()->toArray());
214
            });
215
        }
216
217
        $this->menu->addItem($item = new MenuMenuItem(
218
            $text,
219
            $menu,
220
            $builder->isMenuDisabled()
221
        ));
222
223
        $this->processItemShortcut($item);
224
225
        return $this;
226
    }
227
228
    public function addSubMenuFromBuilder(string $text, CliMenuBuilder $builder) : self
229
    {
230
        $menu = $builder->build();
231
        $menu->setParent($this->menu);
232
233
        //we apply the parent theme if nothing was changed
234
        //if no styles were changed in this sub-menu
235
        if (!$menu->getStyle()->hasChangedFromDefaults()) {
236
            $menu->setStyle($this->menu->getStyle());
237
        }
238
239
        $menu->checkboxStyle(function (CheckboxStyle $style) {
240
            $style->fromArray($this->menu->getCheckboxStyle()->toArray());
241
        });
242
243
        $menu->radioStyle(function (RadioStyle $style) {
244
            $style->fromArray($this->menu->getRadioStyle()->toArray());
245
        });
246
247
        $this->menu->addItem($item = new MenuMenuItem(
248
            $text,
249
            $menu,
250
            $builder->isMenuDisabled()
251
        ));
252
253
        $this->processItemShortcut($item);
254
255
        return $this;
256
    }
257
258
    public function enableAutoShortcuts(string $regex = null) : self
259
    {
260
        $this->autoShortcuts = true;
261
262
        if (null !== $regex) {
263
            $this->autoShortcutsRegex = $regex;
264
        }
265
266
        return $this;
267
    }
268
269
    private function extractShortcut(string $title) : ?string
270
    {
271
        preg_match($this->autoShortcutsRegex, $title, $match);
272
273
        if (!isset($match[1])) {
274
            return null;
275
        }
276
277
        if (mb_strlen($match[1]) > 1) {
278
            throw InvalidShortcutException::fromShortcut($match[1]);
279
        }
280
281
        return isset($match[1]) ? strtolower($match[1]) : null;
282
    }
283
284
    private function processItemShortcut(MenuItemInterface $item) : void
285
    {
286
        $this->processIndividualShortcut($item, function (CliMenu $menu) use ($item) {
287
            $menu->executeAsSelected($item);
288
        });
289
    }
290
291
    private function processSplitItemShortcuts(SplitItem $splitItem) : void
292
    {
293
        foreach ($splitItem->getItems() as $item) {
294
            $this->processIndividualShortcut($item, function (CliMenu $menu) use ($splitItem, $item) {
295
                $current = $splitItem->getSelectedItemIndex();
296
297
                $splitItem->setSelectedItemIndex(
298
                    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

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