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

CliMenuBuilder::setMarginAuto()   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 0
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
     * @var bool
62
     */
63
    private $subMenu = false;
64
65
    public function __construct(Terminal $terminal = null)
66
    {
67
        $this->terminal = $terminal ?? TerminalFactory::fromSystem();
68
        $this->style    = new MenuStyle($this->terminal);
69
        $this->menu     = new CliMenu(null, [], $this->terminal, $this->style);
70
    }
71
    
72
    public static function newSubMenu(Terminal $terminal) : self
73
    {
74
        $instance = new self($terminal);
75
        $instance->subMenu = true;
76
        
77
        return $instance;
78
    }
79
80
    public function setTitle(string $title) : self
81
    {
82
        $this->menu->setTitle($title);
83
84
        return $this;
85
    }
86
87
    public function addMenuItem(MenuItemInterface $item) : self
88
    {
89
        $this->menu->addItem($item);
90
91
        $this->processItemShortcut($item);
92
93
        return $this;
94
    }
95
96
    public function addItem(
97
        string $text,
98
        callable $itemCallable,
99
        bool $showItemExtra = false,
100
        bool $disabled = false
101
    ) : self {
102
        $this->addMenuItem(new SelectableItem($text, $itemCallable, $showItemExtra, $disabled));
103
104
        return $this;
105
    }
106
107
    public function addItems(array $items) : self
108
    {
109
        foreach ($items as $item) {
110
            $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

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

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