Passed
Pull Request — master (#216)
by
unknown
02:26
created

SplitItem::calculateItemExtra()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
c 0
b 0
f 0
nc 4
nop 0
dl 0
loc 13
rs 10
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 int
36
     */
37
    private $largestItemExtra = 0;
38
39
    /**
40
     * @var array
41
     */
42
    private static $blacklistedItems = [
43
        \PhpSchool\CliMenu\MenuItem\AsciiArtItem::class,
44
        \PhpSchool\CliMenu\MenuItem\LineBreakItem::class,
45
        \PhpSchool\CliMenu\MenuItem\SplitItem::class,
46
    ];
47
48
    public function __construct(array $items = [])
49
    {
50
        $this->addItems($items);
51
        $this->setDefaultSelectedItem();
52
    }
53
54
    public function setGutter(int $gutter) : void
55
    {
56
        Assertion::greaterOrEqualThan($gutter, 0);
57
        $this->gutter = $gutter;
58
    }
59
60
    public function getGutter() : int
61
    {
62
        return $this->gutter;
63
    }
64
65
    public function addItem(MenuItemInterface $item) : self
66
    {
67
        foreach (self::$blacklistedItems as $bl) {
68
            if ($item instanceof $bl) {
69
                throw new \InvalidArgumentException("Cannot add a $bl to a SplitItem");
70
            }
71
        }
72
        $this->items[] = $item;
73
        $this->setDefaultSelectedItem();
74
        return $this;
75
    }
76
77
    public function addItems(array $items) : self
78
    {
79
        foreach ($items as $item) {
80
            $this->addItem($item);
81
        }
82
            
83
        return $this;
84
    }
85
86
    public function setItems(array $items) : self
87
    {
88
        $this->items = [];
89
        $this->addItems($items);
90
        return $this;
91
    }
92
93
    /**
94
     * Select default item
95
     */
96
    private function setDefaultSelectedItem() : void
97
    {
98
        foreach ($this->items as $index => $item) {
99
            if ($item->canSelect()) {
100
                $this->canBeSelected = true;
101
                $this->selectedItemIndex = $index;
102
                return;
103
            }
104
        }
105
106
        $this->canBeSelected = false;
107
        $this->selectedItemIndex = null;
108
    }
109
110
    /**
111
     * The output text for the item
112
     */
113
    public function getRows(MenuStyle $style, bool $selected = false) : array
114
    {
115
        $numberOfItems = count($this->items);
116
117
        if ($numberOfItems === 0) {
118
            throw new \RuntimeException(sprintf('There should be at least one item added to: %s', __CLASS__));
119
        }
120
        
121
        if (!$selected) {
122
            $this->setDefaultSelectedItem();
123
        }
124
125
        $this->calculateItemExtra();
126
127
        $length = $this->largestItemExtra > 0
128
            ? floor($style->getContentWidth() / $numberOfItems) - ($this->largestItemExtra + 2)
129
            : floor($style->getContentWidth() / $numberOfItems);
130
131
        $length -= $this->gutter;
132
        $length = (int) $length;
133
134
        $missingLength = $style->getContentWidth() % $numberOfItems;
135
        
136
        return $this->buildRows(
137
            array_map(function ($index, $item) use ($selected, $length, $style) {
138
                $isSelected = $selected && $index === $this->selectedItemIndex;
139
140
                if ($item instanceof CheckboxItem || $item instanceof RadioItem) {
141
                    $markerType = $item->getStyle()->getMarker($item->getChecked());
142
                } else {
143
                    /** @var SelectableStyleInterface $item */
144
                    $markerType = $item->getStyle()->getMarker($isSelected);
145
                }
146
147
                $marker = $item->canSelect()
0 ignored issues
show
Bug introduced by
The method canSelect() does not exist on PhpSchool\CliMenu\MenuIt...electableStyleInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to PhpSchool\CliMenu\MenuIt...electableStyleInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

147
                $marker = $item->/** @scrutinizer ignore-call */ canSelect()
Loading history...
148
                    ? sprintf('%s', $markerType)
149
                    : '';
150
151
                $itemExtra = '';
152
                if ($item->getStyle()->getDisplaysExtra()) {
153
                    $itemExtraVal = $item->getStyle()->getItemExtra();
154
                    $itemExtra = $item->showsItemExtra()
0 ignored issues
show
Bug introduced by
The method showsItemExtra() does not exist on PhpSchool\CliMenu\MenuIt...electableStyleInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to PhpSchool\CliMenu\MenuIt...electableStyleInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

154
                    $itemExtra = $item->/** @scrutinizer ignore-call */ showsItemExtra()
Loading history...
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()),
0 ignored issues
show
Bug introduced by
The method getText() does not exist on PhpSchool\CliMenu\MenuIt...electableStyleInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to PhpSchool\CliMenu\MenuIt...electableStyleInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

163
                            sprintf('%s%s', $marker, $item->/** @scrutinizer ignore-call */ getText()),
Loading history...
164
                            $length,
165
                            sprintf("\n%s", str_repeat(' ', mb_strlen($marker)))
166
                        )
167
                    ),
168
                    $length,
169
                    $style,
170
                    $isSelected,
171
                    $itemExtra
172
                );
173
            }, array_keys($this->items), $this->items),
