Passed
Pull Request — master (#230)
by Aydin
01:50
created

SplitItem::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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