Completed
Pull Request — master (#176)
by Aydin
05:30
created

CliMenuBuilder::enableAutoShortcuts()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

243
                    /** @scrutinizer ignore-type */ array_search($item, $splitItem->getItems(), true)
Loading history...
244
                );
245
246
                $menu->executeAsSelected($splitItem);
247
248
                if ($current !== null) {
249
                    $splitItem->setSelectedItemIndex($current);
250
                }
251
            });
252
        }
253
    }
254
255
    private function processIndividualShortcut(MenuItemInterface $item, callable $callback) : void
256
    {
257
        if (!$this->autoShortcuts) {
258
            return;
259
        }
260
261
        if ($shortcut = $this->extractShortcut($item->getText())) {
262
            $this->menu->addCustomControlMapping(
263
                $shortcut,
264
                $callback
265
            );
266
        }
267
    }
268
269
    public function addSplitItem(\Closure $callback) : self
270
    {
271
        $builder = new SplitItemBuilder($this->menu);
272
273
        $callback = $callback->bindTo($builder);
274
        $callback($builder);
275
        
276
        $this->menu->addItem($splitItem = $builder->build());
277
278
        $this->processSplitItemShortcuts($splitItem);
279
280
        return $this;
281
    }
282
283
    /**
284
     * Disable a submenu
285
     *
286
     * @throws \InvalidArgumentException
287
     */
288
    public function disableMenu() : self
289
    {
290
        if (!$this->subMenu) {
291
            throw new \InvalidArgumentException(
292
                'You can\'t disable the root menu'
293
            );
294
        }
295
296
        $this->disabled = true;
297
298
        return $this;
299
    }
300
301
    public function isMenuDisabled() : bool
302
    {
303
        return $this->disabled;
304
    }
305
306
    public function setGoBackButtonText(string $goBackButtonTest) : self
307
    {
308
        $this->goBackButtonText = $goBackButtonTest;
309
310
        return $this;
311
    }
312
313
    public function setExitButtonText(string $exitButtonText) : self
314
    {
315
        $this->exitButtonText = $exitButtonText;
316
317
        return $this;
318
    }
319
320
    public function setBackgroundColour(string $colour, string $fallback = null) : self
321
    {
322
        $this->style->setBg($colour, $fallback);
323
324
        return $this;
325
    }
326
327
    public function setForegroundColour(string $colour, string $fallback = null) : self
328
    {
329
        $this->style->setFg($colour, $fallback);
330
331
        return $this;
332
    }
333
334
    public function setWidth(int $width) : self
335
    {
336
        $this->style->setWidth($width);
337
338
        return $this;
339
    }
340
341
    public function setPadding(int $topBottom, int $leftRight = null) : self
342
    {
343
        $this->style->setPadding($topBottom, $leftRight);
344
345
        return $this;
346
    }
347
348
    public function setPaddingTopBottom(int $topBottom) : self
349
    {
350
        $this->style->setPaddingTopBottom($topBottom);
351
352
        return $this;
353
    }
354
355
    public function setPaddingLeftRight(int $leftRight) : self
356
    {
357
        $this->style->setPaddingLeftRight($leftRight);
358
359
        return $this;
360
    }
361
362
    public function setMarginAuto() : self
363
    {
364
        $this->style->setMarginAuto();
365
366
        return $this;
367
    }
368
369
    public function setMargin(int $margin) : self
370
    {
371
        $this->style->setMargin($margin);
372
373
        return $this;
374
    }
375
376
    public function setUnselectedMarker(string $marker) : self
377
    {
378
        $this->style->setUnselectedMarker($marker);
379
380
        return $this;
381
    }
382
383
    public function setSelectedMarker(string $marker) : self
384
    {
385
        $this->style->setSelectedMarker($marker);
386
387
        return $this;
388
    }
389
390
    public function setItemExtra(string $extra) : self
391
    {
392
        $this->style->setItemExtra($extra);
393
394
        return $this;
395
    }
396
397
    public function setTitleSeparator(string $separator) : self
398
    {
399
        $this->style->setTitleSeparator($separator);
400
401
        return $this;
402
    }
403
404
    public function setBorder(int $top, $right = null, $bottom = null, $left = null, string $colour = null) : self
405
    {
406
        $this->style->setBorder($top, $right, $bottom, $left, $colour);
407
408
        return $this;
409
    }
410
411
    public function setBorderTopWidth(int $width) : self
412
    {
413
        $this->style->setBorderTopWidth($width);
414
        
415
        return $this;
416
    }
417
418
    public function setBorderRightWidth(int $width) : self
419
    {
420
        $this->style->setBorderRightWidth($width);
421
422
        return $this;
423
    }
424
425
    public function setBorderBottomWidth(int $width) : self
426
    {
427
        $this->style->setBorderBottomWidth($width);
428
429
        return $this;
430
    }
431
432
    public function setBorderLeftWidth(int $width) : self
433
    {
434
        $this->style->setBorderLeftWidth($width);
435
436
        return $this;
437
    }
438
439
    public function setBorderColour(string $colour, $fallback = null) : self
440
    {
441
        $this->style->setBorderColour($colour, $fallback);
442
443
        return $this;
444
    }
445
446
    public function getStyle() : MenuStyle
447
    {
448
        return $this->style;
449
    }
450
451
    public function getTerminal() : Terminal
452
    {
453
        return $this->terminal;
454
    }
455
456
    private function getDefaultItems() : array
457
    {
458
        $actions = [];
459
        if ($this->subMenu) {
460
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
461
        }
462
463
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
464
        return $actions;
465
    }
466
467
    public function disableDefaultItems() : self
468
    {
469
        $this->disableDefaultItems = true;
470
471
        return $this;
472
    }
473
474
    private function itemsHaveExtra(array $items) : bool
475
    {
476
        return !empty(array_filter($items, function (MenuItemInterface $item) {
477
            return $item->showsItemExtra();
478
        }));
479
    }
480
    
481
    public function build() : CliMenu
482
    {
483
        if (!$this->disableDefaultItems) {
484
            $this->menu->addItems($this->getDefaultItems());
485
        }
486
487
        $this->style->setDisplaysExtra($this->itemsHaveExtra($this->menu->getItems()));
488
489
        return $this->menu;
490
    }
491
}
492