Completed
Push — master ( 499201...4a0bb5 )
by Aydin
26s queued 12s
created

SplitItem::calculateItemExtra()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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