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