e7272b3960d5268fb6b48e67c88c4df465676af8
[vlc.git] / modules / gui / skins2 / controls / ctrl_tree.cpp
1 /*****************************************************************************
2  * ctrl_tree.cpp
3  *****************************************************************************
4  * Copyright (C) 2003 VideoLAN
5  * $Id$
6  *
7  * Authors: Antoine Cellerier <dionoea@videolan.org>
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
22  *****************************************************************************/
23
24 #include <math.h>
25 #include "ctrl_tree.hpp"
26 #include "../src/os_factory.hpp"
27 #include "../src/os_graphics.hpp"
28 #include "../src/generic_bitmap.hpp"
29 #include "../src/generic_font.hpp"
30 #include "../src/scaled_bitmap.hpp"
31 #include "../utils/position.hpp"
32 #include "../utils/ustring.hpp"
33 #include "../events/evt_key.hpp"
34 #include "../events/evt_mouse.hpp"
35 #include "../events/evt_scroll.hpp"
36 #include "vlc_keys.h"
37 #ifdef sun
38 #   include "solaris_specific.h" // for lrint
39 #endif
40
41 #define SCROLL_STEP 0.05
42 #define LINE_INTERVAL 1  // Number of pixels inserted between 2 lines
43
44
45 CtrlTree::CtrlTree( intf_thread_t *pIntf,
46                     VarTree &rTree,
47                     const GenericFont &rFont,
48                     const GenericBitmap *pBgBitmap,
49                     const GenericBitmap *pItemBitmap,
50                     const GenericBitmap *pOpenBitmap,
51                     const GenericBitmap *pClosedBitmap,
52                     uint32_t fgColor,
53                     uint32_t playColor,
54                     uint32_t bgColor1,
55                     uint32_t bgColor2,
56                     uint32_t selColor,
57                     const UString &rHelp,
58                     VarBool *pVisible ):
59     CtrlGeneric( pIntf,rHelp, pVisible), m_rTree( rTree), m_rFont( rFont ),
60     m_pBgBitmap( pBgBitmap ), m_pItemBitmap( pItemBitmap ),
61     m_pOpenBitmap( pOpenBitmap ), m_pClosedBitmap( pClosedBitmap ),
62     m_fgColor( fgColor ), m_playColor( playColor ), m_bgColor1( bgColor1 ),
63     m_bgColor2( bgColor2 ), m_selColor( selColor ),
64     m_pLastSelected( NULL ), m_pImage( NULL ), m_dontMove( false )
65 {
66     // Observe the tree and position variables
67     m_rTree.addObserver( this );
68     m_rTree.getPositionVar().addObserver( this );
69
70     m_firstPos = m_rTree.begin();
71
72     makeImage();
73 }
74
75 CtrlTree::~CtrlTree()
76 {
77     m_rTree.getPositionVar().delObserver( this );
78     m_rTree.delObserver( this );
79     if( m_pImage )
80     {
81         delete m_pImage;
82     }
83 }
84
85 int CtrlTree::itemHeight()
86 {
87     int itemHeight = m_rFont.getSize();
88     if( m_pClosedBitmap )
89     {
90         itemHeight = __MAX( m_pClosedBitmap->getHeight(), itemHeight );
91     }
92     if( m_pOpenBitmap )
93     {
94         itemHeight = __MAX( m_pOpenBitmap->getHeight(), itemHeight );
95     }
96     if( m_pItemBitmap )
97     {
98         itemHeight = __MAX( m_pItemBitmap->getHeight(), itemHeight );
99     }
100     itemHeight += LINE_INTERVAL;
101     return itemHeight;
102 }
103
104 int CtrlTree::itemImageWidth()
105 {
106     int bitmapWidth = 5;
107     if( m_pClosedBitmap )
108     {
109         bitmapWidth = __MAX( m_pClosedBitmap->getWidth(), bitmapWidth );
110     }
111     if( m_pOpenBitmap )
112     {
113         bitmapWidth = __MAX( m_pOpenBitmap->getWidth(), bitmapWidth );
114     }
115     if( m_pItemBitmap )
116     {
117         bitmapWidth = __MAX( m_pItemBitmap->getWidth(), bitmapWidth );
118     }
119     return bitmapWidth + 2;
120 }
121
122 int CtrlTree::maxItems()
123 {
124     const Position *pPos = getPosition();
125     if( !pPos )
126     {
127         return -1;
128     }
129     return pPos->getHeight() / itemHeight();
130 }
131
132
133 void CtrlTree::onUpdate( Subject<VarTree, tree_update*> &rTree,
134                          tree_update *arg )
135 {
136     if( arg->i_type == 0 ) // Item update
137     {
138         if( arg->b_active_item )
139         {
140             autoScroll();
141             ///\todo We should make image if we are visible in the view
142             makeImage();
143         }
144     }
145     /// \todo handle delete in a more clever way
146     else if ( arg->i_type == 1 ) // Global change or deletion
147     {
148         m_firstPos = m_rTree.begin();
149         makeImage();
150     }
151     else if ( arg->i_type == 2 ) // Item-append
152     {
153         /// \todo Check if the item is really visible in the view (we only check if it in the document)
154         if( arg->b_visible == true )
155         {
156             makeImage();
157         }
158     }
159     notifyLayout();
160 }
161
162 void CtrlTree::onUpdate( Subject<VarPercent, void*> &rPercent, void* arg)
163 {
164     // Determine what is the first item to display
165     VarTree::Iterator it = m_rTree.begin();
166
167     if( m_dontMove ) return;
168
169     int excessItems = m_rTree.visibleItems() - maxItems();
170
171     if( excessItems > 0)
172     {
173         VarPercent &rVarPos = m_rTree.getPositionVar();
174         // a simple (int)(...) causes rounding errors !
175 #ifdef _MSC_VER
176 #   define lrint (int)
177 #endif
178         it = m_rTree.getVisibleItem(lrint( (1.0 - rVarPos.get()) * (double)excessItems ) + 1);
179     }
180     if( m_firstPos != it )
181     {
182         // Redraw the control if the position has changed
183         m_firstPos = it;
184         makeImage();
185         notifyLayout();
186     }
187 }
188
189 void CtrlTree::onResize()
190 {
191     // Determine what is the first item to display
192     VarTree::Iterator it = m_rTree.begin();
193
194     int excessItems = m_rTree.visibleItems() - maxItems();
195
196     if( excessItems > 0)
197     {
198         VarPercent &rVarPos = m_rTree.getPositionVar();
199         // a simple (int)(...) causes rounding errors !
200 #ifdef _MSC_VER
201 #   define lrint (int)
202 #endif
203         it = m_rTree.getVisibleItem(lrint( (1.0 - rVarPos.get()) * (double)excessItems ) + 1);
204     }
205     // Redraw the control if the position has changed
206     m_firstPos = it;
207     makeImage();
208     notifyLayout();
209 }
210
211 void CtrlTree::onPositionChange()
212 {
213     makeImage();
214     notifyLayout();
215 }
216
217 void CtrlTree::handleEvent( EvtGeneric &rEvent )
218 {
219     bool bChangedPosition = false;
220     VarTree::Iterator toShow; bool needShow = false;
221     if( rEvent.getAsString().find( "key:down" ) != string::npos )
222     {
223         int key = ((EvtKey&)rEvent).getKey();
224         VarTree::Iterator it;
225         bool previousWasSelected = false;
226
227         /* Delete the selection */
228         if( key == KEY_DELETE )
229         {
230             m_rTree.delSelected();
231         }
232         else if( key == KEY_PAGEDOWN )
233         {
234             it = m_firstPos;
235             int i = (int)(maxItems()*1.5);
236             while( i >= 0 )
237             {
238                 VarTree::Iterator it_old = it;
239                 it = m_rTree.getNextVisibleItem( it );
240                 /* End is already visible, dont' scroll */
241                 if( it == m_rTree.end() )
242                 {
243                     it = it_old;
244                     break;
245                 }
246                 needShow = true;
247                 i--;
248             }
249             if( needShow )
250             {
251                 ensureVisible( it );
252                 makeImage();
253                 notifyLayout();
254                 return;
255             }
256         }
257         else if (key == KEY_PAGEUP )
258         {
259             it = m_firstPos;
260             int i = maxItems();
261             while( i >= maxItems()/2 )
262             {
263                 it = m_rTree.getPrevVisibleItem( it );
264                 /* End is already visible, dont' scroll */
265                 if( it == m_rTree.begin() )
266                 {
267                     break;
268                 }
269                 i--;
270             }
271             ensureVisible( it );
272             makeImage();
273             notifyLayout();
274             return;
275         }
276
277
278         for( it = m_rTree.begin(); it != m_rTree.end();
279              it = m_rTree.getNextVisibleItem( it ) )
280         {
281             VarTree::Iterator next = m_rTree.getNextVisibleItem( it );
282             if( key == KEY_UP )
283             {
284                 // Scroll up one item
285                 if( ( it->parent()
286                       && it != it->parent()->begin() )
287                     || &*it != m_pLastSelected )
288                 {
289                     bool nextWasSelected = ( &*next == m_pLastSelected );
290                     it->m_selected = nextWasSelected;
291                     if( nextWasSelected )
292                     {
293                         m_pLastSelected = &*it;
294                         needShow = true; toShow = it;
295                     }
296                 }
297             }
298             else if( key == KEY_DOWN )
299             {
300                 // Scroll down one item
301                 if( ( it->parent()
302                       && next != it->parent()->end() )
303                     || &*it != m_pLastSelected )
304                 {
305                     (*it).m_selected = previousWasSelected;
306                 }
307                 if( previousWasSelected )
308                 {
309                     m_pLastSelected = &*it;
310                     needShow = true; toShow = it;
311                     previousWasSelected = false;
312                 }
313                 else
314                 {
315                     previousWasSelected = ( &*it == m_pLastSelected );
316                 }
317             }
318             else if( key == KEY_RIGHT )
319             {
320                 // Go down one level (and expand node)
321                 if( &*it == m_pLastSelected )
322                 {
323                     if( it->m_expanded )
324                     {
325                         if( it->size() )
326                         {
327                             it->m_selected = false;
328                             it->begin()->m_selected = true;
329                             m_pLastSelected = &*(it->begin());
330                         }
331                         else
332                         {
333                             m_rTree.action( &*it );
334                         }
335                     }
336                     else
337                     {
338                         it->m_expanded = true;
339                         bChangedPosition = true;
340                     }
341                 }
342             }
343             else if( key == KEY_LEFT )
344             {
345                 // Go up one level (and close node)
346                 if( &*it == m_pLastSelected )
347                 {
348                     if( it->m_expanded && it->size() )
349                     {
350                         it->m_expanded = false;
351                         bChangedPosition = true;
352                     }
353                     else
354                     {
355                         if( it->parent() && it->parent() != &m_rTree)
356                         {
357                             it->m_selected = false;
358                             m_pLastSelected = it->parent();
359                             m_pLastSelected->m_selected = true;
360                         }
361                     }
362                 }
363             }
364             else if( key == KEY_ENTER || key == KEY_SPACE )
365             {
366                 // Go up one level (and close node)
367                 if( &*it == m_pLastSelected )
368                 {
369                     m_rTree.action( &*it );
370                 }
371             }
372         }
373         if( needShow )
374             ensureVisible( toShow );
375
376         // Redraw the control
377         makeImage();
378         notifyLayout();
379     }
380
381     else if( rEvent.getAsString().find( "mouse:left" ) != string::npos )
382     {
383         EvtMouse &rEvtMouse = (EvtMouse&)rEvent;
384         const Position *pos = getPosition();
385         int yPos = ( rEvtMouse.getYPos() - pos->getTop() ) / itemHeight();
386         int xPos = rEvtMouse.getXPos() - pos->getLeft();
387         VarTree::Iterator it;
388
389         if( rEvent.getAsString().find( "mouse:left:down:ctrl,shift" ) !=
390             string::npos )
391         {
392             VarTree::Iterator itClicked = findItemAtPos( yPos );
393             // Flag to know if the current item must be selected
394             bool select = false;
395             for( it = m_rTree.begin(); it != m_rTree.end();
396                  it = m_rTree.getNextVisibleItem( it ) )
397             {
398                 bool nextSelect = select;
399                 if( it == itClicked || &*it == m_pLastSelected )
400                 {
401                     if( select )
402                     {
403                         nextSelect = false;
404                     }
405                     else
406                     {
407                         select = true;
408                         nextSelect = true;
409                     }
410                 }
411                 it->m_selected = (*it).m_selected || select;
412                 select = nextSelect;
413             }
414         }
415         else if( rEvent.getAsString().find( "mouse:left:down:ctrl" ) !=
416                  string::npos )
417         {
418             // Invert the selection of the item
419             it = findItemAtPos( yPos );
420             if( it != m_rTree.end() )
421             {
422                 it->m_selected = !it->m_selected;
423                 m_pLastSelected = &*it;
424             }
425         }
426         else if( rEvent.getAsString().find( "mouse:left:down:shift" ) !=
427                  string::npos )
428         {
429             VarTree::Iterator itClicked = findItemAtPos( yPos );
430             // Flag to know if the current item must be selected
431             bool select = false;
432             for( it = m_rTree.begin(); it != m_rTree.end();
433                  it = m_rTree.getNextVisibleItem( it ) )
434             {
435                 bool nextSelect = select;
436                 if( it == itClicked || &*it == m_pLastSelected )
437                 {
438                     if( select )
439                     {
440                         nextSelect = false;
441                     }
442                     else
443                     {
444                         select = true;
445                         nextSelect = true;
446                     }
447                 }
448                 it->m_selected = select;
449                 select = nextSelect;
450             }
451         }
452         else if( rEvent.getAsString().find( "mouse:left:down" ) !=
453                  string::npos )
454         {
455             it = findItemAtPos(yPos);
456             if( it->size() && xPos > (it->depth() - 1) * itemImageWidth()
457                 && xPos < it->depth() * itemImageWidth() )
458             {
459                 // Fold/unfold the item
460                 it->m_expanded = !it->m_expanded;
461                 bChangedPosition = true;
462             }
463             else
464             {
465                 // Unselect any previously selected item
466                 VarTree::Iterator it2;
467                 for( it2 = m_rTree.begin(); it2 != m_rTree.end();
468                      it2 = m_rTree.getNextVisibleItem( it2 ) )
469                 {
470                     it2->m_selected = false;
471                 }
472                 // Select the new item
473                 if( it != m_rTree.end() )
474                 {
475                     it->m_selected = true;
476                     m_pLastSelected = &*it;
477                 }
478             }
479         }
480
481         else if( rEvent.getAsString().find( "mouse:left:dblclick" ) !=
482                  string::npos )
483         {
484             it = findItemAtPos(yPos);
485             if( it != m_rTree.end() )
486             {
487                // Execute the action associated to this item
488                m_rTree.action( &*it );
489             }
490         }
491         // Redraw the control
492         makeImage();
493         notifyLayout();
494     }
495
496     else if( rEvent.getAsString().find( "scroll" ) != string::npos )
497     {
498         int direction = ((EvtScroll&)rEvent).getDirection();
499
500         double percentage = m_rTree.getPositionVar().get();
501         double step = 2.0 / (double)m_rTree.visibleItems();
502         if( direction == EvtScroll::kUp )
503         {
504             percentage += step;
505         }
506         else
507         {
508             percentage -= step;
509         }
510         m_rTree.getPositionVar().set( percentage );
511     }
512
513     /* We changed the nodes, let's fix teh position var */
514     if( bChangedPosition )
515     {
516         VarTree::Iterator it;
517         int i = 0;
518         int iFirst = 0;
519         for( it = m_rTree.begin(); it != m_rTree.end();
520              it = m_rTree.getNextVisibleItem( it ) )
521         {
522             i++;
523             if( it == m_firstPos )
524             {
525                 iFirst = i;
526                 break;
527             }
528         }
529         iFirst += maxItems();
530         if( iFirst >= m_rTree.visibleItems() ) iFirst = m_rTree.visibleItems();
531         float f_new = (float)iFirst / (float)m_rTree.visibleItems();
532         m_dontMove = true;
533         m_rTree.getPositionVar().set( 1.0 - f_new );
534         m_dontMove = false;
535     }
536 }
537
538 bool CtrlTree::mouseOver( int x, int y ) const
539 {
540     const Position *pPos = getPosition();
541     return ( pPos
542        ? x >= 0 && x <= pPos->getWidth() && y >= 0 && y <= pPos->getHeight()
543        : false);
544 }
545
546 void CtrlTree::draw( OSGraphics &rImage, int xDest, int yDest )
547 {
548     if( m_pImage )
549     {
550         rImage.drawGraphics( *m_pImage, 0, 0, xDest, yDest );
551     }
552 }
553
554 bool CtrlTree::ensureVisible( VarTree::Iterator item )
555 {
556     // Find the item to focus
557     int focusItemIndex = 0;
558     VarTree::Iterator it;
559
560     m_rTree.ensureExpanded( item );
561
562     for( it = m_rTree.begin(); it != m_rTree.end();
563          it = m_rTree.getNextVisibleItem( it ) )
564     {
565         if( it->m_id == item->m_id ) break;
566         focusItemIndex++;
567     }
568    return ensureVisible( focusItemIndex );
569 }
570
571 bool CtrlTree::ensureVisible( int focusItemIndex )
572 {
573     // Find  m_firstPos
574     VarTree::Iterator it;
575     int firstPosIndex = 0;
576     for( it = m_rTree.begin(); it != m_rTree.end();
577          it = m_rTree.getNextVisibleItem( it ) )
578     {
579         if( it == m_firstPos ) break;
580         firstPosIndex++;
581     }
582
583     if( it == m_rTree.end() ) return false;
584
585
586     if( it != m_rTree.end()
587         && ( focusItemIndex < firstPosIndex
588            || focusItemIndex > firstPosIndex + maxItems() ) )
589     {
590         // Scroll to have the wanted stream visible
591         VarPercent &rVarPos = m_rTree.getPositionVar();
592         rVarPos.set( 1.0 - (double)focusItemIndex /
593                            (double)m_rTree.visibleItems() );
594         return true;
595     }
596     return false;
597 }
598
599 void CtrlTree::autoScroll()
600 {
601     // Find the current playing stream
602     int playIndex = 0;
603     VarTree::Iterator it;
604
605     for( it = m_rTree.begin(); it != m_rTree.end();
606          it = m_rTree.getNextItem( it ) )
607     {
608         if( it->m_playing )
609         {
610            m_rTree.ensureExpanded( it );
611            break;
612         }
613     }
614     for( it = m_rTree.begin(); it != m_rTree.end();
615          it = m_rTree.getNextVisibleItem( it ) )
616     {
617         if( it->m_playing )
618            break;
619         playIndex++;
620     }
621
622     if( it == m_rTree.end() ) return;
623
624
625     ensureVisible( playIndex );
626 }
627
628
629 void CtrlTree::makeImage()
630 {
631     if( m_pImage )
632     {
633         delete m_pImage;
634     }
635
636     // Get the size of the control
637     const Position *pPos = getPosition();
638     if( !pPos )
639     {
640         return;
641     }
642     int width = pPos->getWidth();
643     int height = pPos->getHeight();
644
645     int i_itemHeight = itemHeight();
646
647     // Create an image
648     OSFactory *pOsFactory = OSFactory::instance( getIntf() );
649     m_pImage = pOsFactory->createOSGraphics( width, height );
650
651     VarTree::Iterator it = m_firstPos;
652
653     if( m_pBgBitmap )
654     {
655         // Draw the background bitmap
656         ScaledBitmap bmp( getIntf(), *m_pBgBitmap, width, height );
657         m_pImage->drawBitmap( bmp, 0, 0 );
658
659         for( int yPos = 0; yPos < height; yPos += i_itemHeight )
660         {
661             if( it != m_rTree.end() )
662             {
663                 if( (*it).m_selected )
664                 {
665                     int rectHeight = __MIN( i_itemHeight, height - yPos );
666                     m_pImage->fillRect( 0, yPos, width, rectHeight,
667                                         m_selColor );
668                 }
669                 it = m_rTree.getNextVisibleItem( it );
670             }
671         }
672     }
673     else
674     {
675         // FIXME (TRYME)
676         // Fill background with background color
677         uint32_t bgColor = m_bgColor1;
678         m_pImage->fillRect( 0, 0, width, height, bgColor );
679         for( int yPos = 0; yPos < height; yPos += i_itemHeight )
680         {
681             int rectHeight = __MIN( i_itemHeight, height - yPos );
682             if( it != m_rTree.end() )
683             {
684                 uint32_t color = ( it->m_selected ? m_selColor : bgColor );
685                 m_pImage->fillRect( 0, yPos, width, rectHeight, color );
686                 it = m_rTree.getNextVisibleItem( it );
687             }
688             else
689             {
690                 m_pImage->fillRect( 0, yPos, width, rectHeight, bgColor );
691             }
692             bgColor = ( bgColor == m_bgColor1 ? m_bgColor2 : m_bgColor1 );
693         }
694     }
695
696     int bitmapWidth = itemImageWidth();
697
698     int yPos = 0;
699     it = m_firstPos;
700     while( it != m_rTree.end() && yPos < height )
701     {
702         const GenericBitmap *m_pCurBitmap;
703         UString *pStr = (UString*)(it->m_cString.get());
704         uint32_t color = ( it->m_playing ? m_playColor : m_fgColor );
705
706         // Draw the text
707         if( pStr != NULL )
708         {
709             int depth = it->depth();
710             GenericBitmap *pText = m_rFont.drawString( *pStr, color, width - bitmapWidth * depth );
711             if( !pText )
712             {
713                 return;
714             }
715             if( it->size() )
716                 m_pCurBitmap = it->m_expanded ? m_pOpenBitmap : m_pClosedBitmap;
717             else
718                 m_pCurBitmap = m_pItemBitmap;
719
720             if( m_pCurBitmap )
721             {
722                 // Make sure we are centered on the line
723                 int yPos2 = yPos+(i_itemHeight-m_pCurBitmap->getHeight()+1)/2;
724                 if( yPos2 >= height )
725                 {
726                     delete pText;
727                     break;
728                 }
729                 m_pImage->drawBitmap( *m_pCurBitmap, 0, 0,
730                                       bitmapWidth * (depth - 1 ), yPos2,
731                                       m_pCurBitmap->getWidth(),
732                                       __MIN( m_pCurBitmap->getHeight(),
733                                              height -  yPos2), true );
734             }
735             yPos += i_itemHeight - pText->getHeight();
736             int ySrc = 0;
737             if( yPos < 0 )
738             {
739                 ySrc = - yPos;
740                 yPos = 0;
741             }
742             int lineHeight = __MIN( pText->getHeight() - ySrc, height - yPos );
743             m_pImage->drawBitmap( *pText, 0, ySrc, bitmapWidth * depth, yPos,
744                                   pText->getWidth(),
745                                   lineHeight, true );
746             yPos += (pText->getHeight() - ySrc );
747             delete pText;
748         }
749         it = m_rTree.getNextVisibleItem( it );
750     }
751 }
752
753 VarTree::Iterator CtrlTree::findItemAtPos( int pos )
754 {
755     // The first item is m_firstPos.
756     // We decrement pos as we try the other items, until pos == 0.
757     VarTree::Iterator it;
758     for( it = m_firstPos; it != m_rTree.end() && pos != 0;
759          it = m_rTree.getNextVisibleItem( it ) )
760     {
761         pos--;
762     }
763
764     return it;
765 }