]> git.sesse.net Git - vlc/blob - modules/gui/skins2/controls/ctrl_tree.cpp
a256c1f1fb482b8333fb00ce527260b3b4bf4f7a
[vlc] / 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 )
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         autoScroll();
139         makeImage();
140     }
141     else if ( arg->i_type == 1 ) // Global change or deletion
142     {
143         m_firstPos = m_rTree.begin();
144         makeImage();
145     }
146     else if ( arg->i_type == 2 ) // Item-append
147     {
148         /// \todo Check if the really is really visible in the view (we only check if it in the document)
149         if( arg->b_visible == true )
150         {
151             makeImage();
152         }
153     }
154     notifyLayout();
155     m_pLastSelected = NULL;
156 }
157
158 void CtrlTree::onUpdate( Subject<VarPercent, void*> &rPercent, void* arg)
159 {
160     // Determine what is the first item to display
161     VarTree::Iterator it = m_rTree.begin();
162
163     int excessItems = m_rTree.visibleItems() - maxItems();
164
165     if( excessItems > 0)
166     {
167         VarPercent &rVarPos = m_rTree.getPositionVar();
168         // a simple (int)(...) causes rounding errors !
169 #ifdef _MSC_VER
170 #   define lrint (int)
171 #endif
172         it = m_rTree.getVisibleItem(lrint( (1.0 - rVarPos.get()) * (double)excessItems ) + 1);
173     }
174     if( m_firstPos != it )
175     {
176         // Redraw the control if the position has changed
177         m_firstPos = it;
178         makeImage();
179         notifyLayout();
180     }
181 }
182
183 void CtrlTree::onResize()
184 {
185     // Determine what is the first item to display
186     VarTree::Iterator it = m_rTree.begin();
187
188     int excessItems = m_rTree.visibleItems() - maxItems();
189
190     if( excessItems > 0)
191     {
192         VarPercent &rVarPos = m_rTree.getPositionVar();
193         // a simple (int)(...) causes rounding errors !
194 #ifdef _MSC_VER
195 #   define lrint (int)
196 #endif
197         it = m_rTree.getVisibleItem(lrint( (1.0 - rVarPos.get()) * (double)excessItems ) + 1);
198     }
199     // Redraw the control if the position has changed
200     m_firstPos = it;
201     makeImage();
202     notifyLayout();
203 }
204
205 void CtrlTree::onPositionChange()
206 {
207     makeImage();
208     notifyLayout();
209 }
210
211 void CtrlTree::handleEvent( EvtGeneric &rEvent )
212 {
213     VarTree::Iterator toShow; bool needShow = false;
214     if( rEvent.getAsString().find( "key:down" ) != string::npos )
215     {
216         int key = ((EvtKey&)rEvent).getKey();
217         VarTree::Iterator it;
218         bool previousWasSelected = false;
219         for( it = m_rTree.begin(); it != m_rTree.end();
220              it = m_rTree.getNextVisibleItem( it ) )
221         {
222             VarTree::Iterator next = m_rTree.getNextVisibleItem( it );
223             if( key == KEY_UP )
224             {
225                 // Scroll up one item
226                 if( ( it->parent()
227                       && it != it->parent()->begin() )
228                     || &*it != m_pLastSelected )
229                 {
230                     bool nextWasSelected = ( &*next == m_pLastSelected );
231                     it->m_selected = nextWasSelected;
232                     if( nextWasSelected )
233                     {
234                         m_pLastSelected = &*it;
235                         needShow = true; toShow = it;
236                     }
237                 }
238             }
239             else if( key == KEY_DOWN )
240             {
241                 // Scroll down one item
242                 if( ( it->parent()
243                       && next != it->parent()->end() )
244                     || &*it != m_pLastSelected )
245                 {
246                     (*it).m_selected = previousWasSelected;
247                 }
248                 if( previousWasSelected )
249                 {
250                     m_pLastSelected = &*it;
251                     needShow = true; toShow = it;
252                     previousWasSelected = false;
253                 }
254                 else
255                 {
256                     previousWasSelected = ( &*it == m_pLastSelected );
257                 }
258             }
259             else if( key == KEY_RIGHT )
260             {
261                 // Go down one level (and expand node)
262                 if( &*it == m_pLastSelected )
263                 {
264                     if( it->m_expanded )
265                     {
266                         if( it->size() )
267                         {
268                             it->m_selected = false;
269                             it->begin()->m_selected = true;
270                             m_pLastSelected = &*(it->begin());
271                         }
272                         else
273                         {
274                             m_rTree.action( &*it );
275                         }
276                     }
277                     else
278                     {
279                         it->m_expanded = true;
280                     }
281                 }
282             }
283             else if( key == KEY_LEFT )
284             {
285                 // Go up one level (and close node)
286                 if( &*it == m_pLastSelected )
287                 {
288                     if( it->m_expanded && it->size() )
289                     {
290                         it->m_expanded = false;
291                     }
292                     else
293                     {
294                         if( it->parent() && it->parent() != &m_rTree)
295                         {
296                             it->m_selected = false;
297                             m_pLastSelected = it->parent();
298                             m_pLastSelected->m_selected = true;
299                         }
300                     }
301                 }
302             }
303             else if( key == KEY_ENTER || key == KEY_SPACE )
304             {
305                 // Go up one level (and close node)
306                 if( &*it == m_pLastSelected )
307                 {
308                     m_rTree.action( &*it );
309                 }
310             }
311         }
312         if( needShow )
313             ensureVisible( toShow );
314
315         // Redraw the control
316         makeImage();
317         notifyLayout();
318     }
319
320     else if( rEvent.getAsString().find( "mouse:left" ) != string::npos )
321     {
322         EvtMouse &rEvtMouse = (EvtMouse&)rEvent;
323         const Position *pos = getPosition();
324         int yPos = ( rEvtMouse.getYPos() - pos->getTop() ) / itemHeight();
325         int xPos = rEvtMouse.getXPos() - pos->getLeft();
326         VarTree::Iterator it;
327
328         if( rEvent.getAsString().find( "mouse:left:down:ctrl,shift" ) !=
329             string::npos )
330         {
331             VarTree::Iterator itClicked = findItemAtPos( yPos );
332             // Flag to know if the current item must be selected
333             bool select = false;
334             for( it = m_rTree.begin(); it != m_rTree.end();
335                  it = m_rTree.getNextVisibleItem( it ) )
336             {
337                 bool nextSelect = select;
338                 if( it == itClicked || &*it == m_pLastSelected )
339                 {
340                     if( select )
341                     {
342                         nextSelect = false;
343                     }
344                     else
345                     {
346                         select = true;
347                         nextSelect = true;
348                     }
349                 }
350                 it->m_selected = (*it).m_selected || select;
351                 select = nextSelect;
352             }
353         }
354         else if( rEvent.getAsString().find( "mouse:left:down:ctrl" ) !=
355                  string::npos )
356         {
357             // Invert the selection of the item
358             it = findItemAtPos( yPos );
359             if( it != m_rTree.end() )
360             {
361                 it->m_selected = !it->m_selected;
362                 m_pLastSelected = &*it;
363             }
364         }
365         else if( rEvent.getAsString().find( "mouse:left:down:shift" ) !=
366                  string::npos )
367         {
368             VarTree::Iterator itClicked = findItemAtPos( yPos );
369             // Flag to know if the current item must be selected
370             bool select = false;
371             for( it = m_rTree.begin(); it != m_rTree.end();
372                  it = m_rTree.getNextVisibleItem( it ) )
373             {
374                 bool nextSelect = select;
375                 if( it == itClicked || &*it == m_pLastSelected )
376                 {
377                     if( select )
378                     {
379                         nextSelect = false;
380                     }
381                     else
382                     {
383                         select = true;
384                         nextSelect = true;
385                     }
386                 }
387                 it->m_selected = select;
388                 select = nextSelect;
389             }
390         }
391         else if( rEvent.getAsString().find( "mouse:left:down" ) !=
392                  string::npos )
393         {
394             it = findItemAtPos(yPos);
395             if( it->size() && xPos > (it->depth() - 1) * itemImageWidth()
396                 && xPos < it->depth() * itemImageWidth() )
397             {
398                 // Fold/unfold the item
399                 it->m_expanded = !it->m_expanded;
400             }
401             else
402             {
403                 // Unselect any previously selected item
404                 VarTree::Iterator it2;
405                 for( it2 = m_rTree.begin(); it2 != m_rTree.end();
406                      it2 = m_rTree.getNextVisibleItem( it2 ) )
407                 {
408                     it2->m_selected = false;
409                 }
410                 // Select the new item
411                 if( it != m_rTree.end() )
412                 {
413                     it->m_selected = true;
414                     m_pLastSelected = &*it;
415                 }
416             }
417         }
418
419         else if( rEvent.getAsString().find( "mouse:left:dblclick" ) !=
420                  string::npos )
421         {
422             it = findItemAtPos(yPos);
423             if( it != m_rTree.end() )
424             {
425                // Execute the action associated to this item
426                m_rTree.action( &*it );
427             }
428         }
429         // Redraw the control
430         makeImage();
431         notifyLayout();
432     }
433
434     else if( rEvent.getAsString().find( "scroll" ) != string::npos )
435     {
436         int direction = ((EvtScroll&)rEvent).getDirection();
437
438         double percentage = m_rTree.getPositionVar().get();
439         double step = 2.0 / (double)m_rTree.visibleItems();
440         if( direction == EvtScroll::kUp )
441         {
442             percentage += step;
443         }
444         else
445         {
446             percentage -= step;
447         }
448         m_rTree.getPositionVar().set( percentage );
449     }
450 }
451
452 bool CtrlTree::mouseOver( int x, int y ) const
453 {
454     const Position *pPos = getPosition();
455     return ( pPos
456        ? x >= 0 && x <= pPos->getWidth() && y >= 0 && y <= pPos->getHeight()
457        : false);
458 }
459
460 void CtrlTree::draw( OSGraphics &rImage, int xDest, int yDest )
461 {
462     if( m_pImage )
463     {
464         rImage.drawGraphics( *m_pImage, 0, 0, xDest, yDest );
465     }
466 }
467
468 bool CtrlTree::ensureVisible( VarTree::Iterator item )
469 {
470     // Find the item to focus
471     int focusItemIndex = 0;
472     VarTree::Iterator it;
473
474     m_rTree.ensureExpanded( item );
475
476     for( it = m_rTree.begin(); it != m_rTree.end();
477          it = m_rTree.getNextVisibleItem( it ) )
478     {
479         if( it->m_id == item->m_id ) break;
480         focusItemIndex++;
481     }
482    return ensureVisible( focusItemIndex );
483 }
484
485 bool CtrlTree::ensureVisible( int focusItemIndex )
486 {
487     // Find  m_firstPos
488     VarTree::Iterator it;
489     int firstPosIndex = 0;
490     for( it = m_rTree.begin(); it != m_rTree.end();
491          it = m_rTree.getNextVisibleItem( it ) )
492     {
493         if( it == m_firstPos ) break;
494         firstPosIndex++;
495     }
496
497     if( it == m_rTree.end() ) return false;
498
499
500     if( it != m_rTree.end()
501         && ( focusItemIndex < firstPosIndex
502            || focusItemIndex > firstPosIndex + maxItems() ) )
503     {
504         // Scroll to have the wanted stream visible
505         VarPercent &rVarPos = m_rTree.getPositionVar();
506         rVarPos.set( 1.0 - (double)focusItemIndex /
507                            (double)m_rTree.visibleItems() );
508         return true;
509     }
510     return false;
511 }
512
513 void CtrlTree::autoScroll()
514 {
515     // Find the current playing stream
516     int playIndex = 0;
517     VarTree::Iterator it;
518
519     for( it = m_rTree.begin(); it != m_rTree.end();
520          it = m_rTree.getNextItem( it ) )
521     {
522         if( it->m_playing )
523         {
524            m_rTree.ensureExpanded( it );
525            break;
526         }
527     }
528     for( it = m_rTree.begin(); it != m_rTree.end();
529          it = m_rTree.getNextVisibleItem( it ) )
530     {
531         if( it->m_playing )
532            break;
533         playIndex++;
534     }
535
536     if( it == m_rTree.end() ) return;
537
538
539     ensureVisible( playIndex );
540 }
541
542
543 void CtrlTree::makeImage()
544 {
545     if( m_pImage )
546     {
547         delete m_pImage;
548     }
549
550     // Get the size of the control
551     const Position *pPos = getPosition();
552     if( !pPos )
553     {
554         return;
555     }
556     int width = pPos->getWidth();
557     int height = pPos->getHeight();
558
559     int i_itemHeight = itemHeight();
560
561     // Create an image
562     OSFactory *pOsFactory = OSFactory::instance( getIntf() );
563     m_pImage = pOsFactory->createOSGraphics( width, height );
564
565     VarTree::Iterator it = m_firstPos;
566
567     if( m_pBgBitmap )
568     {
569         // Draw the background bitmap
570         ScaledBitmap bmp( getIntf(), *m_pBgBitmap, width, height );
571         m_pImage->drawBitmap( bmp, 0, 0 );
572
573         for( int yPos = 0; yPos < height; yPos += i_itemHeight )
574         {
575             if( it != m_rTree.end() )
576             {
577                 if( (*it).m_selected )
578                 {
579                     int rectHeight = __MIN( i_itemHeight, height - yPos );
580                     m_pImage->fillRect( 0, yPos, width, rectHeight,
581                                         m_selColor );
582                 }
583                 it = m_rTree.getNextVisibleItem( it );
584             }
585         }
586     }
587     else
588     {
589         // FIXME (TRYME)
590         // Fill background with background color
591         uint32_t bgColor = m_bgColor1;
592         m_pImage->fillRect( 0, 0, width, height, bgColor );
593         for( int yPos = 0; yPos < height; yPos += i_itemHeight )
594         {
595             int rectHeight = __MIN( i_itemHeight, height - yPos );
596             if( it != m_rTree.end() )
597             {
598                 uint32_t color = ( it->m_selected ? m_selColor : bgColor );
599                 m_pImage->fillRect( 0, yPos, width, rectHeight, color );
600                 it = m_rTree.getNextVisibleItem( it );
601             }
602             else
603             {
604                 m_pImage->fillRect( 0, yPos, width, rectHeight, bgColor );
605             }
606             bgColor = ( bgColor == m_bgColor1 ? m_bgColor2 : m_bgColor1 );
607         }
608     }
609
610     int bitmapWidth = itemImageWidth();
611
612     int yPos = 0;
613     it = m_firstPos;
614     while( it != m_rTree.end() && yPos < height )
615     {
616         const GenericBitmap *m_pCurBitmap;
617         UString *pStr = (UString*)(it->m_cString.get());
618         uint32_t color = ( it->m_playing ? m_playColor : m_fgColor );
619
620         // Draw the text
621         if( pStr != NULL )
622         {
623             int depth = it->depth();
624             GenericBitmap *pText = m_rFont.drawString( *pStr, color, width - bitmapWidth * depth );
625             if( !pText )
626             {
627                 return;
628             }
629             if( it->size() )
630                 m_pCurBitmap = it->m_expanded ? m_pOpenBitmap : m_pClosedBitmap;
631             else
632                 m_pCurBitmap = m_pItemBitmap;
633
634             if( m_pCurBitmap )
635             {
636                 // Make sure we are centered on the line
637                 int yPos2 = yPos+(i_itemHeight-m_pCurBitmap->getHeight()+1)/2;
638                 if( yPos2 >= height )
639                 {
640                     delete pText;
641                     break;
642                 }
643                 m_pImage->drawBitmap( *m_pCurBitmap, 0, 0,
644                                       bitmapWidth * (depth - 1 ), yPos2,
645                                       m_pCurBitmap->getWidth(),
646                                       __MIN( m_pCurBitmap->getHeight(),
647                                              height -  yPos2), true );
648             }
649             yPos += i_itemHeight - pText->getHeight();
650             int ySrc = 0;
651             if( yPos < 0 )
652             {
653                 ySrc = - yPos;
654                 yPos = 0;
655             }
656             int lineHeight = __MIN( pText->getHeight() - ySrc, height - yPos );
657             m_pImage->drawBitmap( *pText, 0, ySrc, bitmapWidth * depth, yPos,
658                                   pText->getWidth(),
659                                   lineHeight, true );
660             yPos += (pText->getHeight() - ySrc );
661             delete pText;
662         }
663         it = m_rTree.getNextVisibleItem( it );
664     }
665     /// \todo Reposition percentage var to accomodate if it's not suitable anymore (if we expanded a node)
666 }
667
668 VarTree::Iterator CtrlTree::findItemAtPos( int pos )
669 {
670     // The first item is m_firstPos.
671     // We decrement pos as we try the other items, until pos == 0.
672     VarTree::Iterator it;
673     for( it = m_firstPos; it != m_rTree.end() && pos != 0;
674          it = m_rTree.getNextVisibleItem( it ) )
675     {
676         pos--;
677     }
678
679     return it;
680 }