Passed
Pull Request — master (#176)
by Aydin
01:45
created

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

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

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