SplitItem::setItems()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpSchool\CliMenu\MenuItem;
4
5
use Assert\Assertion;
6
use PhpSchool\CliMenu\CliMenu;
7
use PhpSchool\CliMenu\MenuStyle;
8
use PhpSchool\CliMenu\Style\DefaultStyle;
9
use PhpSchool\CliMenu\Style\ItemStyle;
10
use PhpSchool\CliMenu\Style\Selectable;
11
use PhpSchool\CliMenu\Util\StringUtil;
12
use function PhpSchool\CliMenu\Util\collect;
13
use function PhpSchool\CliMenu\Util\each;
14
use function PhpSchool\CliMenu\Util\mapWithKeys;
15
use function PhpSchool\CliMenu\Util\max;
16
17
/**
18
 * @author Michael Woodward <[email protected]>
19
 */
20
class SplitItem implements MenuItemInterface, PropagatesStyles
21
{
22
    /**
23
     * @var array
24
     */
25
    private $items = [];
26
27
    /**
28
     * @var int|null
29
     */
30
    private $selectedItemIndex;
31
32
    /**
33
     * @var bool
34
     */
35
    private $canBeSelected = true;
36
37
    /**
38
     * @var int
39
     */
40
    private $gutter = 2;
41
42
    /**
43
     * @var DefaultStyle
44
     */
45
    private $style;
46
47
    /**
48
     * @var array
49
     */
50
    private static $blacklistedItems = [
51
        \PhpSchool\CliMenu\MenuItem\AsciiArtItem::class,
52
        \PhpSchool\CliMenu\MenuItem\LineBreakItem::class,
53
        \PhpSchool\CliMenu\MenuItem\SplitItem::class,
54
    ];
55
56
    public function __construct(array $items = [])
57
    {
58
        $this->addItems($items);
59
        $this->setDefaultSelectedItem();
60
61
        $this->style = new DefaultStyle();
62
    }
63
64
    public function getGutter() : int
65
    {
66
        return $this->gutter;
67
    }
68
69
    public function setGutter(int $gutter) : void
70
    {
71
        Assertion::greaterOrEqualThan($gutter, 0);
72
        $this->gutter = $gutter;
73
    }
74
75
    public function addItem(MenuItemInterface $item) : self
76
    {
77
        foreach (self::$blacklistedItems as $bl) {
78
            if ($item instanceof $bl) {
79
                throw new \InvalidArgumentException("Cannot add a $bl to a SplitItem");
80
            }
81
        }
82
        $this->items[] = $item;
83
        $this->setDefaultSelectedItem();
84
        return $this;
85
    }
86
87
    public function addItems(array $items) : self
88
    {
89
        foreach ($items as $item) {
90
            $this->addItem($item);
91
        }
92
            
93
        return $this;
94
    }
95
96
    public function setItems(array $items) : self
97
    {
98
        $this->items = [];
99
        $this->addItems($items);
100
        return $this;
101
    }
102
103
    /**
104
     * Select default item
105
     */
106
    private function setDefaultSelectedItem() : void
107
    {
108
        foreach ($this->items as $index => $item) {
109
            if ($item->canSelect()) {
110
                $this->canBeSelected = true;
111
                $this->selectedItemIndex = $index;
112
                return;
113
            }
114
        }
115
116
        $this->canBeSelected = false;
117
        $this->selectedItemIndex = null;
118
    }
119
120
    /**
121
     * The output text for the item
122
     */
123
    public function getRows(MenuStyle $style, bool $selected = false) : array
124
    {
125
        $numberOfItems = count($this->items);
126
127
        if ($numberOfItems === 0) {
128
            throw new \RuntimeException(sprintf('There should be at least one item added to: %s', __CLASS__));
129
        }
130
        
131
        if (!$selected) {
132
            $this->setDefaultSelectedItem();
133
        }
134
135
        $largestItemExtra = $this->calculateItemExtra();
136
137
        $length = $largestItemExtra > 0
138
            ? floor($style->getContentWidth() / $numberOfItems) - ($largestItemExtra + 2)
139
            : floor($style->getContentWidth() / $numberOfItems);
140
141
        $length -= $this->gutter;
142
        $length = (int) $length;
143
144
        $missingLength = $style->getContentWidth() % $numberOfItems;
145
        
146
        return $this->buildRows(
147
            mapWithKeys($this->items, function (int $index, MenuItemInterface $item) use ($selected, $length, $style) {
148
                $isSelected = $selected && $index === $this->selectedItemIndex;
149
150
                $marker = $item->getStyle()->getMarker($item, $isSelected);
151
152
                $itemExtra = '';
153
                if ($item->getStyle()->getDisplaysExtra()) {
154
                    $itemExtraVal = $item->getStyle()->getItemExtra();
155
                    $itemExtra = $item->showsItemExtra()
156
                        ? sprintf('  %s', $itemExtraVal)
157
                        : sprintf('  %s', str_repeat(' ', mb_strlen($itemExtraVal)));
158
                }
159
160
                return $this->buildCell(
161
                    explode(
162
                        "\n",
163
                        StringUtil::wordwrap(
164
                            sprintf('%s%s', $marker, $item->getText()),
165
                            $length,
166
                            sprintf("\n%s", str_repeat(' ', mb_strlen($marker)))
167
                        )
168
                    ),
169
                    $length,
170
                    $style,
171
                    $isSelected,
172
                    $itemExtra
173
                );
174
            }),
175
            $missingLength,
176
            $length,
177
            $largestItemExtra
178
        );
179
    }
180
181
    private function buildRows(array $cells, int $missingLength, int $length, int $largestItemExtra) : array
182
    {
183
        $extraPadLength = $largestItemExtra > 0 ? 2 + $largestItemExtra : 0;
184
        
185
        return array_map(
186
            function ($i) use ($cells, $length, $missingLength, $extraPadLength) {
187
                return $this->buildRow($cells, $i, $length, $missingLength, $extraPadLength);
188
            },
189
            range(0, max(array_map('count', $cells)) - 1)
190
        );
191
    }
192
193
    private function buildRow(array $cells, int $index, int $length, int $missingLength, int $extraPadLength) : string
194
    {
195
        return sprintf(
196
            '%s%s',
197
            implode(
198
                '',
199
                array_map(
200
                    function ($cell) use ($index, $length, $extraPadLength) {
201
                        return $cell[$index] ?? str_repeat(' ', $length + $this->gutter + $extraPadLength);
202
                    },
203
                    $cells
204
                )
205
            ),
206
            str_repeat(' ', $missingLength)
207
        );
208
    }
209
210
    private function buildCell(
211
        array $content,
212
        int $length,
213
        MenuStyle $style,
214
        bool $isSelected,
215
        string $itemExtra
216
    ) : array {
217
        return array_map(function ($row, $index) use ($length, $style, $isSelected, $itemExtra) {
218
            $invertedColoursSetCode = $isSelected
219
                ? $style->getInvertedColoursSetCode()
220
                : '';
221
            $invertedColoursUnsetCode = $isSelected
222
                ? $style->getInvertedColoursUnsetCode()
223
                : '';
224
225
            return sprintf(
226
                '%s%s%s%s%s%s',
227
                $invertedColoursSetCode,
228
                $row,
229
                str_repeat(' ', $length - mb_strlen($row)),
230
                $index === 0 ? $itemExtra : str_repeat(' ', mb_strlen($itemExtra)),
231
                $invertedColoursUnsetCode,
232
                str_repeat(' ', $this->gutter)
233
            );
234
        }, $content, array_keys($content));
235
    }
236
237
    /**
238
     * Is there an item with this index and can it be
239
     * selected?
240
     */
241
    public function canSelectIndex(int $index) : bool
242
    {
243
        return isset($this->items[$index]) && $this->items[$index]->canSelect();
244
    }
245
246
    /**
247
     * Set the item index which should be selected. If the item does
248
     * not exist then throw an exception.
249
     */
250
    public function setSelectedItemIndex(int $index) : void
251
    {
252
        if (!isset($this->items[$index])) {
253
            throw new \InvalidArgumentException(sprintf('Index: "%s" does not exist', $index));
254
        }
255
        
256
        $this->selectedItemIndex = $index;
257
    }
258
259
    /**
260
     * Get the currently select item index.
261
     * May be null in case of no selectable item.
262
     */
263
    public function getSelectedItemIndex() : ?int
264
    {
265
        return $this->selectedItemIndex;
266
    }
267
268
    /**
269
     * Get the currently selected item - if no items are selectable
270
     * then throw an exception.
271
     */
272
    public function getSelectedItem() : MenuItemInterface
273
    {
274
        if (null === $this->selectedItemIndex) {
275
            throw new \RuntimeException('No item is selected');
276
        }
277
        
278
        return $this->items[$this->selectedItemIndex];
279
    }
280
281
    public function getItems() : array
282
    {
283
        return $this->items;
284
    }
285
286
    /**
287
     * Can the item be selected
288
     * In this case, it indicates if at least 1 item inside the SplitItem can be selected
289
     */
290
    public function canSelect() : bool
291
    {
292
        return $this->canBeSelected;
293
    }
294
295
    /**
296
     * Execute the items callable if required
297
     */
298
    public function getSelectAction() : ?callable
299
    {
300
        return null;
301
    }
302
303
    /**
304
     * Whether or not the menu item is showing the menustyle extra value
305
     */
306
    public function showsItemExtra() : bool
307
    {
308
        return false;
309
    }
310
311
    /**
312
     * Enable showing item extra
313
     */
314
    public function showItemExtra() : void
315
    {
316
        //noop
317
    }
318
319
    /**
320
     * Disable showing item extra
321
     */
322
    public function hideItemExtra() : void
323
    {
324
        //noop
325
    }
326
327
    /**
328
     * Nothing to return with SplitItem
329
     */
330
    public function getText() : string
331
    {
332
        throw new \BadMethodCallException(sprintf('Not supported on: %s', __CLASS__));
333
    }
334
335
    /**
336
     * Finds largest itemExtra value in items
337
     */
338
    private function calculateItemExtra() : int
339
    {
340
        return max(array_map(
341
            function (MenuItemInterface $item) {
342
                return mb_strlen($item->getStyle()->getItemExtra());
343
            },
344
            array_filter($this->items, function (MenuItemInterface $item) {
345
                return $item->getStyle()->getDisplaysExtra();
346
            })
347
        ));
348
    }
349
350
    /**
351
     * @return DefaultStyle
352
     */
353
    public function getStyle(): ItemStyle
354
    {
355
        return $this->style;
356
    }
357
358
    public function setStyle(DefaultStyle $style): void
359
    {
360
        $this->style = $style;
361
    }
362
363
    /**
364
     * @inheritDoc
365
     */
366
    public function propagateStyles(CliMenu $parent): void
367
    {
368
        collect($this->items)
369
            ->filter(function (int $k, MenuItemInterface $item) use ($parent) {
370
                return $parent->getStyleLocator()->hasStyleForMenuItem($item);
371
            })
372
            ->filter(function (int $k, MenuItemInterface $item) {
373
                return !$item->getStyle()->hasChangedFromDefaults();
374
            })
375
            ->each(function (int $k, $item) use ($parent) {
376
                $item->setStyle(clone $parent->getItemStyleForItem($item));
377
            });
378
379
        collect($this->items)
380
            ->filter(function (int $k, MenuItemInterface $item) {
381
                return $item instanceof PropagatesStyles;
382
            })
383
            ->each(function (int $k, $item) use ($parent) {
384
                $item->propagateStyles($parent);
385
            });
386
    }
387
}
388