1 //////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright (c) 2004-2021 musikcube team
4 //
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions are met:
9 //
10 // * Redistributions of source code must retain the above copyright notice,
11 // this list of conditions and the following disclaimer.
12 //
13 // * Redistributions in binary form must reproduce the above copyright
14 // notice, this list of conditions and the following disclaimer in the
15 // documentation and/or other materials provided with the distribution.
16 //
17 // * Neither the name of the author nor the names of other contributors may
18 // be used to endorse or promote products derived from this software
19 // without specific prior written permission.
20 //
21 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
25 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 //
33 //////////////////////////////////////////////////////////////////////////////
34
35 #include <stdafx.h>
36 #include <algorithm>
37 #include <cursespp/ListWindow.h>
38 #include <cursespp/Scrollbar.h>
39
40 using namespace cursespp;
41
42 typedef IScrollAdapter::ScrollPosition ScrollPos;
43
44 size_t ListWindow::NO_SELECTION = (size_t) -1;
45
ListWindow(std::shared_ptr<IScrollAdapter> adapter,IWindow * parent)46 ListWindow::ListWindow(std::shared_ptr<IScrollAdapter> adapter, IWindow *parent)
47 : ScrollableWindow(adapter, parent)
48 , selectedIndex(0)
49 , showScrollbar(true) {
50
51 }
52
ListWindow(IWindow * parent)53 ListWindow::ListWindow(IWindow *parent)
54 : ListWindow(std::shared_ptr<IScrollAdapter>(), parent) {
55
56 }
57
~ListWindow()58 ListWindow::~ListWindow() {
59
60 }
61
SetScrollbarVisible(bool visible)62 void ListWindow::SetScrollbarVisible(bool visible) {
63 if (this->showScrollbar != visible) {
64 this->showScrollbar = visible;
65 this->Invalidate();
66 }
67 }
68
SetDecorator(Decorator decorator)69 void ListWindow::SetDecorator(Decorator decorator) {
70 this->decorator = decorator;
71 }
72
Invalidate()73 void ListWindow::Invalidate() {
74 this->DecorateFrame();
75 Window::Invalidate();
76 }
77
DecorateFrame()78 void ListWindow::DecorateFrame() {
79 if (this->decorator) {
80 this->decorator(this);
81 }
82
83 if (this->IsFrameVisible()) {
84 Scrollbar::Draw(this);
85 }
86 }
87
ScrollToTop()88 void ListWindow::ScrollToTop() {
89 this->SetSelectedIndex(0);
90 this->ScrollTo(0);
91 }
92
ScrollToBottom()93 void ListWindow::ScrollToBottom() {
94 IScrollAdapter& adapter = this->GetScrollAdapter();
95 this->SetSelectedIndex(std::max((size_t) 0, adapter.GetEntryCount() - 1));
96 this->ScrollTo(selectedIndex);
97 }
98
ScrollUp(int delta)99 void ListWindow::ScrollUp(int delta) {
100 IScrollAdapter& adapter = this->GetScrollAdapter();
101
102 if (adapter.GetEntryCount() > 0) {
103 ScrollPos spos = this->GetScrollPosition();
104
105 size_t first = spos.firstVisibleEntryIndex;
106 size_t last = first + spos.visibleEntryCount;
107 int drawIndex = (int) first;
108
109 int minIndex = 0;
110 int newIndex = (int) this->selectedIndex - delta;
111 newIndex = std::max(newIndex, minIndex);
112
113 if (newIndex < (int)first + 1) {
114 drawIndex = newIndex - 1;
115 }
116
117 drawIndex = std::max(0, drawIndex);
118
119 this->SetSelectedIndex(newIndex);
120 this->ScrollTo(drawIndex);
121 }
122 }
123
OnInvalidated()124 void ListWindow::OnInvalidated() {
125 this->Invalidated(this, this->GetSelectedIndex());
126 }
127
IsSelectedItemCompletelyVisible()128 bool ListWindow::IsSelectedItemCompletelyVisible() {
129 IScrollAdapter& adapter = this->GetScrollAdapter();
130 ScrollPos spos = this->GetScrollPosition();
131
132 size_t first = spos.firstVisibleEntryIndex;
133 size_t last = first + spos.visibleEntryCount - 1;
134
135 if (last <= spos.logicalIndex) {
136 /* get the height of all the visible items combined. */
137 int sum = 0;
138 for (size_t i = first; i <= spos.logicalIndex; i++) {
139 sum += adapter.GetEntry(this, i)->GetLineCount();
140 }
141
142 int delta = this->GetContentHeight() - sum;
143
144 /* special case -- the last item in the adapter is selected
145 and the heights match exactly -- we're at the end! */
146 if (delta == 0 && last == adapter.GetEntryCount() - 1) {
147 return true;
148 }
149
150 /* compare the visible height to the actual content
151 height. if the visible items are taller, and th selected
152 item is the last one, it means it's partially obscured. */
153 if (delta <= 0) {
154 return false;
155 }
156 }
157
158 return true;
159 }
160
ScrollDown(int delta)161 void ListWindow::ScrollDown(int delta) {
162 IScrollAdapter& adapter = this->GetScrollAdapter();
163
164 if (adapter.GetEntryCount() > 0) {
165 ScrollPos spos = this->GetScrollPosition();
166
167 size_t first = spos.firstVisibleEntryIndex;
168 size_t last = first + spos.visibleEntryCount;
169 size_t drawIndex = first;
170
171 size_t maxIndex = adapter.GetEntryCount() - 1;
172 size_t newIndex = this->selectedIndex + delta;
173 newIndex = std::min(newIndex, maxIndex);
174
175 if (newIndex >= last - 1) {
176 drawIndex = drawIndex + delta;
177 }
178
179 this->SetSelectedIndex(newIndex);
180
181 this->ScrollTo(drawIndex);
182
183 /* when scrolling down it's possible for the last item to be
184 the selection, and partially obscured. if we hit this case, we
185 just continue scrolling until the selected item is completely
186 visible to the user. */
187 while (!IsSelectedItemCompletelyVisible()) {
188 this->ScrollTo(++drawIndex);
189 }
190 }
191 }
192
PageUp()193 void ListWindow::PageUp() {
194 IScrollAdapter &adapter = this->GetScrollAdapter();
195 ScrollPos spos = this->GetScrollPosition();
196 int target = (int) this->GetPreviousPageEntryIndex();
197
198 /* if the target position is zero, let it be so the user can see
199 the top of the list. otherwise, scroll down by one to give indication
200 there is more to see. */
201 target = (target > 0) ? target + 1 : 0;
202
203 this->SetSelectedIndex((target == 0) ? 0 : target + 1);
204 this->ScrollTo(target);
205 }
206
PageDown()207 void ListWindow::PageDown() {
208 /* page down always makes the last item of this page, the first item
209 of the next page, and selects the following item. */
210
211 IScrollAdapter &adapter = this->GetScrollAdapter();
212 ScrollPos spos = this->GetScrollPosition();
213
214 size_t lastVisible = spos.firstVisibleEntryIndex + spos.visibleEntryCount - 1;
215 this->SetSelectedIndex(std::min(adapter.GetEntryCount() - 1, lastVisible + 1));
216
217 this->ScrollTo(lastVisible);
218 }
219
ScrollTo(size_t index)220 void ListWindow::ScrollTo(size_t index) {
221 this->GetScrollAdapter().DrawPage(
222 this, index, this->GetMutableScrollPosition());
223
224 this->Invalidate();
225 }
226
OnSelectionChanged(size_t newIndex,size_t oldIndex)227 void ListWindow::OnSelectionChanged(size_t newIndex, size_t oldIndex) {
228 /* for subclass use */
229 }
230
IsEntryVisible(size_t index)231 bool ListWindow::IsEntryVisible(size_t index) {
232 auto pos = this->GetScrollPosition();
233 size_t first = pos.firstVisibleEntryIndex;
234 size_t last = first + pos.visibleEntryCount;
235 return (index >= first && index < last);
236 }
237
SetSelectedIndex(size_t index)238 void ListWindow::SetSelectedIndex(size_t index) {
239 if (this->selectedIndex != index) {
240 if (index > this->GetScrollAdapter().GetEntryCount() &&
241 index != NO_SELECTION)
242 {
243 this->selectedIndex = NO_SELECTION;
244 return;
245 }
246
247 size_t prev = this->selectedIndex;
248 this->selectedIndex = index;
249
250 this->GetScrollAdapter().DrawPage(
251 this,
252 this->scrollPosition.firstVisibleEntryIndex,
253 this->GetMutableScrollPosition());
254
255 this->Invalidate();
256
257 this->OnSelectionChanged(index, prev); /* internal */
258 this->SelectionChanged(this, index, prev); /* external */
259 }
260 }
261
GetSelectedIndex()262 size_t ListWindow::GetSelectedIndex() {
263 if (this->selectedIndex >= this->GetScrollAdapter().GetEntryCount()) {
264 return NO_SELECTION;
265 }
266
267 return this->selectedIndex;
268 }
269
OnAdapterChanged()270 void ListWindow::OnAdapterChanged() {
271 IScrollAdapter *adapter = &GetScrollAdapter();
272
273 size_t count = adapter->GetEntryCount();
274
275 /* update initial state... */
276 if (selectedIndex == NO_SELECTION) {
277 if (count) {
278 this->SetSelectedIndex(0);
279 }
280 }
281 else if (count && selectedIndex >= count) {
282 if (count) {
283 this->SetSelectedIndex(count - 1);
284 }
285 }
286 else if (count == 0) {
287 this->SetSelectedIndex(NO_SELECTION);
288 }
289
290 this->ScrollTo(this->scrollPosition.firstVisibleEntryIndex);
291 }
292
OnDimensionsChanged()293 void ListWindow::OnDimensionsChanged() {
294 ScrollableWindow::OnDimensionsChanged();
295 this->ScrollTo(this->GetScrollPosition().firstVisibleEntryIndex);
296 }
297
KeyPress(const std::string & key)298 bool ListWindow::KeyPress(const std::string& key) {
299 if (key == "KEY_ENTER") {
300 auto selected = this->GetSelectedIndex();
301 if (selected != NO_SELECTION) {
302 return this->OnEntryActivated(selected);
303 }
304 }
305 else if (key == "M-enter") {
306 auto selected = this->GetSelectedIndex();
307 if (selected != NO_SELECTION) {
308 return this->OnEntryContextMenu(selected);
309 }
310 }
311 return ScrollableWindow::KeyPress(key);
312 }
313
MouseEvent(const IMouseHandler::Event & event)314 bool ListWindow::MouseEvent(const IMouseHandler::Event& event) {
315 /* CAL TODO: this method assumes each row is a single cell tall. */
316 bool result = ScrollableWindow::MouseEvent(event);
317
318 auto first = this->scrollPosition.firstVisibleEntryIndex;
319
320 if (first == NO_SELECTION) {
321 return result;
322 }
323
324 size_t offset = first + (size_t) event.y;
325
326 if (offset < this->GetScrollAdapter().GetEntryCount()) {
327 if (event.Button1Clicked()) {
328 this->SetSelectedIndex(offset);
329 }
330 if (event.Button3Clicked()) {
331 this->SetSelectedIndex(offset);
332 this->OnEntryContextMenu(offset);
333 }
334 else if (event.Button1DoubleClicked()) {
335 this->FocusInParent();
336 this->SetSelectedIndex(offset);
337 this->OnEntryActivated(offset); /* internal */
338 }
339 }
340
341 return result;
342 }
343
OnEntryActivated(size_t index)344 bool ListWindow::OnEntryActivated(size_t index) {
345 if (this->EntryActivated.has_connections()) {
346 this->EntryActivated(this, index);
347 return true;
348 }
349 return false;
350 }
351
OnEntryContextMenu(size_t index)352 bool ListWindow::OnEntryContextMenu(size_t index) {
353 if (this->EntryContextMenu.has_connections()) {
354 this->EntryContextMenu(this, index);
355 return true;
356 }
357 return false;
358 }
359
GetMutableScrollPosition()360 IScrollAdapter::ScrollPosition& ListWindow::GetMutableScrollPosition() {
361 this->scrollPosition.logicalIndex = this->GetSelectedIndex(); /* hack */
362 return this->scrollPosition;
363 }
364
GetScrollPosition()365 const IScrollAdapter::ScrollPosition& ListWindow::GetScrollPosition() {
366 return this->GetMutableScrollPosition();
367 }
368