Merge lp://staging/~samuel-buffet/entertainer/reactive_tabs into lp://staging/entertainer

Proposed by Samuel Buffet
Status: Merged
Approved by: Matt Layman
Approved revision: 405
Merged at revision: not available
Proposed branch: lp://staging/~samuel-buffet/entertainer/reactive_tabs
Merge into: lp://staging/entertainer
Diff against target: None lines
To merge this branch: bzr merge lp://staging/~samuel-buffet/entertainer/reactive_tabs
Reviewer Review Type Date Requested Status
Matt Layman Approve
Review via email: mp+6315@code.staging.launchpad.net

Commit message

Tabs are now reactive to pointer events.

To post a comment you must log in.
Revision history for this message
Samuel Buffet (samuel-buffet) wrote :

Hi everybody,

This branch provides reactive Tabs on Screens like Music, AudioPlay etc

The interaction is at the moment limited to switching from one tab to another one by clicking on the Tab title. Further interactions will be possible when other reactive widgets will be integrated on Tabs.

I've also slightly changed effect when switching (now there's a fade out AND a fade in)

Samuel-

380. By Samuel Buffet

merged with trunk 376

Revision history for this message
Matt Layman (mblayman) wrote :

I really liked the results of this branch Samuel. The few added python properties went a long way to cleaning up some code that used to be fairly awkward and confusing. The fade in and fade out will have to grow on me a little (I'm just jaded from the old transition), but I'm fine with the change you made. Like I said on IRC, it only looks slightly weird (IMO) when there are multiple "no content" message between tabs. Here are my specific review comments:

General:
 * Need a full stop for docstrings (since they are normal English sentences).
 * _on_activate should be _on_activated because the event is 'activated.' This will affect the doc string too ('''Tab activated.''').

entertainerlib/frontend/gui/tabs/series_tab.py
 * _update_series_info doc string. There is no such thing as a TV-Serie. Series always has an 's' on the end in English. I know this one is just a carry over from the previous doc string, but now is a good time to correct the spelling mistake.

entertainerlib/frontend/gui/tabs/tab.py
 * Tab inherits from Base which inherits from object so there is no need for Tab to directly inherit from object.

entertainerlib/frontend/gui/widgets/tab_group.py
 * TabGroup inherits from Base which inherits from object so there is no need for TabGroup to directly inherit from object.
 * If you're inheriting from Base, there is no need for another call to "self.config = Configuration()" because that happens in the init of Base.
 * Could you add a comment about the timelines code in _activate_tab? I had to comment out the lines to see that they affected the label name of each tab. It wasn't very clear what that code did.

I noticed that there are no tests for this branch either. I'm going to approve this code because I'm not sure exactly what you could test. However, I will leave it up to your discretion to write tests that might be helpful for the long term maintenance of this code. Maybe you could test the boundary conditions of the TabGroup? Simulate clicking new tabs and see if the current tab got updated correctly? I think that there are some worthwhile things here to test, and I hope that you agree and can come up with some stuff to add to the test suite.

Thanks,
Matt

review: Approve
381. By Samuel Buffet

- docstrings update in album.tab.py
- self._on_activate is now self._on_activated

382. By Samuel Buffet

- docstrings update in artists_tab.py
- self._on_activate is now self._on_activated

383. By Samuel Buffet

- docstrings update in lyrics_tab.py
- self._on_activate is now self._on_activated

384. By Samuel Buffet

- docstrings update in movies_tab.py
- self._on_activate is now self._on_activated

385. By Samuel Buffet

- docstrings update in series_tab.py
- self._on_activate is now self._on_activated

386. By Samuel Buffet

- docstrings update in tracks_tab.py
- self._on_activate is now self._on_activated

387. By Samuel Buffet

- docstrings update in video_clips_tab.py
- self._on_activate is now self._on_activated

388. By Samuel Buffet

docstring update in series_tab.py because TV-serie doesn't exist in English.

389. By Samuel Buffet

- removed object in Tab and TabGroup Classes (they inherit from Base which already inherit from object)
- removed self.config = Configuration() from TabGroup __init__ because that's already done in Base's __init__

390. By Samuel Buffet

lint fix

391. By Samuel Buffet

comments added in tab_group.py to give more information about what we do with timelines in the _activate_tab method.

392. By Samuel Buffet

a test has been created for the TabGroup Class

393. By Samuel Buffet

- some fixes because the Tab Class uses now an active property
- Screen is fixed because the TabGroup Class uses an active property
- creation of the active property on TabGroup
- 2 new methods added to the music library mock
- TabGroup test improvments

394. By Samuel Buffet

removed the useless resize_widget method

395. By Samuel Buffet

2 new tests added to test_tabgroup.py
- test_add_tab and
- test_keyboard_navigation

396. By Samuel Buffet

removed some forgotten set_active

397. By Samuel Buffet

fixed a forgotten _on_activate in series_tab.py

398. By Samuel Buffet

docstrings clean up to comply with standards.

399. By Samuel Buffet

- created an activated signal for the TabGroup Class
- removed the deactivated signal for the Tab Class

400. By Samuel Buffet

- fix the tests
- tabgroup change its opacity accordingly to is state active/deactive
- clean up

Revision history for this message
Samuel Buffet (samuel-buffet) wrote :
Download full text (41.6 KiB)

Matt,

> I really liked the results of this branch Samuel. The few added python
> properties went a long way to cleaning up some code that used to be fairly
> awkward and confusing. The fade in and fade out will have to grow on me a
> little (I'm just jaded from the old transition), but I'm fine with the change
> you made. Like I said on IRC, it only looks slightly weird (IMO) when there
> are multiple "no content" message between tabs. Here are my specific review
> comments:
>
> General:
> * Need a full stop for docstrings (since they are normal English sentences).
> * _on_activate should be _on_activated because the event is 'activated.' This
> will affect the doc string too ('''Tab activated.''').

Yeah, I don't know why I've choosen on_activate for the "activated" signal. I surly was dreaming a bit.

>
> entertainerlib/frontend/gui/tabs/series_tab.py
> * _update_series_info doc string. There is no such thing as a TV-Serie.
> Series always has an 's' on the end in English. I know this one is just a
> carry over from the previous doc string, but now is a good time to correct the
> spelling mistake.

Fixed.

> entertainerlib/frontend/gui/tabs/tab.py
> * Tab inherits from Base which inherits from object so there is no need for
> Tab to directly inherit from object.

Yes totally right, thanks.

> entertainerlib/frontend/gui/widgets/tab_group.py
> * TabGroup inherits from Base which inherits from object so there is no need
> for TabGroup to directly inherit from object.
> * If you're inheriting from Base, there is no need for another call to
> "self.config = Configuration()" because that happens in the init of Base.

Yes totally again.

> * Could you add a comment about the timelines code in _activate_tab? I had to
> comment out the lines to see that they affected the label name of each tab. It
> wasn't very clear what that code did.

Comment added.

> I noticed that there are no tests for this branch either. I'm going to approve
> this code because I'm not sure exactly what you could test. However, I will
> leave it up to your discretion to write tests that might be helpful for the
> long term maintenance of this code. Maybe you could test the boundary
> conditions of the TabGroup? Simulate clicking new tabs and see if the current
> tab got updated correctly? I think that there are some worthwhile things here
> to test, and I hope that you agree and can come up with some stuff to add to
> the test suite.

Yes Matt, I fully agree with you once more. I've created test for this widget as you will see in the diff. But not really for the reactive part of the widget (for the standard one). I'm missing inspiration for the reactive part, I've not found a good way to do that so I'll come to that later.

See the incremental diff which is a bit big because I took the opportunity to change/fix a few things I should have done before.

Samuel-

=== modified file 'entertainerlib/frontend/gui/screens/artist.py'
--- entertainerlib/frontend/gui/screens/artist.py 2009-05-10 06:35:30 +0000
+++ entertainerlib/frontend/gui/screens/artist.py 2009-05-11 11:37:05 +0000
@@ -39,7 +39,7 @@
         Override function from Screen class. See Screen class for
 ...

Revision history for this message
Matt Layman (mblayman) wrote :

Samuel,

Here are my last few follow up comments after reviewing your branch again following all of my initial input.

albums_tab.py
 * redundant gobject.timeout_add was removed from artists_tab so it should probably be removed from albums_tab too.

movies_tab.py
 * In _update_movie_info, the text for movie_info should be translatable, but it currently isn't. Can you put the value of the "set_text" into translatable format?

tab.py
 * self._active = None seem problematic for init. What happens if something tries to call the active property before you set the value? Shouldn't we default to False initially?

tab_group.py
 * Same comment as tab.py

test_tabgroup.py
 * tearDown isn't necessary since you aren't doing anything that the superclass isn't already doing.

401. By Samuel Buffet

removed the tearDown method of test_tabgroup.py

402. By Samuel Buffet

small modification in albums_tab.py to avoid a useless _init_selected_album.

403. By Samuel Buffet

movies_tab.py, improvments in _update_movie_info.

404. By Samuel Buffet

The MockMusicLibrary is modified in order to provide a MockAlbum in return of get_albums_by_artist.
This was mandatory to fix the test__generate_artist

405. By Samuel Buffet

- artist screen pass test as I've added a protection against empty results
- Tab & TabGroup Classes will return False on the first attempt to read the active property
- RAZ of what I've done in mock.py

Revision history for this message
Samuel Buffet (samuel-buffet) wrote :
Download full text (6.8 KiB)

Hi Matt,

All your points are very good questions. See bellow my fixes and explanations.

> albums_tab.py
> * redundant gobject.timeout_add was removed from artists_tab so it should
> probably be removed from albums_tab too.

Fixed.

> movies_tab.py
> * In _update_movie_info, the text for movie_info should be translatable, but
> it currently isn't. Can you put the value of the "set_text" into translatable
> format?

Fixed using the gettext macro _() and using a formatted string.

> tab.py
> * self._active = None seem problematic for init. What happens if something
> tries to call the active property before you set the value? Shouldn't we
> default to False initially?
>
> tab_group.py
> * Same comment as tab.py

Here, there's an explanation to have None in the __init__ and not False (but the point really makes sense). Indeed, if self._active is set to False then if we try to do Tab.active = False later on then we wont run the entire setter's code because of the IF statement at the beginning.

The IF statement is needed to run the setter's code only when there's a change in the property's value. This is really frequent on every language using object's properties.

So look the new incremental diff, I've fixed the point keeping that None initialization of self._active

> test_tabgroup.py
> * tearDown isn't necessary since you aren't doing anything that the
> superclass isn't already doing.

Removed.

the new new incremental diff (very short this time).
Thanks for the review.
Samuel-

inc_diff :

=== modified file 'entertainerlib/frontend/gui/screens/artist.py'
--- entertainerlib/frontend/gui/screens/artist.py 2009-05-11 11:43:56 +0000
+++ entertainerlib/frontend/gui/screens/artist.py 2009-05-12 17:03:51 +0000
@@ -26,13 +26,14 @@

         # Tabs
         albums = music_library.get_albums_by_artist(artist)
- tab1 = AlbumsTab(albums, move_to_new_screen_callback)
+ if albums:
+ tab1 = AlbumsTab(albums, move_to_new_screen_callback)
+ self.add_tab(tab1)

         tracks = music_library.get_tracks_by_artist(artist)
- tab2 = TracksTab(tracks, move_to_new_screen_callback)
-
- self.add_tab(tab1)
- self.add_tab(tab2)
+ if tracks:
+ tab2 = TracksTab(tracks, move_to_new_screen_callback)
+ self.add_tab(tab2)

     def is_interested_in_play_action(self):
         """

=== modified file 'entertainerlib/frontend/gui/tabs/albums_tab.py'
--- entertainerlib/frontend/gui/tabs/albums_tab.py 2009-05-11 20:28:41 +0000
+++ entertainerlib/frontend/gui/tabs/albums_tab.py 2009-05-12 10:45:08 +0000
@@ -62,7 +62,8 @@
         # Default texture
         self.default_cover = Texture(self.theme.getImage("default_album_art"))

- gobject.timeout_add(50, self._async_menu_build, albums, x_percent)
+ self._async_menu_build(albums, x_percent)
+ self._selected_album = self.menu.get_current_menuitem().get_userdata()

         self.menu.set_position(self.get_abs_x(0.0586), self.get_abs_y(0.1823))
         self.menu.show()
@@ -95,9 +96,6 @@
         self.album_tracks = Label(0.037, "subtitle", 0.22, 0.91, "")
         self.add(self.album_tracks)

- self._sel...

Read more...

Revision history for this message
Samuel Buffet (samuel-buffet) wrote :

Forgot to mention that the modification on the artist screen was done to fix Test.
For some reason, the generation test of this screen was failing (due to changes on the AlbumTab). The failure itself was due to the fact that this screen does not tolerate an empty music_library on creation (and we mock and empty library). This is not a big issue as to access that screen we have to go first through the music screen which HAS this protection (creating an empty notice in that case).

But anyway, I've added to that screen an internal protection.

Samuel-

Revision history for this message
Samuel Buffet (samuel-buffet) wrote :

Matt I don't understand what you mean with?

"* tearDown isn't necessary since you aren't doing anything that the superclass isn't already doing."

I don't get when we have to do that tearDown and when we don't have to do it.

Samuel-

Revision history for this message
Matt Layman (mblayman) wrote :

Sure Samuel, I can explain. A unittest test will always run the following
for each test (specifically, I think the TestLoader is delegating the work
and running each TestCase):

1) setUp
2) The actual test method
3) tearDown

Our test class, EntertainerTest, is a subclass of testtools.TestCase, which
is a subclass of unittest.TestCase. Just like any other subclasses in
Python, method resolution order will try to execute the lowest level class
first. So if you run a test on Foo() in FooTest below, the execution of
setUp will call FooTest.setUp() -> EntertainerTest.setUp() ->
testtools.TestCase.setUp() -> unitest.TestCase.setUp().

We need the setUp method for FooTest because we need to have self.foo
accessible. However, foo doesn't need to do anything special to clean up so
FooTest doesn't have any extra code to write. Therefore, when the loader
tries to execute tearDown, it will "see" that no tearDown exists for FooTest
so it will attempt to execute the method from the superclass. Since
EntertainerTest has a tearDown method, the loader happily executes it.

In summary, tearDown is not need when all you would have is:

def tearDown(self):
    EntertainerTest.tearDown(self)

but is necessary when your test has:

def tearDown(self):
    EntertainerTest.tearDown(self)

    <do clean up stuff for this TestCase here>

I hope this makes sense. Please let me know if you have more questions.

-Matt

from foo import Foo
from entertainerlib.tests import EntertainerTest

class FooTest(EntertainerTest):

    def setUp(self):
        EntertainerTest.setUp(self)

        self.foo = Foo()

    def test_create(self)
        self.assertTrue(isinstance(self.foo, Foo))

On Tue, May 12, 2009 at 3:03 PM, Samuel Buffet <email address hidden>wrote:

> Matt I don't understand what you mean with?
>
> "* tearDown isn't necessary since you aren't doing anything that the
> superclass isn't already doing."
>
> I don't get when we have to do that tearDown and when we don't have to do
> it.
>
> Samuel-
> --
>
> https://code.launchpad.net/~samuel-buffet/entertainer/reactive_tabs/+merge/6315<https://code.launchpad.net/%7Esamuel-buffet/entertainer/reactive_tabs/+merge/6315>
> You are reviewing the proposed merge of
> lp:~samuel-buffet/entertainer/reactive_tabs into lp:entertainer.
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'entertainerlib/frontend/gui/screens/artist.py'
2--- entertainerlib/frontend/gui/screens/artist.py 2009-04-27 03:32:30 +0000
3+++ entertainerlib/frontend/gui/screens/artist.py 2009-05-05 11:18:31 +0000
4@@ -33,7 +33,6 @@
5
6 tracks = music_library.get_tracks_by_artist(artist)
7 tab2 = TracksTab(tracks, move_to_new_screen_callback)
8- tab2.hide_tab()
9
10 self.add_tab(tab1)
11 self.add_tab(tab2)
12
13=== modified file 'entertainerlib/frontend/gui/screens/music.py'
14--- entertainerlib/frontend/gui/screens/music.py 2009-04-27 03:32:30 +0000
15+++ entertainerlib/frontend/gui/screens/music.py 2009-05-05 11:18:31 +0000
16@@ -34,7 +34,6 @@
17 move_to_new_screen_callback)
18 tab2 = AlbumsTab(music_library.get_all_albums(),
19 move_to_new_screen_callback)
20- tab2.hide_tab()
21
22 self.add_tab(tab1)
23 self.add_tab(tab2)
24
25=== modified file 'entertainerlib/frontend/gui/tabs/albums_tab.py'
26--- entertainerlib/frontend/gui/tabs/albums_tab.py 2009-02-03 23:37:57 +0000
27+++ entertainerlib/frontend/gui/tabs/albums_tab.py 2009-04-22 10:28:29 +0000
28@@ -102,6 +102,9 @@
29 self._selected_album = None
30 gobject.timeout_add(50, self._init_selected_album)
31
32+ self.connect('activated', self._on_activate)
33+ self.connect('deactivated', self._on_deactivated)
34+
35 def _async_menu_build(self, albums, item_size_percent):
36 """
37 This function is called as an async callback. It is also a recursive
38@@ -152,30 +155,23 @@
39 least one album'''
40 return True
41
42- def set_active(self, boolean):
43- """
44- Override method from parent class.
45- @param boolean: True to set this tab active, False to set inactive
46- """
47- Tab.set_active(self, boolean)
48- if boolean:
49- self._update_album_info(
50- self.menu.get_current_menuitem().get_userdata())
51- self.menu.set_active(True)
52- else:
53- self.menu.set_active(False)
54-
55- def _update_album_info(self, album):
56+ def _update_album_info(self):
57 '''Update the album information labels'''
58- self.album_title.set_text(album.get_title())
59- self.album_artist.set_text(album.get_artist())
60- self.album_tracks.set_text(_("%(total)s tracks") % \
61- {'total': album.get_number_of_tracks()})
62+ if self.active:
63+ album = self.menu.get_current_menuitem().get_userdata()
64+ self.album_title.set_text(album.get_title())
65+ self.album_artist.set_text(album.get_artist())
66+ self.album_tracks.set_text(_("%(total)s tracks") % \
67+ {'total': album.get_number_of_tracks()})
68+ else:
69+ self.album_title.set_text("")
70+ self.album_artist.set_text("")
71+ self.album_tracks.set_text("")
72
73 def _update_menu(self, direction):
74 '''Update the menu based on the provided menu direction'''
75 self.menu.move(direction)
76- self._update_album_info(self.menu.get_current_menuitem().get_userdata())
77+ self._update_album_info()
78 self.li.set_current(self.menu.get_current_position() + 1)
79 return False
80
81@@ -205,3 +201,17 @@
82 self.callback("album", kwargs)
83 return False
84
85+ def _on_activate(self, event=None):
86+ '''Tab activation'''
87+ self._update_album_info()
88+ self.menu.set_active(True)
89+ self.menu.set_opacity(255)
90+ return False
91+
92+ def _on_deactivated(self, event=None):
93+ '''Tab deactivated'''
94+ self._update_album_info()
95+ self.menu.set_active(False)
96+ self.menu.set_opacity(128)
97+ return False
98+
99
100=== modified file 'entertainerlib/frontend/gui/tabs/artists_tab.py'
101--- entertainerlib/frontend/gui/tabs/artists_tab.py 2009-02-03 23:37:57 +0000
102+++ entertainerlib/frontend/gui/tabs/artists_tab.py 2009-04-22 10:28:29 +0000
103@@ -73,6 +73,9 @@
104 self._selected_artist = None
105 gobject.timeout_add(25, self._init_selected_artist)
106
107+ self.connect('activated', self._on_activate)
108+ self.connect('deactivated', self._on_deactivated)
109+
110 def _async_menu_build(self, artists):
111 """
112 This function is called as an async callback. It is also a recursive
113@@ -118,32 +121,24 @@
114 least one album'''
115 return True
116
117- def set_active(self, boolean):
118- """
119- Override method from parent class.
120- @param boolean: True to set this tab active, False to set inactive
121- """
122- Tab.set_active(self, boolean)
123- if boolean:
124- self._update_artist_info(
125- self.menu.get_current_menuitem().get_userdata())
126- self.menu.set_active(True)
127- else:
128- self.menu.set_active(False)
129-
130- def _update_artist_info(self, artist):
131+ def _update_artist_info(self):
132 '''Update the artist information labels'''
133- self.artist_title.set_text(artist)
134- self.artist_albums.set_text(_("%(albums)d albums") %
135- {'albums': self.library.number_of_albums_by_artist(artist)})
136- self.artist_tracks.set_text(_("%(tracks)d tracks") %
137- {'tracks': self.library.number_of_tracks_by_artist(artist)})
138+ if self.active:
139+ artist = self.menu.get_current_menuitem().get_userdata()
140+ self.artist_title.set_text(artist)
141+ self.artist_albums.set_text(_("%(albums)d albums") %
142+ {'albums': self.library.number_of_albums_by_artist(artist)})
143+ self.artist_tracks.set_text(_("%(tracks)d tracks") %
144+ {'tracks': self.library.number_of_tracks_by_artist(artist)})
145+ else:
146+ self.artist_title.set_text("")
147+ self.artist_albums.set_text("")
148+ self.artist_tracks.set_text("")
149
150 def _update_menu(self, direction):
151 '''Update the menu based on the provided menu direction'''
152 self.menu.move(direction)
153- self._update_artist_info(
154- self.menu.get_current_menuitem().get_userdata())
155+ self._update_artist_info()
156 self.li.set_current(self.menu.get_current_position() + 1)
157 return False
158
159@@ -173,3 +168,17 @@
160 self.callback("artist", kwargs)
161 return False
162
163+ def _on_activate(self, event=None):
164+ '''Tab activation'''
165+ self._update_artist_info()
166+ self.menu.set_active(True)
167+ self.menu.set_opacity(255)
168+ return False
169+
170+ def _on_deactivated(self, event=None):
171+ '''Tab deactivated'''
172+ self._update_artist_info()
173+ self.menu.set_active(False)
174+ self.menu.set_opacity(128)
175+ return False
176+
177
178=== modified file 'entertainerlib/frontend/gui/tabs/lyrics_tab.py'
179--- entertainerlib/frontend/gui/tabs/lyrics_tab.py 2009-04-21 21:04:56 +0000
180+++ entertainerlib/frontend/gui/tabs/lyrics_tab.py 2009-05-07 16:39:54 +0000
181@@ -32,6 +32,9 @@
182
183 self.lyrics_area = ScrollArea(0.4, 0.57, 0.5, 0.23, lyrics)
184 self.add(self.lyrics_area)
185+
186+ self.connect('activated', self._on_activate)
187+ self.connect('deactivated', self._on_deactivated)
188 else:
189 # Display throbber animation while searching for lyrics
190 white = clutter.Color(255, 255, 255, 255)
191@@ -42,8 +45,6 @@
192 self.add(self.throbber)
193 self.track.fetch_lyrics(self._lyrics_search_callback)
194
195- self.hide_tab()
196-
197 def _lyrics_search_callback(self, lyrics_text):
198 """
199 This function is called when lyrics search is over.
200@@ -75,17 +76,6 @@
201 else:
202 return True
203
204- def set_active(self, boolean):
205- """
206- Override method from parent class.
207- @param boolean: True to set this tab active, False to set inactive
208- """
209- Tab.set_active(self, boolean)
210- if boolean:
211- self.lyrics_area.set_active(True)
212- else:
213- self.lyrics_area.set_active(False)
214-
215 def _handle_up(self):
216 '''Handle the up user event'''
217 if self.lyrics_area.get_offset() == 0:
218@@ -97,3 +87,15 @@
219 '''Handle the down user event'''
220 return self.lyrics_area.scroll_down()
221
222+ def _on_activate(self, event=None):
223+ '''Tab activation'''
224+ self.lyrics_area.set_active(True)
225+ self.lyrics_area.set_opacity(255)
226+ return False
227+
228+ def _on_deactivated(self, event=None):
229+ '''Tab deactivated'''
230+ self.lyrics_area.set_active(False)
231+ self.lyrics_area.set_opacity(128)
232+ return False
233+
234
235=== modified file 'entertainerlib/frontend/gui/tabs/movies_tab.py'
236--- entertainerlib/frontend/gui/tabs/movies_tab.py 2009-02-03 23:37:57 +0000
237+++ entertainerlib/frontend/gui/tabs/movies_tab.py 2009-04-22 10:28:29 +0000
238@@ -40,6 +40,9 @@
239 else:
240 self._create_menu()
241
242+ self.connect('activated', self._on_activate)
243+ self.connect('deactivated', self._on_deactivated)
244+
245 def can_activate(self):
246 """
247 Allow if we have some movies indexed.
248@@ -49,19 +52,6 @@
249 else:
250 return True
251
252- def set_active(self, boolean):
253- """
254- Override method from parent class.
255- @param boolean: True to set this tab active, False to set inactive
256- """
257- Tab.set_active(self, boolean)
258- if boolean:
259- self._update_movie_info(
260- self.menu.get_current_menuitem().get_userdata())
261- self.menu.set_active(True)
262- else:
263- self.menu.set_active(False)
264-
265 def _create_empty_library_notice(self):
266 """
267 Create an information box that is displayed if there are no indexed
268@@ -138,26 +128,29 @@
269 self.movie_plot.width = 0.7
270 self.add(self.movie_plot)
271
272- def _update_movie_info(self, movie):
273- """
274- Update movie information. Updates movie specific labels.
275- @param movie: Currently selected movie (Movie object)
276- """
277- genres = movie.get_genres()
278- if len(genres) > 1:
279- genre = genres[0] + "/" + genres[1]
280+ def _update_movie_info(self):
281+ '''Update the movie information labels'''
282+ if self.active:
283+ movie = self.menu.get_current_menuitem().get_userdata()
284+ genres = movie.get_genres()
285+ if len(genres) > 1:
286+ genre = genres[0] + "/" + genres[1]
287+ else:
288+ genre = genres[0]
289+
290+ self.movie_title.set_text(movie.get_title() + \
291+ " (" + str(movie.get_year()) + ")")
292+ self.movie_info.set_text(str(movie.get_runtime()) + "min, " + genre)
293+ self.movie_plot.set_text(movie.get_short_plot())
294 else:
295- genre = genres[0]
296-
297- self.movie_title.set_text(movie.get_title() + \
298- " (" + str(movie.get_year()) + ")")
299- self.movie_info.set_text(str(movie.get_runtime()) + "min, " + genre)
300- self.movie_plot.set_text(movie.get_short_plot())
301+ self.movie_title.set_text("")
302+ self.movie_info.set_text("")
303+ self.movie_plot.set_text("")
304
305 def _update_menu(self, direction):
306 '''Update the menu based on the provided menu direction'''
307 self.menu.move(direction)
308- self._update_movie_info(self.menu.get_current_menuitem().get_userdata())
309+ self._update_movie_info()
310 self.list_indicator.set_current(self.menu.get_current_position() + 1)
311 return False
312
313@@ -187,3 +180,17 @@
314 self.callback("movie", kwargs)
315 return False
316
317+ def _on_activate(self, event=None):
318+ '''Tab activation'''
319+ self._update_movie_info()
320+ self.menu.set_active(True)
321+ self.menu.set_opacity(255)
322+ return False
323+
324+ def _on_deactivated(self, event=None):
325+ '''Tab deactivated'''
326+ self._update_movie_info()
327+ self.menu.set_active(False)
328+ self.menu.set_opacity(128)
329+ return False
330+
331
332=== modified file 'entertainerlib/frontend/gui/tabs/series_tab.py'
333--- entertainerlib/frontend/gui/tabs/series_tab.py 2009-02-03 23:37:57 +0000
334+++ entertainerlib/frontend/gui/tabs/series_tab.py 2009-04-22 10:28:29 +0000
335@@ -39,7 +39,8 @@
336 else:
337 self._create_menu()
338
339- self.hide_tab()
340+ self.connect('activated', self._on_activate)
341+ self.connect('deactivated', self._on_deactivated)
342
343 def can_activate(self):
344 """
345@@ -50,19 +51,6 @@
346 else:
347 return True
348
349- def set_active(self, boolean):
350- """
351- Override method from parent class.
352- @param boolean: True to set this tab active, False to set inactive
353- """
354- Tab.set_active(self, boolean)
355- if boolean:
356- self._update_series_info(
357- self.menu.get_current_menuitem().get_userdata())
358- self.menu.set_active(True)
359- else:
360- self.menu.set_active(False)
361-
362 def _create_empty_library_notice(self):
363 """
364 Create an information box that is displayed if there are no indexed
365@@ -134,23 +122,24 @@
366 self.series_info = Label(0.034, "subtitle", 0.2, 0.82, "")
367 self.add(self.series_info)
368
369- def _update_series_info(self, series):
370- """
371- Update TV-Serie information. Updates clutter labels on TV-series tab
372- @param series: Currently selected TV-Serie (TVSeries object)
373- """
374- info = _("%(season)d Seasons\n%(episode)d Episodes") % {'season':
375- series.get_number_of_seasons(), 'episode':
376- series.get_number_of_episodes()}
377+ def _update_series_info(self):
378+ '''Update the TV-Serie information labels'''
379+ if self.active:
380+ series = self.menu.get_current_menuitem().get_userdata()
381+ info = _("%(season)d Seasons\n%(episode)d Episodes") % {'season':
382+ series.get_number_of_seasons(), 'episode':
383+ series.get_number_of_episodes()}
384
385- self.series_info.set_text(info)
386- self.series_title.set_text(series.get_title())
387+ self.series_info.set_text(info)
388+ self.series_title.set_text(series.get_title())
389+ else:
390+ self.series_info.set_text("")
391+ self.series_title.set_text("")
392
393 def _update_menu(self, direction):
394 '''Update the menu based on the provided menu direction'''
395 self.menu.move(direction)
396- self._update_series_info(
397- self.menu.get_current_menuitem().get_userdata())
398+ self._update_series_info()
399 self.list_indicator.set_current(self.menu.get_current_position() + 1)
400 return False
401
402@@ -180,3 +169,17 @@
403 self.callback("tv_series", kwargs)
404 return False
405
406+ def _on_activate(self, event=None):
407+ '''Tab activation'''
408+ self._update_series_info()
409+ self.menu.set_active(True)
410+ self.menu.set_opacity(255)
411+ return False
412+
413+ def _on_deactivated(self, event=None):
414+ '''Tab deactivated'''
415+ self._update_series_info()
416+ self.menu.set_active(False)
417+ self.menu.set_opacity(128)
418+ return False
419+
420
421=== modified file 'entertainerlib/frontend/gui/tabs/tab.py'
422--- entertainerlib/frontend/gui/tabs/tab.py 2009-02-28 17:22:46 +0000
423+++ entertainerlib/frontend/gui/tabs/tab.py 2009-05-07 19:25:47 +0000
424@@ -5,28 +5,33 @@
425 __author__ = "Lauri Taimila <lauri@taimila.com>"
426
427 import clutter
428+import gobject
429
430 from entertainerlib.frontend.gui.user_event import UserEvent
431 from entertainerlib.frontend.gui.widgets.base import Base
432 from entertainerlib.frontend.gui.widgets.label import Label
433 from entertainerlib.frontend.gui.widgets.texture import Texture
434
435-class Tab(Base, clutter.Group):
436+class Tab(Base, clutter.Group, object):
437 """
438 Tab can be used as part of the TabGroup
439
440 Tab is a very simple container that contains all the widgets and logic
441 of the tab page.
442 """
443+ __gtype_name__ = 'Tab'
444+ __gsignals__ = {
445+ 'activated' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
446+ 'deactivated' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
447+ }
448
449 def __init__(self, name, title="Untitled tab", callback=None):
450-
451 Base.__init__(self)
452 clutter.Group.__init__(self)
453- self._name = name
454- self._title = title
455+
456+ self.name = name
457+ self.title = title
458 self.callback = callback
459- self.active = False
460 self.theme = self.config.theme
461
462 self.dispatch = {
463@@ -37,20 +42,49 @@
464 UserEvent.NAVIGATE_SELECT : self._handle_select,
465 }
466
467- self.timeline = clutter.Timeline(15, 60)
468+ # show/hide animation on the Tab
469+ self.timeline = clutter.Timeline(30, 60)
470 self.alpha = clutter.Alpha(self.timeline, clutter.smoothstep_inc_func)
471 self.behaviour = clutter.BehaviourOpacity(0, 255, self.alpha)
472 self.behaviour.apply(self)
473
474- @property
475- def name(self):
476- '''Internal string name that can be used as a key for the tab group'''
477- return self._name
478-
479- @property
480- def title(self):
481- '''User string title displayed in the interface'''
482- return self._title
483+ # Tabs are created deactivated and invisible
484+ self._active = False
485+ self._visible = False
486+ self.set_opacity(0)
487+
488+ def _get_active(self):
489+ """active property getter."""
490+ return self._active
491+
492+ def _set_active(self, boolean):
493+ """active property setter."""
494+ self._active = boolean
495+
496+ if boolean:
497+ self.emit('activated')
498+ else:
499+ self.emit('deactivated')
500+
501+ active = property(_get_active, _set_active)
502+
503+ def _get_visible(self):
504+ """visible property getter."""
505+ return self._visible
506+
507+ def _set_visible(self, boolean):
508+ """visible property setter."""
509+ self._visible = boolean
510+
511+ if boolean:
512+ self.timeline.set_direction(clutter.TIMELINE_FORWARD)
513+ self.timeline.rewind()
514+ self.timeline.start()
515+ else:
516+ self.timeline.set_direction(clutter.TIMELINE_BACKWARD)
517+ self.timeline.start()
518+
519+ visible = property(_get_visible, _set_visible)
520
521 def can_activate(self):
522 """
523@@ -61,20 +95,6 @@
524 """
525 return False
526
527- def is_active(self):
528- """
529- Is this tab active
530- @return: boolean
531- """
532- return self.active
533-
534- def set_active(self, boolean):
535- """
536- Set this tab to active or inactive state
537- @param boolean: True to set active, False to set inactive
538- """
539- self.active = boolean
540-
541 def show_empty_tab_notice(self, title=_("Empty tab"),
542 message_body=_("This tab doesn't contain any elements.")):
543 """
544@@ -95,23 +115,6 @@
545 info.width = 0.57
546 self.add(info)
547
548- def show_tab(self):
549- """
550- Activate this tab. This is called when tab is changed and this tab
551- is becoming active. This method should activate animations.
552- """
553- self.set_opacity(0)
554- self.show()
555- self.timeline.set_direction(clutter.TIMELINE_FORWARD)
556- self.timeline.start()
557-
558- def hide_tab(self):
559- """
560- Deactivate this tab. This is called when tab is changed and this tab
561- is becoming non-active. This method should disable animations.
562- """
563- self.hide()
564-
565 def _handle_up(self):
566 '''Dummy method for unimplemented handlers in subclass'''
567 return False
568
569=== modified file 'entertainerlib/frontend/gui/tabs/tracks_tab.py'
570--- entertainerlib/frontend/gui/tabs/tracks_tab.py 2009-02-03 23:37:57 +0000
571+++ entertainerlib/frontend/gui/tabs/tracks_tab.py 2009-04-22 10:28:29 +0000
572@@ -67,37 +67,33 @@
573 self.li.show()
574 self.add(self.li)
575
576+ self.connect('activated', self._on_activate)
577+ self.connect('deactivated', self._on_deactivated)
578+
579 def can_activate(self):
580 '''Tracks tab will always be created from an existing artist with at
581 least one track'''
582 return True
583
584- def set_active(self, boolean):
585- """
586- Override method from parent class.
587- @param boolean: True to set this tab active, False to set inactive
588- """
589- Tab.set_active(self, boolean)
590- if boolean:
591- self._update_track_info(
592- self.menu.get_current_menuitem().get_userdata())
593- self.menu.set_active(True)
594- else:
595- self.menu.set_active(False)
596-
597- def _update_track_info(self, track):
598+ def _update_track_info(self):
599 '''Update the track information labels'''
600- self.track_title.set_text(track.get_title())
601- self.track_length.set_text(str(track.get_length() / 60) + ":" + \
602- str(track.get_length() % 60).zfill(2))
603- self.track_number.set_text(_("Track %(track)s from %(album)s") % \
604- {'track': track.get_tracknumber(),
605- 'album': track.get_album().get_title()})
606+ if self.active:
607+ track = self.menu.get_current_menuitem().get_userdata()
608+ self.track_title.set_text(track.get_title())
609+ self.track_length.set_text(str(track.get_length() / 60) + ":" + \
610+ str(track.get_length() % 60).zfill(2))
611+ self.track_number.set_text(_("Track %(track)s from %(album)s") % \
612+ {'track': track.get_tracknumber(),
613+ 'album': track.get_album().get_title()})
614+ else:
615+ self.track_title.set_text("")
616+ self.track_length.set_text("")
617+ self.track_number.set_text("")
618
619 def _update_menu(self, direction):
620 '''Update the menu based on the provided menu direction'''
621 self.menu.move(direction)
622- self._update_track_info(self.menu.get_current_menuitem().get_userdata())
623+ self._update_track_info()
624 self.li.set_current(self.menu.get_current_position() + 1)
625 return False
626
627@@ -127,3 +123,17 @@
628 self.callback("audio_play", kwargs)
629 return False
630
631+ def _on_activate(self, event=None):
632+ '''Tab activation'''
633+ self._update_track_info()
634+ self.menu.set_active(True)
635+ self.menu.set_opacity(255)
636+ return False
637+
638+ def _on_deactivated(self, event=None):
639+ '''Tab deactivated'''
640+ self._update_track_info()
641+ self.menu.set_active(False)
642+ self.menu.set_opacity(128)
643+ return False
644+
645
646=== modified file 'entertainerlib/frontend/gui/tabs/video_clips_tab.py'
647--- entertainerlib/frontend/gui/tabs/video_clips_tab.py 2009-02-03 23:37:57 +0000
648+++ entertainerlib/frontend/gui/tabs/video_clips_tab.py 2009-04-22 10:28:29 +0000
649@@ -40,7 +40,8 @@
650 else:
651 self._create_menu()
652
653- self.hide_tab()
654+ self.connect('activated', self._on_activate)
655+ self.connect('deactivated', self._on_deactivated)
656
657 def can_activate(self):
658 """
659@@ -51,19 +52,6 @@
660 else:
661 return True
662
663- def set_active(self, boolean):
664- """
665- Override method from parent class.
666- @param boolean: True to set this tab active, False to set inactive
667- """
668- Tab.set_active(self, boolean)
669- if boolean:
670- self._update_clip_info(
671- self.menu.get_current_menuitem().get_userdata())
672- self.menu.set_active(True)
673- else:
674- self.menu.set_active(False)
675-
676 def _create_empty_library_notice(self):
677 """
678 Create an information box that is displayed if there are no indexed
679@@ -126,19 +114,21 @@
680 self.clip_info.width = 0.5
681 self.add(self.clip_info)
682
683- def _update_clip_info(self, clip):
684- """
685- Update VideoClip information. Updates clutter labels on Clips tab
686- @param clip: Currently selected VideoClip (VideoClip object)
687- """
688- (folder, filename) = os.path.split(clip.get_filename())
689- self.clip_title.set_text(filename)
690- self.clip_info.set_text(folder)
691+ def _update_clip_info(self):
692+ '''Update the VideoClip information labels'''
693+ if self.active:
694+ clip = self.menu.get_current_menuitem().get_userdata()
695+ (folder, filename) = os.path.split(clip.get_filename())
696+ self.clip_title.set_text(filename)
697+ self.clip_info.set_text(folder)
698+ else:
699+ self.clip_title.set_text("")
700+ self.clip_info.set_text("")
701
702 def _update_menu(self, direction):
703 '''Update the menu based on the provided menu direction'''
704 self.menu.move(direction)
705- self._update_clip_info(self.menu.get_current_menuitem().get_userdata())
706+ self._update_clip_info()
707 self.list_indicator.set_current(self.menu.get_current_position() + 1)
708 return False
709
710@@ -169,3 +159,17 @@
711 self.callback("video_osd")
712 return False
713
714+ def _on_activate(self, event=None):
715+ '''Tab activation'''
716+ self._update_clip_info()
717+ self.menu.set_active(True)
718+ self.menu.set_opacity(255)
719+ return False
720+
721+ def _on_deactivated(self, event=None):
722+ '''Tab deactivated'''
723+ self._update_clip_info()
724+ self.menu.set_active(False)
725+ self.menu.set_opacity(128)
726+ return False
727+
728
729=== modified file 'entertainerlib/frontend/gui/widgets/tab_group.py'
730--- entertainerlib/frontend/gui/widgets/tab_group.py 2009-02-27 00:01:53 +0000
731+++ entertainerlib/frontend/gui/widgets/tab_group.py 2009-04-16 18:52:06 +0000
732@@ -7,11 +7,12 @@
733 import clutter
734 import pango
735
736+from entertainerlib.frontend.gui.widgets.base import Base
737 from entertainerlib.frontend.gui.widgets.label import Label
738 from entertainerlib.frontend.gui.user_event import UserEvent
739 from entertainerlib.utils.configuration import Configuration
740
741-class TabGroup(clutter.Group):
742+class TabGroup(Base, clutter.Group, object):
743 """
744 This is a top level container for tabs.
745
746@@ -26,7 +27,9 @@
747 @param height: height of the tab bar in percents (0-1)
748 @param color: color string to call from the theme
749 """
750+ Base.__init__(self)
751 clutter.Group.__init__(self)
752+
753 self.config = Configuration()
754
755 self.active = False
756@@ -112,16 +115,23 @@
757
758 self.tabs_list.append(tab)
759
760- # Set first tab active
761+ tab.active = False
762+
763+ # Set first tab visible
764 if self.current_tab == None:
765 self.current_tab = 0
766- self.tabs_list[0].show_tab()
767+ self.tabs_list[0].visible = True
768
769 tab_title = Label(0.01, self.color, 0, 0, tab.title)
770+ self.labels.append(tab_title)
771+ tab_title.set_name(tab.name)
772+
773 self.add(tab_title)
774- self.labels.append(tab_title)
775 self._reposition_labels()
776
777+ tab_title.set_reactive(True)
778+ tab_title.connect('button-press-event', self._on_tab_title_button_press)
779+
780 timeline = clutter.Timeline(25, 60)
781 alpha = clutter.Alpha(timeline, clutter.smoothstep_inc_func)
782 behaviour = clutter.BehaviourOpacity(255, 96, alpha)
783@@ -151,44 +161,45 @@
784 label.set_line_wrap(False)
785 label.set_ellipsize(pango.ELLIPSIZE_END)
786
787+ def _activate_tab(self, new_tab):
788+ """Activation of new selected Tab"""
789+ self.timelines[self.current_tab][0].set_direction(
790+ clutter.TIMELINE_FORWARD)
791+ self.timelines[self.current_tab][0].start()
792+
793+ self.timelines[new_tab][0].set_direction(clutter.TIMELINE_BACKWARD)
794+ self.timelines[new_tab][0].start()
795+
796+ # Change visible tab
797+ self.tabs_list[self.current_tab].visible = False
798+ self.tabs_list[self.current_tab].active = False
799+ self.tabs_list[new_tab].visible = True
800+ self.tabs_list[new_tab].active = False
801+
802 def _switch_tab_to_left(self):
803 """
804 Switch one tab to left if possible. Do nothing if there is no tab
805 """
806 if self.current_tab > 0 and self.current_tab is not None:
807+ self._activate_tab(self.current_tab - 1)
808 self.current_tab = self.current_tab - 1
809
810- # Animate labels
811- self.timelines[self.current_tab + 1][0].set_direction(
812- clutter.TIMELINE_FORWARD)
813- self.timelines[self.current_tab + 1][0].start()
814- self.timelines[self.current_tab][0].set_direction(
815- clutter.TIMELINE_BACKWARD)
816- self.timelines[self.current_tab][0].start()
817-
818- # Change visible tab
819- self.tabs_list[self.current_tab + 1].hide_tab()
820- self.tabs_list[self.current_tab].show_tab()
821-
822 def _switch_tab_to_right(self):
823 """
824 Switch one tab to right if possible. Do nothing if there is no tab
825 """
826 if self.current_tab < len(self.tabs_list) - 1 and (
827 self.current_tab is not None):
828+ self._activate_tab(self.current_tab + 1)
829 self.current_tab = self.current_tab + 1
830
831- # Animate labels
832- self.timelines[self.current_tab - 1][0].set_direction(
833- clutter.TIMELINE_FORWARD)
834- self.timelines[self.current_tab - 1][0].start()
835- self.timelines[self.current_tab][0].set_direction(
836- clutter.TIMELINE_BACKWARD)
837- self.timelines[self.current_tab][0].start()
838-
839- # Change visible tab
840- self.tabs_list[self.current_tab - 1].hide_tab()
841- self.tabs_list[self.current_tab].show_tab()
842+ def _switch_tab_to_down(self):
843+ """
844+ Switch down from current tab if allowed
845+ """
846+ if self.tabs_list[self.current_tab].can_activate():
847+ self.set_active(False)
848+ self.tabs_list[self.current_tab].active = True
849
850 def handle_user_event(self, event):
851 """
852@@ -201,13 +212,19 @@
853 self._switch_tab_to_left()
854 elif event_type == UserEvent.NAVIGATE_RIGHT:
855 self._switch_tab_to_right()
856- elif event_type == UserEvent.NAVIGATE_DOWN and (
857- self.tabs_list[self.current_tab].can_activate()):
858- self.set_active(False)
859- self.tabs_list[self.current_tab].set_active(True)
860+ elif event_type == UserEvent.NAVIGATE_DOWN:
861+ self._switch_tab_to_down()
862 else:
863 result = self.tabs_list[self.current_tab].handle_user_event(event)
864 if result:
865 self.set_active(True)
866- self.tabs_list[self.current_tab].set_active(False)
867+ self.tabs_list[self.current_tab].active = False
868+
869+ def _on_tab_title_button_press(self, actor, event):
870+ '''Title Label button-press handler'''
871+ clicked_tab = self._tabs[actor.get_name()]
872+ clicked_tab_index = self.tabs_list.index(clicked_tab)
873+
874+ self._activate_tab(clicked_tab_index)
875+ self.current_tab = clicked_tab_index
876

Subscribers

People subscribed via source and target branches