174
            $missingLength,
175
            $length
176
        );
177
    }
178
179
    private function buildRows(array $cells, int $missingLength, int $length) : array
180
    {
181
        $extraPadLength = $this->largestItemExtra > 0 ? 2 + $this->largestItemExtra : 0;
182
        
183
        return array_map(
184
            function ($i) use ($cells, $length, $missingLength, $extraPadLength) {
185
                return $this->buildRow($cells, $i, $length, $missingLength, $extraPadLength);
186
            },
187
            range(0, max(array_map('count', $cells)) - 1)
188
        );
189
    }
190
191
    private function buildRow(array $cells, int $index, int $length, int $missingLength, int $extraPadLength) : string
192
    {
193
        return sprintf(
194
            '%s%s',
195
            implode(
196
                '',
197
                array_map(
198
                    function ($cell) use ($index, $length, $extraPadLength) {
199
                        return $cell[$index] ?? str_repeat(' ', $length + $this->gutter + $extraPadLength);
200
                    },
201
                    $cells
202
                )
203
            ),
204
            str_repeat(' ', $missingLength)
205
        );
206
    }
207
208
    private function buildCell(
209
        array $content,
210
        int $length,
211
        MenuStyle $style,
212
        bool $isSelected,
213
        string $itemExtra
214
    ) : array {
215
        return array_map(function ($row, $index) use ($length, $style, $isSelected, $itemExtra) {
216
            $invertedColoursSetCode = $isSelected
217
                ? $style->getInvertedColoursSetCode()
218
                : '';
219
            $invertedColoursUnsetCode = $isSelected
220
                ? $style->getInvertedColoursUnsetCode()
221
                : '';
222
223
            return sprintf(
224
                '%s%s%s%s%s%s',
225
                $invertedColoursSetCode,
226
                $row,
227
                str_repeat(' ', $length - mb_strlen($row)),
228
                $index === 0 ? $itemExtra : str_repeat(' ', mb_strlen($itemExtra)),
229
                $invertedColoursUnsetCode,
230
                str_repeat(' ', $this->gutter)
231
            );
232
        }, $content, array_keys($content));
233
    }
234
235
    /**
236
     * Is there an item with this index and can it be
237
     * selected?
238
     */
239
    public function canSelectIndex(int $index) : bool
240
    {
241
        return isset($this->items[$index]) && $this->items[$index]->canSelect();
242
    }
243
244
    /**
245
     * Set the item index which should be selected. If the item does
246
     * not exist then throw an exception.
247
     */
248
    public function setSelectedItemIndex(int $index) : void
249
    {
250
        if (!isset($this->items[$index])) {
251
            throw new \InvalidArgumentException(sprintf('Index: "%s" does not exist', $index));
252
        }
253
        
254
        $this->selectedItemIndex = $index;
255
    }
256
257
    /**
258
     * Get the currently select item index.
259
     * May be null in case of no selectable item.
260
     */
261
    public function getSelectedItemIndex() : ?int
262
    {
263
        return $this->selectedItemIndex;
264
    }
265
266
    /**
267
     * Get the currently selected item - if no items are selectable
268
     * then throw an exception.
269
     */
270
    public function getSelectedItem() : MenuItemInterface
271
    {
272
        if (null === $this->selectedItemIndex) {
273
            throw new \RuntimeException('No item is selected');
274
        }
275
        
276
        return $this->items[$this->selectedItemIndex];
277
    }
278
279
    public function getItems() : array
280
    {
281
        return $this->items;
282
    }
283
284
    /**
285
     * Can the item be selected
286
     * In this case, it indicates if at least 1 item inside the SplitItem can be selected
287
     */
288
    public function canSelect() : bool
289
    {
290
        return $this->canBeSelected;
291
    }
292
293
    /**
294
     * Execute the items callable if required
295
     */
296
    public function getSelectAction() : ?callable
297
    {
298
        return null;
299
    }
300
301
    /**
302
     * Whether or not the menu item is showing the menustyle extra value
303
     */
304
    public function showsItemExtra() : bool
305
    {
306
        return false;
307
    }
308
309
    /**
310
     * Enable showing item extra
311
     */
312
    public function showItemExtra() : void
313
    {
314
        //noop
315
    }
316
317
    /**
318
     * Disable showing item extra
319
     */
320
    public function hideItemExtra() : void
321
    {
322
        //noop
323
    }
324
325
    /**
326
     * Nothing to return with SplitItem
327
     */
328
    public function getText() : string
329
    {
330
        throw new \BadMethodCallException(sprintf('Not supported on: %s', __CLASS__));
331
    }
332
333
    /**
334
     * Finds largest itemExtra value in items
335
     */
336
    private function calculateItemExtra() : void
337
    {
338
        /** @var CheckboxItem|RadioItem|SelectableStyleInterface $item */
339
        foreach ($this->items as $item) {
340
            if (!$item->getStyle()->getDisplaysExtra()) {
341
                continue;
342
            }
343
344
            if (mb_strlen($item->getStyle()->getItemExtra()) < $this->largestItemExtra) {
345
                continue;
346
            }
347
348
            $this->largestItemExtra = mb_strlen($item->getStyle()->getItemExtra());
349
        }
350
    }
351
}
352