Completed
Push — master ( 2edd9d...d97347 )
by Aydin
21s queued 11s
created

SplitItem::setStyle()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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