1 /***************************************************************************
2  *   Copyright (c) 2017-2019 by the fifechan team                               *
3  *   https://github.com/fifengine/fifechan                                 *
4  *   This file is part of fifechan.                                        *
5  *                                                                         *
6  *   fifechan is free software; you can redistribute it and/or             *
7  *   modify it under the terms of the GNU Lesser General Public            *
8  *   License as published by the Free Software Foundation; either          *
9  *   version 2.1 of the License, or (at your option) any later version.    *
10  *                                                                         *
11  *   This library is distributed in the hope that it will be useful,       *
12  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
13  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *
14  *   Lesser General Public License for more details.                       *
15  *                                                                         *
16  *   You should have received a copy of the GNU Lesser General Public      *
17  *   License along with this library; if not, write to the                 *
18  *   Free Software Foundation, Inc.,                                       *
19  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA          *
20  ***************************************************************************/
21 
22 /*      _______   __   __   __   ______   __   __   _______   __   __
23  *     / _____/\ / /\ / /\ / /\ / ____/\ / /\ / /\ / ___  /\ /  |\/ /\
24  *    / /\____\// / // / // / // /\___\// /_// / // /\_/ / // , |/ / /
25  *   / / /__   / / // / // / // / /    / ___  / // ___  / // /| ' / /
26  *  / /_// /\ / /_// / // / // /_/_   / / // / // /\_/ / // / |  / /
27  * /______/ //______/ //_/ //_____/\ /_/ //_/ //_/ //_/ //_/ /|_/ /
28  * \______\/ \______\/ \_\/ \_____\/ \_\/ \_\/ \_\/ \_\/ \_\/ \_\/
29  *
30  * Copyright (c) 2004 - 2008 Olof Naess�n and Per Larsson
31  *
32  *
33  * Per Larsson a.k.a finalman
34  * Olof Naess�n a.k.a jansem/yakslem
35  *
36  * Visit: http://guichan.sourceforge.net
37  *
38  * License: (BSD)
39  * Redistribution and use in source and binary forms, with or without
40  * modification, are permitted provided that the following conditions
41  * are met:
42  * 1. Redistributions of source code must retain the above copyright
43  *    notice, this list of conditions and the following disclaimer.
44  * 2. Redistributions in binary form must reproduce the above copyright
45  *    notice, this list of conditions and the following disclaimer in
46  *    the documentation and/or other materials provided with the
47  *    distribution.
48  * 3. Neither the name of Guichan nor the names of its contributors may
49  *    be used to endorse or promote products derived from this software
50  *    without specific prior written permission.
51  *
52  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
53  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
54  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
55  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
56  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
57  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
58  * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
59  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
60  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
61  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
62  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
63  */
64 
65 #include "fifechan/widgets/dropdown.hpp"
66 
67 #include "fifechan/exception.hpp"
68 #include "fifechan/font.hpp"
69 #include "fifechan/graphics.hpp"
70 #include "fifechan/key.hpp"
71 #include "fifechan/listmodel.hpp"
72 #include "fifechan/mouseinput.hpp"
73 #include "fifechan/widgets/listbox.hpp"
74 #include "fifechan/widgets/scrollarea.hpp"
75 
76 namespace fcn
77 {
DropDown(ListModel * listModel,ScrollArea * scrollArea,ListBox * listBox)78     DropDown::DropDown(ListModel *listModel,
79                        ScrollArea *scrollArea,
80                        ListBox *listBox)
81     {
82         setWidth(100);
83         setFocusable(true);
84         mDroppedDown = false;
85         mPushed = false;
86         mIsDragged = false;
87 
88         setInternalFocusHandler(&mInternalFocusHandler);
89 
90         mInternalScrollArea = (scrollArea == NULL);
91         mInternalListBox = (listBox == NULL);
92 
93         if (mInternalScrollArea)
94         {
95             mScrollArea = new ScrollArea();
96         }
97         else
98         {
99             mScrollArea = scrollArea;
100         }
101 
102         if (mInternalListBox)
103         {
104             mListBox = new ListBox();
105         }
106         else
107         {
108             mListBox = listBox;
109         }
110 
111         mScrollArea->setContent(mListBox);
112         add(mScrollArea);
113 
114         mListBox->addActionListener(this);
115         mListBox->addSelectionListener(this);
116 
117         setListModel(listModel);
118 
119         addMouseListener(this);
120         addKeyListener(this);
121         addFocusListener(this);
122 
123         adjustHeight();
124     }
125 
~DropDown()126     DropDown::~DropDown()
127     {
128         if (widgetExists(mListBox))
129         {
130             mListBox->removeActionListener(this);
131             mListBox->removeSelectionListener(this);
132         }
133 
134         if (mInternalScrollArea)
135         {
136             delete mScrollArea;
137         }
138 
139         if (mInternalListBox)
140         {
141             delete mListBox;
142         }
143 
144         setInternalFocusHandler(NULL);
145     }
146 
draw(Graphics * graphics)147     void DropDown::draw(Graphics* graphics)
148     {
149         int h;
150 
151         if (mDroppedDown)
152         {
153             h = mFoldedUpHeight;
154         }
155         else
156         {
157             h = getHeight();
158         }
159 
160         Color faceColor = getBaseColor();
161         Color highlightColor, shadowColor;
162         int alpha = getBaseColor().a;
163         highlightColor = faceColor + 0x303030;
164         highlightColor.a = alpha;
165         shadowColor = faceColor - 0x303030;
166         shadowColor.a = alpha;
167 
168         // Draw a border.
169         graphics->setColor(shadowColor);
170         graphics->drawLine(0, 0, getWidth() - 1, 0);
171         graphics->drawLine(0, 1, 0, h - 2);
172         graphics->setColor(highlightColor);
173         graphics->drawLine(getWidth() - 1, 1, getWidth() - 1, h - 1);
174         graphics->drawLine(0, h - 1, getWidth() - 1, h - 1);
175 
176          // Push a clip area so the other drawings don't need to worry
177         // about the border.
178         graphics->pushClipArea(Rectangle(1, 1, getWidth() - 2, h - 2));
179         const Rectangle currentClipArea = graphics->getCurrentClipArea();
180 
181         graphics->setColor(getBackgroundColor());
182         graphics->fillRectangle(0, 0, currentClipArea.width, currentClipArea.height);
183 
184         if (isFocused())
185         {
186             graphics->setColor(getSelectionColor());
187             graphics->fillRectangle(0,
188                                     0,
189                                     currentClipArea.width - currentClipArea.height,
190                                     currentClipArea.height);
191             graphics->setColor(getForegroundColor());
192         }
193 
194         if (mListBox->getListModel()
195             && mListBox->getSelected() >= 0)
196         {
197             graphics->setColor(getForegroundColor());
198             graphics->setFont(getFont());
199 
200             graphics->drawText(mListBox->getListModel()->getElementAt(mListBox->getSelected()), 1, 0);
201         }
202         else if (mListBox->getListModel()
203             && mListBox->getSelected() < 0)
204         {
205             graphics->setColor(getForegroundColor());
206             graphics->setFont(getFont());
207 
208             graphics->drawText("", 1, 0);
209         }
210 
211         // Push a clip area before drawing the button.
212         graphics->pushClipArea(Rectangle(currentClipArea.width - currentClipArea.height,
213                                          0,
214                                          currentClipArea.height,
215                                          currentClipArea.height));
216         drawButton(graphics);
217         graphics->popClipArea();
218         graphics->popClipArea();
219 
220          if (mDroppedDown)
221          {
222              // Draw a border around the children.
223              graphics->setColor(shadowColor);
224              graphics->drawRectangle(0,
225                                      mFoldedUpHeight,
226                                      getWidth(),
227                                      getHeight() - mFoldedUpHeight);
228              //drawChildren(graphics);
229          }
230     }
231 
drawButton(Graphics * graphics)232     void DropDown::drawButton(Graphics *graphics)
233     {
234         Color faceColor, highlightColor, shadowColor;
235         int offset;
236         int alpha = getBaseColor().a;
237 
238         if (mPushed)
239         {
240             faceColor = getBaseColor() - 0x303030;
241             faceColor.a = alpha;
242             highlightColor = faceColor - 0x303030;
243             highlightColor.a = alpha;
244             shadowColor = faceColor + 0x303030;
245             shadowColor.a = alpha;
246             offset = 1;
247         }
248         else
249         {
250             faceColor = getBaseColor();
251             faceColor.a = alpha;
252             highlightColor = faceColor + 0x303030;
253             highlightColor.a = alpha;
254             shadowColor = faceColor - 0x303030;
255             shadowColor.a = alpha;
256             offset = 0;
257         }
258 
259         const Rectangle currentClipArea = graphics->getCurrentClipArea();
260         graphics->setColor(highlightColor);
261         graphics->drawLine(0,
262                            0,
263                            currentClipArea.width - 1,
264                            0);
265         graphics->drawLine(0,
266                            1,
267                            0,
268                            currentClipArea.height - 1);
269         graphics->setColor(shadowColor);
270         graphics->drawLine(currentClipArea.width - 1,
271                            1,
272                            currentClipArea.width - 1,
273                            currentClipArea.height - 1);
274         graphics->drawLine(1,
275                            currentClipArea.height - 1,
276                            currentClipArea.width - 2,
277                            currentClipArea.height - 1);
278 
279         graphics->setColor(faceColor);
280         graphics->fillRectangle(1,
281                                 1,
282                                 currentClipArea.width - 2,
283                                 currentClipArea.height - 2);
284 
285         graphics->setColor(getForegroundColor());
286 
287         int i;
288         int n = currentClipArea.height / 3;
289         int dx = currentClipArea.height / 2;
290         int dy = (currentClipArea.height * 2) / 3;
291         for (i = 0; i < n; i++)
292         {
293             graphics->drawLine(dx - i + offset,
294                                dy - i + offset,
295                                dx + i + offset,
296                                dy - i + offset);
297         }
298     }
299 
getSelected() const300     int DropDown::getSelected() const
301     {
302         return mListBox->getSelected();
303     }
304 
setSelected(int selected)305     void DropDown::setSelected(int selected)
306     {
307         mListBox->setSelected(selected);
308     }
309 
keyPressed(KeyEvent & keyEvent)310     void DropDown::keyPressed(KeyEvent& keyEvent)
311     {
312         if (keyEvent.isConsumed())
313             return;
314 
315         Key key = keyEvent.getKey();
316 
317         if ((key.getValue() == Key::Enter || key.getValue() == Key::Space)
318             && !mDroppedDown)
319         {
320             dropDown();
321             keyEvent.consume();
322         }
323         else if (key.getValue() == Key::Up)
324         {
325             setSelected(getSelected() - 1);
326             keyEvent.consume();
327         }
328         else if (key.getValue() == Key::Down)
329         {
330             setSelected(getSelected() + 1);
331             keyEvent.consume();
332         }
333     }
334 
mousePressed(MouseEvent & mouseEvent)335     void DropDown::mousePressed(MouseEvent& mouseEvent)
336     {
337         // If we have a mouse press on the widget.
338         if (0 <= mouseEvent.getY()
339             && mouseEvent.getY() < getHeight()
340             && mouseEvent.getX() >= 0
341             && mouseEvent.getX() < getWidth()
342             && mouseEvent.getButton() == MouseEvent::Left
343             && !mDroppedDown
344             && mouseEvent.getSource() == this)
345         {
346             mPushed = true;
347             dropDown();
348             requestModalMouseInputFocus();
349         }
350         // Fold up the listbox if the upper part is clicked after fold down
351         else if (0 <= mouseEvent.getY()
352                  && mouseEvent.getY() < mFoldedUpHeight
353                  && mouseEvent.getX() >= 0
354                  && mouseEvent.getX() < getWidth()
355                  && mouseEvent.getButton() == MouseEvent::Left
356                  && mDroppedDown
357                  && mouseEvent.getSource() == this)
358         {
359             mPushed = false;
360             foldUp();
361             releaseModalMouseInputFocus();
362         }
363         // If we have a mouse press outside the widget
364         else if (0 > mouseEvent.getY()
365                  || mouseEvent.getY() >= getHeight()
366                  || mouseEvent.getX() < 0
367                  || mouseEvent.getX() >= getWidth())
368         {
369             mPushed = false;
370             foldUp();
371         }
372     }
373 
mouseReleased(MouseEvent & mouseEvent)374     void DropDown::mouseReleased(MouseEvent& mouseEvent)
375     {
376         if (mIsDragged)
377         {
378             mPushed = false;
379         }
380 
381         // Released outside of widget. Can happen when we have modal input focus.
382         if ((0 > mouseEvent.getY()
383             || mouseEvent.getY() >= getHeight()
384             || mouseEvent.getX() < 0
385             || mouseEvent.getX() >= getWidth())
386             && mouseEvent.getButton() == MouseEvent::Left
387             && isModalMouseInputFocused())
388         {
389             releaseModalMouseInputFocus();
390 
391             if (mIsDragged)
392             {
393                 foldUp();
394             }
395         }
396         else if (mouseEvent.getButton() == MouseEvent::Left)
397         {
398             mPushed = false;
399         }
400 
401         mIsDragged = false;
402     }
403 
mouseDragged(MouseEvent & mouseEvent)404     void DropDown::mouseDragged(MouseEvent& mouseEvent)
405     {
406         mIsDragged = true;
407 
408         mouseEvent.consume();
409     }
410 
setListModel(ListModel * listModel)411     void DropDown::setListModel(ListModel *listModel)
412     {
413         mListBox->setListModel(listModel);
414 
415         adjustHeight();
416     }
417 
getListModel() const418     ListModel *DropDown::getListModel() const
419     {
420         return mListBox->getListModel();
421     }
422 
adjustHeight()423     void DropDown::adjustHeight()
424     {
425         if (mScrollArea == NULL)
426         {
427             throw FCN_EXCEPTION("Scroll area has been deleted.");
428         }
429 
430         if (mListBox == NULL)
431         {
432             throw FCN_EXCEPTION("List box has been deleted.");
433         }
434 
435         int listBoxHeight = mListBox->getHeight();
436 
437         // We add 2 for the border
438         int h2 = getFont()->getHeight() + 2;
439 
440         setHeight(h2);
441 
442         // The addition/subtraction of 2 compensates for the seperation lines
443         // seperating the selected element view and the scroll area.
444 
445         if (mDroppedDown && getParent())
446         {
447             int h = getParent()->getChildrenArea().height - getY();
448 
449             if (listBoxHeight > h - h2 - 2)
450             {
451                 mScrollArea->setHeight(h - h2 - 2);
452                 setHeight(h);
453             }
454             else
455             {
456                 setHeight(listBoxHeight + h2 + 2);
457                 mScrollArea->setHeight(listBoxHeight);
458             }
459         }
460 
461         mScrollArea->setWidth(getWidth());
462         // Resize the ListBox to exactly fit the ScrollArea.
463         mListBox->setWidth(mScrollArea->getChildrenArea().width);
464         mScrollArea->setPosition(0, 0);
465     }
466 
resizeToContent(bool recursiv)467     void DropDown::resizeToContent(bool recursiv) {
468         if (mScrollArea != NULL) {
469             mScrollArea->resizeToContent();
470         }
471 
472         if (mListBox != NULL) {
473             mScrollArea->resizeToContent();
474         }
475         adjustHeight();
476     }
477 
adjustSize()478     void DropDown::adjustSize() {
479         adjustHeight();
480     }
481 
dropDown()482     void DropDown::dropDown()
483     {
484         if (!mDroppedDown)
485         {
486             mDroppedDown = true;
487             mFoldedUpHeight = getHeight();
488             adjustHeight();
489 
490 
491             if (getParent())
492             {
493                 getParent()->moveToTop(this);
494             }
495         }
496 
497         mListBox->requestFocus();
498     }
499 
foldUp()500     void DropDown::foldUp()
501     {
502         if (mDroppedDown)
503         {
504             mDroppedDown = false;
505             adjustHeight();
506             mInternalFocusHandler.focusNone();
507         }
508     }
509 
focusLost(const Event & event)510     void DropDown::focusLost(const Event& event)
511     {
512         foldUp();
513         mInternalFocusHandler.focusNone();
514     }
515 
516 
death(const Event & event)517     void DropDown::death(const Event& event)
518     {
519         if (event.getSource() == mScrollArea)
520         {
521             mScrollArea = NULL;
522         }
523     }
524 
action(const ActionEvent & actionEvent)525     void DropDown::action(const ActionEvent& actionEvent)
526     {
527         foldUp();
528         releaseModalMouseInputFocus();
529         distributeActionEvent();
530     }
531 
getChildrenArea()532     Rectangle DropDown::getChildrenArea()
533     {
534         if (mDroppedDown)
535         {
536             // Calculate the children area (with the one pixel border in mind)
537             return Rectangle(1,
538                              mFoldedUpHeight + 1,
539                              getWidth() - 2,
540                              getHeight() - mFoldedUpHeight - 2);
541         }
542 
543         return Rectangle();
544     }
545 
setBaseColor(const Color & color)546     void DropDown::setBaseColor(const Color& color)
547     {
548         if (mInternalScrollArea)
549         {
550             mScrollArea->setBaseColor(color);
551         }
552 
553         if (mInternalListBox)
554         {
555             mListBox->setBaseColor(color);
556         }
557 
558         Widget::setBaseColor(color);
559     }
560 
setBackgroundColor(const Color & color)561     void DropDown::setBackgroundColor(const Color& color)
562     {
563         if (mInternalScrollArea)
564         {
565             mScrollArea->setBackgroundColor(color);
566         }
567 
568         if (mInternalListBox)
569         {
570             mListBox->setBackgroundColor(color);
571         }
572 
573         Widget::setBackgroundColor(color);
574     }
575 
setForegroundColor(const Color & color)576     void DropDown::setForegroundColor(const Color& color)
577     {
578         if (mInternalScrollArea)
579         {
580             mScrollArea->setForegroundColor(color);
581         }
582 
583         if (mInternalListBox)
584         {
585             mListBox->setForegroundColor(color);
586         }
587 
588         Widget::setForegroundColor(color);
589     }
590 
setFont(Font * font)591     void DropDown::setFont(Font *font)
592     {
593         if (mInternalScrollArea)
594         {
595             mScrollArea->setFont(font);
596         }
597 
598         if (mInternalListBox)
599         {
600             mListBox->setFont(font);
601         }
602 
603         Widget::setFont(font);
604     }
605 
mouseWheelMovedUp(MouseEvent & mouseEvent)606     void DropDown::mouseWheelMovedUp(MouseEvent& mouseEvent)
607     {
608         if (isFocused() && mouseEvent.getSource() == this)
609         {
610             mouseEvent.consume();
611 
612             if (mListBox->getSelected() > 0)
613             {
614                 mListBox->setSelected(mListBox->getSelected() - 1);
615             }
616         }
617     }
618 
mouseWheelMovedDown(MouseEvent & mouseEvent)619     void DropDown::mouseWheelMovedDown(MouseEvent& mouseEvent)
620     {
621         if (isFocused() && mouseEvent.getSource() == this)
622         {
623             mouseEvent.consume();
624 
625             mListBox->setSelected(mListBox->getSelected() + 1);
626         }
627     }
628 
setSelectionColor(const Color & color)629     void DropDown::setSelectionColor(const Color& color)
630     {
631         Widget::setSelectionColor(color);
632 
633         if (mInternalListBox)
634         {
635             mListBox->setSelectionColor(color);
636         }
637     }
638 
valueChanged(const SelectionEvent & event)639     void DropDown::valueChanged(const SelectionEvent& event)
640     {
641         distributeValueChangedEvent();
642     }
643 
addSelectionListener(SelectionListener * selectionListener)644     void DropDown::addSelectionListener(SelectionListener* selectionListener)
645     {
646         mSelectionListeners.push_back(selectionListener);
647     }
648 
removeSelectionListener(SelectionListener * selectionListener)649     void DropDown::removeSelectionListener(SelectionListener* selectionListener)
650     {
651         mSelectionListeners.remove(selectionListener);
652     }
653 
distributeValueChangedEvent()654     void DropDown::distributeValueChangedEvent()
655     {
656         SelectionListenerIterator iter;
657 
658         for (iter = mSelectionListeners.begin(); iter != mSelectionListeners.end(); ++iter)
659         {
660             SelectionEvent event(this);
661             (*iter)->valueChanged(event);
662         }
663     }
664 }
665 
666