Merge lp:~samuel-buffet/entertainer/new-scrollmenu into lp:entertainer

Proposed by Samuel Buffet
Status: Merged
Approved by: Paul Hummer
Approved revision: 365
Merged at revision: not available
Proposed branch: lp:~samuel-buffet/entertainer/new-scrollmenu
Merge into: lp:entertainer
Diff against target: None lines
To merge this branch: bzr merge lp:~samuel-buffet/entertainer/new-scrollmenu
Reviewer Review Type Date Requested Status
Paul Hummer Approve
Matt Layman Approve
Jamie Bennett Pending
Review via email: mp+5342@code.launchpad.net

Commit message

The ScrollMenu is now reactive to pointer events.

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

Hi,

Finally, I think this branch is robust enough to be proposed.

The goal of this new scroll menu was to improve animations and to make it pointer reactive.

So, the scroll menu core mechanism has been created from scratch but I kept some old interface methods.

A new LoopedPath behaviour was created. It's more or less a 2 knots clutter path behaviour driven by an index. Index=0 place the actor on the starting knot and Index=1 place it on the end knot. Index can be whatever value we want but the actor will always be positioned on the 2 knots "infinite" path (ie if we apply the behaviour from idx=0 to idx=2 then the actor will move 2 times along the segment).

I've also added a MotionBuffer Class to make some calculations (distances, speeds) when we move the mouse. I've placed it its own file because this is something we can use later on other widgets (probably other menus). Right now it does more than needed by the ScrollMenu widget (only a vertical speed was needed).

I've also added a hide/show cursor mechanism (the one I discussed with Matt previously).

New interaction possible with the mouse are :

click on one menu item => moves it to central position
click on the menu item in central position => *enter* that menu
click + keeping the mouse button pressed and moving => we can slowly move the menu or give it an impulse to "turn" with a first try of kinetic effect based on the mouse speed motion.

The main screen has been modified to react to the new signals.

Cheers,
Samuel

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

As I've done some small changes since the proposal I post here the new diff.

=== modified file 'entertainerlib/frontend/gui/screens/main.py'
--- entertainerlib/frontend/gui/screens/main.py 2009-03-02 03:11:24 +0000
+++ entertainerlib/frontend/gui/screens/main.py 2009-04-09 19:29:11 +0000
@@ -44,65 +44,88 @@
         self.preview = clutter.Group() # Group that contains preview actors
         self.preview.set_position(self.get_abs_x(0.07), self.get_abs_y(0.1))
         self.preview.show()
+ self.preview.set_opacity(0x00)
         self.add(self.preview)

- self.active_menu = "main"
- self.rss_preview_menu = None # Pointer to the current preview menu
-
- self.menu = None
- self.playing_item = None
- self.create_main_menu()
-
- clock = ClockLabel(0.13, "screentitle", 0, 0.87)
- self.add(clock)
-
- def create_main_menu(self):
+ self.rss_preview_menu = self._create_rss_preview_menu()
+ self.preview.add(self.rss_preview_menu)
+
+ self._create_main_menu()
+ self.menu.active = True
+
+ self.add(ClockLabel(0.13, "screentitle", 0, 0.87))
+
+ def get_type(self):
+ """Return screen type."""
+ return Screen.NORMAL
+
+ def get_name(self):
+ """Return screen name (human readble)"""
+ return "Main"
+
+ def _create_main_menu(self):
         """Create main menu of the home screen"""
- self.menu = ScrollMenu(10, 60, self.config.show_effects())
+ self.menu = ScrollMenu(10, 60, 0.045, "menuitem_active")
         self.menu.set_name("mainmenu")
-
- # Common values for all ScrollMenu items
- size = 0.045
- color = "menuitem_active"
-
- item0 = Label(size, color, 0, 0, _("Play CD"), "disc")
- self.menu.add(item0)
-
- item2 = Label(size, color, 0, 0, _("Videos"), "videos")
- self.menu.add(item2)
-
- item3 = Label(size, color, 0, 0, _("Music"), "music")
- self.menu.add(item3)
-
- item4 = Label(size, color, 0, 0, _("Photographs"), "photo")
- self.menu.add(item4)
-
- item5 = Label(size, color, 0, 0, _("Headlines"), "rss")
- self.menu.add(item5)
+ self.menu.connect('selected', self._handle_select)
+ self.menu.connect('moved', self._on_menu_moved)
+ self.menu.connect('activated', self.main_menu_activation)
+
+ self.menu.add_item(_("Play CD"), "disc")
+ self.menu.add_item(_("Videos"), "videos")
+ self.menu.add_item(_("Music"), "music")
+ self.menu.add_item(_("Photographs"), "photo")
+ self.menu.add_item(_("Headlines"), "rss")

         if self.config.display_weather_in_frontend():
- item6 = Label(size, color, 0, 0, _("Weather"), "weather")
- self.menu.add(item6)
+ self.menu.add_item(_("Weather"), "weather")

         if self.config.display_cd_eject_in_frontend():
- item7 = Label(size, color, 0, 0, _("Eject CD"), "eject_cd")
- self.menu.add(item7)
+ self.menu.add_item(_("Eject CD"), "eject_cd")

         if self.media_player.has_media():
- playing = Label(size, color, 0, 0, _("Playing now..."), "playing")
- ...

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

Samuel,

I'm finally getting a chance to look at some of this code. I'm going to do this review in a couple of stages because of some functional regressions that I found while doing user testing. First, I'll let you know what those problems are so that you'll get a chance to fix them, then I'll come back and review the new widget code in more detail.

But let me start by saying this is really cool stuff. Sometimes it is easy to be critical and just point out faults in code, but I just want to let you know in advance that I think this work is pretty awesome. I'm sure you put quite a bit of effort into this code, and it shows.

Okay, now for the comments:

1) The scroll menu no longer remembers its last item when you return to a screen. Every time I hit back, it started on Music again.

2) The major problem that I have so far is that the keyboard interface is mostly broken if you return to the main screen after being on a different screen. For some reason, the menu doesn't think that it is active so it won't react until the user presses the right arrow key, then it becomes active.

Now for the minor comments (keep in mind that in this stage of the review, I've only looked at the Main screen and UserInterface files):

3) I'm not a fan of variable abbreviations most of the time. I noticed that you have the menu property "visible_nb" and I understand what it is supposed to mean, but since ScrollMenu uses the term "item," I think that "visible_items" would be a much clearer property name.

4) You created some new methods in Main and UserInterface (which is fine, of course), but I don't think that they match the conventions that we use for "private" methods. Since the methods you made aren't meant to be used outside the class, I would recommend that you prepend an underscore character to make that clear (e.g., preview_activation becomes _preview_activation). This isn't directly stated in PEP8, but there is some discussion about having private portions start with _.

As soon as these regressions are addressed, I'll come back and look specifically at the underlying widget code.

Thanks.

review: Needs Fixing
Revision history for this message
Samuel Buffet (samuel-buffet) wrote :
Download full text (47.2 KiB)

Hi Matt,

Thanks a lot for your review. I've fixed the things you've already noticed.

> 1) The scroll menu no longer remembers its last item when you return to a
> screen. Every time I hit back, it started on Music again.

This is fixed. I've also replace "..has_media()" by "..is_playing" because I thought it was closer to what we want in the preview. This will allow us in a close future to connect "play" "stop" events of the media_player to the refresh method so we'll have a dynamic update of it. (those events are created in *reactive_progress_bar*)

> 2) The major problem that I have so far is that the keyboard interface is
> mostly broken if you return to the main screen after being on a different
> screen. For some reason, the menu doesn't think that it is active so it won't
> react until the user presses the right arrow key, then it becomes active.

Yep didn't notice that. It's fixed now.

> 3) I'm not a fan of variable abbreviations most of the time. I noticed that
> you have the menu property "visible_nb" and I understand what it is supposed
> to mean, but since ScrollMenu uses the term "item," I think that
> "visible_items" would be a much clearer property name.
>
> 4) You created some new methods in Main and UserInterface (which is fine, of
> course), but I don't think that they match the conventions that we use for
> "private" methods. Since the methods you made aren't meant to be used outside
> the class, I would recommend that you prepend an underscore character to make
> that clear (e.g., preview_activation becomes _preview_activation). This isn't
> directly stated in PEP8, but there is some discussion about having private
> portions start with _.

Yes, that's right. I've renamed those private methods.

Thanks again Matt
Samuel-

new diff :

=== modified file 'entertainerlib/frontend/gui/screens/main.py'
--- entertainerlib/frontend/gui/screens/main.py 2009-03-02 03:11:24 +0000
+++ entertainerlib/frontend/gui/screens/main.py 2009-04-14 19:43:51 +0000
@@ -44,65 +44,89 @@
         self.preview = clutter.Group() # Group that contains preview actors
         self.preview.set_position(self.get_abs_x(0.07), self.get_abs_y(0.1))
         self.preview.show()
+ self.preview.set_opacity(0x00)
         self.add(self.preview)

- self.active_menu = "main"
- self.rss_preview_menu = None # Pointer to the current preview menu
-
- self.menu = None
- self.playing_item = None
- self.create_main_menu()
-
- clock = ClockLabel(0.13, "screentitle", 0, 0.87)
- self.add(clock)
-
- def create_main_menu(self):
+ self.rss_preview_menu = self._create_rss_preview_menu()
+ self.preview.add(self.rss_preview_menu)
+
+ self._create_main_menu()
+ self.menu.active = True
+
+ self.add(ClockLabel(0.13, "screentitle", 0, 0.87))
+
+ def get_type(self):
+ """Return screen type."""
+ return Screen.NORMAL
+
+ def get_name(self):
+ """Return screen name (human readble)"""
+ return "Main"
+
+ def _create_main_menu(self):
         """Create main menu of the home screen"""
- self.menu = ScrollMenu(10, 60, self.config.show_effects())
+ ...

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

Some details about the LoopedPathBehaviour Class and the way it's used in the ScrollMenu.

First, you have to know that this class is inspired by ClutterBehaviourEllipse.

With this standard Clutter behaviour, we can move actors along an Ellipse. Actors's position on this Ellipse are defined by angles, and a motion can be described by a angle_start and a angle_end. The angle can be seen as a parameter describing an actor's position along the ellipse.

The LoopedPathBehaviour can be seen as an extension of the regular PathBehaviour but where we specify a path (here only a line in fact => only 2 knots allowed atm) AND a couple start_index/end_index. This can't be done with the PathBehaviour, with this standard behaviour we must start at the starting knot and stop at the ending knot.

LoopedPathBehaviour allows us to specify a start_index and a end_index corresponding to a starting point and a ending point on the segment defined by the 2 knots.

This ability to specify a start_index and a end_index is used in the ScrollMenu that way :

* we create a LoopedPathBehaviour for every menu item and apply it to this item
* in the _update_behaviours we set the start knot and the end knot. Those knots (so the path) are the same for every menu item. And then for each item we specify a start_index and a end_index which is a function of the position of the item in the item list AND the position of the *selected item* (meaning the item which is in the middle of the visible are, the clipped area of the widget).

Trust this helps,
Samuel-

Note: I know this one is not totally obvious. It's used in a much simpler way on the ScrollArea widget because there it's applied to only one widget, the moving clutter group including the clutter Label.

On the _update_behaviours method, we

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

Samuel,

These are the comments that I had written down before you last push. I still haven't looked at MotionBuffer and I need to give all the files a second look through, but I think it would be a good start to address some of these things. I hope you find them helpful. Most of the comments are just an attempt to keep a unified style in the codebase. The comments might seem a little rough because they are basically just my notes. I hope that they're readable enough.

main.py:
 * _create_main_menu should return the menu. And the main screen should add that menu to itself. This will ensure that the init is the place that self.menu is created. It would also allow testing of _create_main_menu in the future (if we ever figure out what we would need to test). I did this for _create_rss_preview_menu and I think that it's a nice separation of responsibility. After all, shouldn't it only be the init that adds instance variables to itself?
 * docstrings need improvement for _on_menu_moved and _on_menu_selected. Don't use "We" and try to reduce to one line if you can.
 * hide_cursor_timeout_callback and handle_motion_event seem private. They should probably start with an underscore.

scroll_menu.py:
 * import order is wrong because math is a core library.
 * docstring doesn't need "This is" and might fit on one line
 * What is __gtype_name__ for?
 * Could clean up docstrings for get_active, set_active, get_selected (no need for @return and @param).
 * idx, xx, and yy aren't very intuitive variable names in _display_items_at_target
 * In _set_selected_index, I don't understand "#compute selected index to be between 0 and nb." What is nb? number? What number if so.
 * I'd suggest you remove the @author tag in get_index and just add Joshua Scotton as an author to the top of the file. I don't think we need to identify authors at the method level (that's what bzr annotate is for), but he still has the right to credit himself with something if that is what he wrote.

special_behaviours.py:
 * docstring is wrong at top of file.
 * import order is wrong. math is a core lib and clutter is not so math should be first and clutter should be below it separated by a newline.
 * No need for the space between class name and parenthetical superclass.
 * "custome" is actually spelled custom
 * do_alpha_notify has a space before its parameter list
 * Don't need "We" in the docstring for do_alpha_notify
 * what are idx and idxx?
 * Does path_length need to return an integer? (because it says length in pixels)

test_scrollmenu.py:
 * has an empty line with spaces.
 * testCreate should use a PEP8 method name. test_create (We use camel case for setUp and tearDown because that's what unittest does. Any existing test that we have that uses camel case is just because it's from a time before started being fully PEP8 compliant.)
 * test ScrollMenu instead of object

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

Hi Matt,

First, I'd like to thank you for all the efforts you put in this review. This really helps and I deeply appreciate.

In my last push, I've fixed all what you've already notice and more in the same spirit (mainly docstrings).

> a unified style in the codebase. The comments might seem a little rough
> because they are basically just my notes. I hope that they're readable enough.

No pb here, there are perfectly readable and not rough at all (I mean things have to be said)

> test_scrollmenu.py:
> * has an empty line with spaces.
> * testCreate should use a PEP8 method name. test_create (We use camel case
> for setUp and tearDown because that's what unittest does. Any existing test
> that we have that uses camel case is just because it's from a time before

Hmm, Matt I don't know what *camel* is about

Now, the good point is that after this review, everything will look more or less relax and easy ;)

Samuel-

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

Samuel,

It seems like you have been very busy making modifications to this branch. Now that thing have stabilized a bit, let me throw another few comments at you. After this round of comments, I really only have to review the MotionBuffer class (I don't the time to do that tonight). Again, thanks for the good work.

main.py:
 * _move_menu is now a dead unused method that can be removed.

scroll_menu.py:
 * _on_release_event has more idx variables
 * line 301 could use some spacing around the less than character for clarity
 * _on_motion_event: why is it showing the cursor? That is done by the UserInterface.
 * _on_scroll_event: why is it showing the cursor? That is done by the UserInterface.

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

Hi Matt,

> Samuel,
>
> It seems like you have been very busy making modifications to this branch. Now
> that thing have stabilized a bit, let me throw another few comments at you.
> After this round of comments, I really only have to review the MotionBuffer
> class (I don't the time to do that tonight). Again, thanks for the good work.
>
> main.py:
> * _move_menu is now a dead unused method that can be removed.

Indeed, I've removed it.

> scroll_menu.py:
> * _on_release_event has more idx variables
> * line 301 could use some spacing around the less than character for clarity

Fixed.

> * _on_motion_event: why is it showing the cursor? That is done by the
> UserInterface.
> * _on_scroll_event: why is it showing the cursor? That is done by the
> UserInterface.

Yeah that's done by the UserInterface but if for instance you place your cursor on the menu and scroll it (no motion only scroll) then after 4 seconds the cursor will be hidden without the cursor.show in _on_scroll_event because there's no motion event grabbed by the stage. Just try to comment that line out, it looks strange from the user experience point of view (I think).

Same kind of thing with _on_motion_event if you want to click on the menu and, keeping the pressure on the button you play with it up and down. It will hide the cursor after 4 seconds but here the answer to "why?" is more subtle. Because here we have motion-events but they are grabbed only by the menu because of the clutter.grab_pointer(self) placed on _on_button_press_event. This redirection of all events to the menu is only released in _on_button_release_event by clutter.ungrab_pointer().
So until the button is released the stage is blind. It see no event at all so the timeout is not reset and hides the cursor after 4 seconds.

Now, after having explain that, maybe I should explain why I've done this grab/ungrab stuff. Simply it's because without clutter.grab_pointer(self) event are seen by the menu only if the cursor is over the menu. And when we move it to give it an impulse to scroll we can easily finish the cursor's motion (= release the mouse's button) anywhere with no guaranty to be over the menu. So in this case, the _on_button_release_event event will never be seen by the menu and it will be in a unstable weird state.

I've tried another way to detect the button's release using the ClutterModifierType field of a motion event which is supposed to provide the state of button (if I understand correctly). But unfortunately, it looks like it doesn't work (maybe after more tests I'll file a bug upstream).

Cheers,
Samuel-

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

Okay, I finally had a good chance to sit down and look at MotionBuffer so without further adieu:

motion_buffer.py:

This is my understanding of MotionBuffer: A widget will have a private instance of MotionBuffer. When an event occurs, MotionBuffer will have some methods called to do some calculations. Once the calculations are done, the widget will access some properties of the buffer to determine what kind of motion happened and how to handle that motion. If this understanding is incorrect, please feel free to point out other details.

Comments:
 * I think that the class should be a new style class (inherit from object)
 * I'm not a big fan of the class doc string. I know Paul has recommended to me in the past to stay away from "This class..." wording. Additionally, the description is pretty vague. What are "some calculations"? It's not a bad thing to have a longer docstring, especially if the class is more abstract like this one is.
 * init could use a comment describing what "ema" is.
 * compute methods docstrings are sentences so they each need a period. And it should be "deltas and speeds".
 * scroll menu is using instance variables of motion buffer without using property methods. I know that there are a lot of instance variables here. Would it make sense to turn these into property methods?
 * Speaking of lots of instance variables, there seems to be a natural grouping of these variables. Would it help the class if those variable were grouped into their own class in a generic way. Maybe it would be a Moment class or something, and MotionBuffer would have three different instance of moments for start, last_motion_event, and ema. Then you could have a common compute method that would eliminate some duplicated code. I'm just trying to brainstorm here because I feel like I'm getting lost in a sea of instance variables. From the design perspective, this code seems to have a "smell" because of the long variable names. When you have a variable named "dy_from_last_motion_event," it makes me wonder if the design can be improved to create some important abstract concept.

Revision history for this message
Paul Hummer (rockstar) wrote :
Download full text (22.5 KiB)

Samuel-

  This is a very large patch, which makes it difficult to truly review well.
I've asked lots of questions, and hope that through those questions, you might
be able to evaluate the implementation from another viewpoint, and discover
issues on your own. I'm not going to vote just yet, as I'm curious to hear
your response to my review.

> === modified file 'entertainerlib/frontend/gui/screens/main.py'
> --- entertainerlib/frontend/gui/screens/main.py 2009-04-17 21:02:35 +0000
> +++ entertainerlib/frontend/gui/screens/main.py 2009-04-25 23:30:15 +0000
> @@ -47,16 +47,55 @@ class Main(Screen):
> self.preview.set_opacity(0x00)
> self.add(self.preview)
>
> - self.active_menu = "main"
> self.rss_preview_menu = self._create_rss_preview_menu()
> self.preview.add(self.rss_preview_menu)
>
> - self.menu = None
> - self.playing_item = None
> - self.create_main_menu()
> + self.menu = self._create_main_menu()
> + self.add(self.menu)
> + self.menu.connect('selected', self._on_menu_selected)
> + self.menu.connect('moved', self._on_menu_moved)
> + self.menu.connect('activated', self._main_menu_activation)
> + self.menu.active = True
> +
> + self.add(ClockLabel(0.13, "screentitle", 0, 0.87))
> +

This makes me a bit queesy. Why are you creating an instance variable, and
then calling an instance method on that instance variable. What does self.add
do? Why are we calling it? If it is required (and I'm concerned it isn't),
self._create_main_menu should be calling self.add(). Also, _create_main_menu
should be doing all the self.menu.connect type stuff.

Also, I see the clock got moved here instead of the _create_main_menu code.
Why?

> + def get_type(self):
> + """Return screen type."""
> + return Screen.NORMAL
> +
> + def get_name(self):
> + """Return screen name (human readble)"""
> + return "Main"
> +

I've seen this convention a few times. What are we using get_name for? Is
there a better way to handle this than using a string? I'd like some answers
about this, because I thought Joshua had eliminated the need for this code.

Also, it seems that type and name would be better as properties. They never
change dynamically, so making them instance properties is a good idea. If type
or name had the ability to change within the life of the instance, we would use
accessors, but that's more for convention than anything. In this case though,
there isn't even a need for defining a function. This code could be simplified
to:

    type = Screen.NORMAL
    name = "Main"

> @@ -206,42 +200,23 @@ class Main(Screen):
>
> return preview
>
> - def get_type(self):
> - """Return screen type."""
> - return Screen.NORMAL
> -
> - def get_name(self):
> - """Return screen name (human readble)"""
> - return "Main"
> -
> - def update(self):
> - """
> - Update screen widgets. This is called always when screen is poped from
> - the screen history. Updates main menu widget.
> - """
> - selected_name = self.menu.get_selected().get_name()
...

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

Matt,

> motion_buffer.py:
>
> This is my understanding of MotionBuffer: A widget will have a private
> instance of MotionBuffer. When an event occurs, MotionBuffer will have some
> methods called to do some calculations. Once the calculations are done, the
> widget will access some properties of the buffer to determine what kind of
> motion happened and how to handle that motion. If this understanding is
> incorrect, please feel free to point out other details.

Yes this is precisely what this Class does. It's a tool Class added to widgets which need to computes cursor's motion characteristics (speeds / distances) in order to do a kinetic effect.

> Comments:
> * I think that the class should be a new style class (inherit from object)

Fixed.

> * I'm not a big fan of the class doc string. I know Paul has recommended to
> me in the past to stay away from "This class..." wording. Additionally, the
> description is pretty vague. What are "some calculations"? It's not a bad
> thing to have a longer docstring, especially if the class is more abstract
> like this one is.
> * init could use a comment describing what "ema" is.

Fixed. Yeah, that was a bit short. More detailed now.

> * compute methods docstrings are sentences so they each need a period. And it
> should be "deltas and speeds".

Fixed

> * scroll menu is using instance variables of motion buffer without using
> property methods. I know that there are a lot of instance variables here.
> Would it make sense to turn these into property methods?

Well, this is a point but I don't see the benefit.

I can understand:

@property
def foo(self):
    do_something
    return value

Because there we wrap a treatment before returning the value.

also I understand:

def _get_myproperty(self):
    return value

def _set_myproperty(self, value):
    self._value = value
    do_something

myproperty = property(_get_myproperty, _set_myproperty)

Because there we wrap a treatment setting the value.

But I don't see the added value of

@property
def foo(self):
    return value

In this case I think (I may be wrong) that it's cleaner, simpler and correct to access directly the variable itself.

> * Speaking of lots of instance variables, there seems to be a natural
> grouping of these variables. Would it help the class if those variable were
> grouped into their own class in a generic way. Maybe it would be a Moment
> class or something, and MotionBuffer would have three different instance of
> moments for start, last_motion_event, and ema. Then you could have a common
> compute method that would eliminate some duplicated code. I'm just trying to
> brainstorm here because I feel like I'm getting lost in a sea of instance
> variables. From the design perspective, this code seems to have a "smell"
> because of the long variable names. When you have a variable named
> "dy_from_last_motion_event," it makes me wonder if the design can be improved
> to create some important abstract concept.

Well, I don't see the need to add complexity there.

I've pushed the fixes mentioned *Fixed* above.

Thanks a lot Matt.
Samuel-

Revision history for this message
Matt Layman (mblayman) wrote :
Download full text (7.4 KiB)

Paul,

I'd like to add some comments of my own to what you've said. I think I can clear up some points about clutter since you haven't been doing much work in the frontend. I also have some general remarks based on what you've stated.

> > === modified file 'entertainerlib/frontend/gui/screens/main.py'
> > --- entertainerlib/frontend/gui/screens/main.py 2009-04-17 21:02:35
> +0000
> > +++ entertainerlib/frontend/gui/screens/main.py 2009-04-25 23:30:15
> +0000
> > @@ -47,16 +47,55 @@ class Main(Screen):
> > self.preview.set_opacity(0x00)
> > self.add(self.preview)
> >
> > - self.active_menu = "main"
> > self.rss_preview_menu = self._create_rss_preview_menu()
> > self.preview.add(self.rss_preview_menu)
> >
> > - self.menu = None
> > - self.playing_item = None
> > - self.create_main_menu()
> > + self.menu = self._create_main_menu()
> > + self.add(self.menu)
> > + self.menu.connect('selected', self._on_menu_selected)
> > + self.menu.connect('moved', self._on_menu_moved)
> > + self.menu.connect('activated', self._main_menu_activation)
> > + self.menu.active = True
> > +
> > + self.add(ClockLabel(0.13, "screentitle", 0, 0.87))
> > +
>
> This makes me a bit queesy. Why are you creating an instance variable, and
> then calling an instance method on that instance variable. What does self.add
> do? Why are we calling it? If it is required (and I'm concerned it isn't),
> self._create_main_menu should be calling self.add(). Also, _create_main_menu
> should be doing all the self.menu.connect type stuff.

I hadn't really thought about it, but I think you're correct, the connect method calls and active setting could move into _create_main_menu.

self.add() is a clutter.Group method. What's it's doing is adding the self.menu clutter.Group to the Main screen (which itself is a clutter.Group). I had suggested to Samuel that self.add does not belong in create_main_menu because it should be clear that it is the init's responsibility to add new groups to itself. That's my opinion because I've often asked myself "where the hell is the widget being added?" and if it was all the init, we wouldn't have to hunt in other methods for when things show up on the screen. Basically, it was an experience judgment call from working with some of our clutter code.

> Also, I see the clock got moved here instead of the _create_main_menu code.
> Why?

main_menu is really just an initialized version of a ScrollMenu widget. The group doesn't represent everything that could show up on the Main screen. I think declaring the ClockLabel in Main's init makes sense.

> > + def get_type(self):
> > + """Return screen type."""
> > + return Screen.NORMAL
> > +
> > + def get_name(self):
> > + """Return screen name (human readble)"""
> > + return "Main"
> > +
>
> I've seen this convention a few times. What are we using get_name for? Is
> there a better way to handle this than using a string? I'd like some answers
> about this, because I thought Joshua had eliminated the need for this code.
>
> Also, it seems that type and name ...

Read more...

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

Hi Paul,

> > === modified file 'entertainerlib/frontend/gui/screens/main.py'
> > --- entertainerlib/frontend/gui/screens/main.py 2009-04-17 21:02:35
> +0000
> > +++ entertainerlib/frontend/gui/screens/main.py 2009-04-25 23:30:15
> +0000
> > @@ -47,16 +47,55 @@ class Main(Screen):
> > self.preview.set_opacity(0x00)
> > self.add(self.preview)
> >
> > - self.active_menu = "main"
> > self.rss_preview_menu = self._create_rss_preview_menu()
> > self.preview.add(self.rss_preview_menu)
> >
> > - self.menu = None
> > - self.playing_item = None
> > - self.create_main_menu()
> > + self.menu = self._create_main_menu()
> > + self.add(self.menu)
> > + self.menu.connect('selected', self._on_menu_selected)
> > + self.menu.connect('moved', self._on_menu_moved)
> > + self.menu.connect('activated', self._main_menu_activation)
> > + self.menu.active = True
> > +
> > + self.add(ClockLabel(0.13, "screentitle", 0, 0.87))
> > +
>
> This makes me a bit queesy. Why are you creating an instance variable, and
> then calling an instance method on that instance variable. What does self.add
> do? Why are we calling it? If it is required (and I'm concerned it isn't),
> self._create_main_menu should be calling self.add(). Also, _create_main_menu
> should be doing all the self.menu.connect type stuff.

Well, this was discussed with Matt in a previous comment. The idea is to add widgets only in the __init__ method of a screen. I really don't know at the moment the *right* place of the connect calls. It's more tricky than it seems on the first looks. Because every actor has to be created before. Why? simply because if we create a widget (let's say a menu) and connect its *activated* signal to a *menu_activate* callback. In this call back we will deactivate (in the future when reactive widgets will be all together) the other reactive widgets of the screen. So if there not created ... error

That is why I think the good place for connections is in the end of the __init__ method. I'll take care of that (including other optimizations and simplifications) in the *reactive_global* branch I'll do in the end.

> Also, I see the clock got moved here instead of the _create_main_menu code.
> Why?

Yep, see above.

>
> > + def get_type(self):
> > + """Return screen type."""
> > + return Screen.NORMAL
> > +
> > + def get_name(self):
> > + """Return screen name (human readble)"""
> > + return "Main"
> > +
>
> I've seen this convention a few times. What are we using get_name for? Is
> there a better way to handle this than using a string? I'd like some answers
> about this, because I thought Joshua had eliminated the need for this code.
>
> Also, it seems that type and name would be better as properties. They never
> change dynamically, so making them instance properties is a good idea. If
> type
> or name had the ability to change within the life of the instance, we would
> use
> accessors, but that's more for convention than anything. In this case though,
> there isn't even a need for defining a function. This code ...

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

Samuel, the only thing keeping me from approving this branch is a lack of some tests. As Paul suggested, you could add some tests to test the state of some of the instance variables. It may require creating a mock event that you can feed to the ScrollMenu to probe things (rather than connecting to any handler methods).

I would suggest something like this:
1) You could add assertEqual statements to the create test so that you verify that your instance variables that get parameter values actually equal those values
2) There aren't any tests for MotionBuffer yet and I think it is important to add some for that class (because I think MotionBuffer is going to affect the behavior of most of our widgets so it needs some coverage).
 * You could do a create test and check that instance variable are the values you expect them to be.
 * You could pass in a mock event, compute the changes, and then make sure that those values are what you would expect. One way to do this would be to set up the test in a way that you know it will fail, then when you get the correct value from the failure, replace your faulty value with the value from the failure. You might ask "what is the value for this kind of test?" My answer is that it helps you capture the current behavior. If you think that MotionBuffer is acting the way you expect it to, then you want to preserve that. If you add the kind of test that I just described, you'll be able to find out when someone changes the behavior of MotionBuffer. In the future, you might decide that it is okay or you can decide that the change in behavior is wrong.

E.g.,

self.motion_buffer.compute_from_start(mock_event)
self.assertEqual(self.motion_buffer.distance_from_start, 'boogers')

I'm hoping this will fail :). Once it fails, replace 'boogers' with the value that you received. You may have to use assertAlmostEqual for some of the math instance variables.

Anyway, those are just my thoughts about what you could do to get some test coverage on this widgets. Trust me, I feel your pain when it comes to testing widgets. It's a big pain in the rear.

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

Matt, Paul,

Find bellow the diff of my last 3 commits :

I've made the diff between a local copy and the one pushed on lp:
So everything starting with + has been removed and everything starting with - has been added.

I've done a bit of clean up in the MotionBuffer Class and added more tests.

Samuel-

=== modified file 'entertainerlib/frontend/gui/widgets/motion_buffer.py'
--- entertainerlib/frontend/gui/widgets/motion_buffer.py 2009-04-28 09:49:40 +0000
+++ entertainerlib/frontend/gui/widgets/motion_buffer.py 2009-04-28 12:19:55 +0000
@@ -22,6 +22,9 @@
         self.dx_from_start = 0
         self.dy_from_start = 0
         self.distance_from_start = 0
+ self.speed_x_from_start = 0
+ self.speed_y_from_start = 0
+ self.speed_from_start = 0

         self.dt_from_last_motion_event = 0
         self.dx_from_last_motion_event = 0
@@ -84,6 +87,18 @@
         self.distance_from_start = math.sqrt(self.dx_from_start * \
             self.dx_from_start + self.dy_from_start * self.dy_from_start)

+ if self.dt_from_start != 0 :
+ self.speed_x_from_start = float(self.dx_from_start) / \
+ float(self.dt_from_start)
+ self.speed_y_from_start = float(self.dy_from_start) / \
+ float(self.dt_from_start)
+ self.speed_from_start = float(self.distance_from_start) / \
+ float(self.dt_from_start)
+ else:
+ self.speed_x_from_start = 0
+ self.speed_y_from_start = 0
+ self.speed_from_start = 0
+
     def compute_from_last_motion_event(self, event):
         '''Compute deltas and speeds from the last motion-event.'''
         self.dt_from_last_motion_event = event.time - \

=== modified file 'entertainerlib/frontend/gui/widgets/scroll_menu.py'
--- entertainerlib/frontend/gui/widgets/scroll_menu.py 2009-04-28 12:18:16 +0000
+++ entertainerlib/frontend/gui/widgets/scroll_menu.py 2009-04-28 12:19:55 +0000
@@ -117,10 +117,6 @@

     active = property(_get_active, _set_active)

- def stop_animation(self):
- '''Stops the timeline driving menu animation.'''
- self._timeline.stop()
-
     def _update_behaviours(self, target):
         """Preparation of behaviours applied to menu items before animation"""
         items_len = len(self._items)

=== modified file 'entertainerlib/tests/mock.py'
--- entertainerlib/tests/mock.py 2009-04-28 11:49:43 +0000
+++ entertainerlib/tests/mock.py 2009-04-28 12:19:55 +0000
@@ -304,12 +304,3 @@
         '''See `VideoLibrary.get_number_of_video_clips`.'''
         return 0

-
-class MockPointerEvent(object):
- '''Mock a pointer event like those generated by Clutter'''
-
- def __init__(self):
- self.x = 0
- self.y = 0
- self.time = 0
-

=== removed file 'entertainerlib/tests/test_motionbuffer.py'
--- entertainerlib/tests/test_motionbuffer.py 2009-04-28 12:18:16 +0000
+++ entertainerlib/tests/test_motionbuffer.py 1970-01-01 00:00:00 +0000
@@ -1,83 +0,0 @@
-"""Tests MotionBuffer"""
-
-__license__ = "GPLv2"
-__copyright__ = "2009, Samuel Buffet"
-__author__ = "Samuel Buffet <email address hidden>"
-
-from entertainerlib.frontend.gui.widgets.motion_buf...

Read more...

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

Samuel, I'll be able to look over your latest changes and tests when I get home from work today. Barring any obvious problems, I'll likely approve the proposal today. If you could set the commit message (it's an option in the proposal details), then the approval and commit message should be indicators to tarmac to commit the work to the trunk.

I think this is how we will be committing stuff to the trunk from here on out, but I need to document that on the mailing list and work with Paul to make sure that Tarmac is ready to handle that.

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

What a long review.... Samuel, I checked out your tests and they seem reasonable to me. Also, make test and lint passes (to the level I would expect, there are errors but they are unrelated to this code). There could probably be more tests, but I guess there could always be more tests. :)

I am satisfied enough to have this code committed to the trunk. This is a great start toward reactive widgets and I'm hoping that future ones will be easier to review now that we've covered the groundwork. I think that this branch is one step closer to making a lot of people happy who will eventually want touch screen support for Entertainer.

Nice work.

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

> What a long review....

And it will get a bit longer because it's important for me to have Paul's vote on it ;)
Et has commented it and wrote that I'll give his vote later so it means that he has an opinion, and I'd like to have it. Paul ?

> Nice work.

Thanks Matt,
Samuel-

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

Sorry, I didn't realize Paul actually wanted to cast a vote on it for this branch.

364. By Samuel Buffet

merged with trunk 367

365. By Samuel Buffet

pylint: disable-msg=W0702 added to fix lint after last reintroduction of a try/except without particular exception catched.

Revision history for this message
Paul Hummer (rockstar) wrote :

You probably don't need to block on my comments. I can make code comments, but Matt really knows the UI code a lot better than I do. Even if this branch isn't ready for production, we have a little over a month to test it out and make sure that we can actually get things ironed out before release. Since this is a core part of the next release, I think even early code is better than no code.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'entertainerlib/frontend/gui/screens/main.py'
--- entertainerlib/frontend/gui/screens/main.py 2009-03-02 03:11:24 +0000
+++ entertainerlib/frontend/gui/screens/main.py 2009-04-08 11:23:26 +0000
@@ -44,65 +44,89 @@
44 self.preview = clutter.Group() # Group that contains preview actors44 self.preview = clutter.Group() # Group that contains preview actors
45 self.preview.set_position(self.get_abs_x(0.07), self.get_abs_y(0.1))45 self.preview.set_position(self.get_abs_x(0.07), self.get_abs_y(0.1))
46 self.preview.show()46 self.preview.show()
47 self.preview.set_opacity(0x00)
47 self.add(self.preview)48 self.add(self.preview)
4849
49 self.active_menu = "main"50
50 self.rss_preview_menu = None # Pointer to the current preview menu51 self.rss_preview_menu = self._create_rss_preview_menu()
5152 self.preview.add(self.rss_preview_menu)
52 self.menu = None53
53 self.playing_item = None54 self._create_main_menu()
54 self.create_main_menu()55 self.menu.active = True
5556
56 clock = ClockLabel(0.13, "screentitle", 0, 0.87)57 self.add(ClockLabel(0.13, "screentitle", 0, 0.87))
57 self.add(clock)58
5859 def get_type(self):
59 def create_main_menu(self):60 """Return screen type."""
61 return Screen.NORMAL
62
63 def get_name(self):
64 """Return screen name (human readble)"""
65 return "Main"
66
67 def _create_main_menu(self):
60 """Create main menu of the home screen"""68 """Create main menu of the home screen"""
61 self.menu = ScrollMenu(10, 60, self.config.show_effects())69 self.menu = ScrollMenu(10, 60, 0.045, "menuitem_active")
62 self.menu.set_name("mainmenu")70 self.menu.set_name("mainmenu")
6371 self.menu.connect('selected', self._handle_select)
64 # Common values for all ScrollMenu items72 self.menu.connect('moved', self._on_menu_moved)
65 size = 0.04573 self.menu.connect('activated', self.main_menu_activation)
66 color = "menuitem_active"74
6775 self.menu.add_item(_("Play CD"), "disc")
68 item0 = Label(size, color, 0, 0, _("Play CD"), "disc")76 self.menu.add_item(_("Videos"), "videos")
69 self.menu.add(item0)77 self.menu.add_item(_("Music"), "music")
7078 self.menu.add_item(_("Photographs"), "photo")
71 item2 = Label(size, color, 0, 0, _("Videos"), "videos")79 self.menu.add_item(_("Headlines"), "rss")
72 self.menu.add(item2)
73
74 item3 = Label(size, color, 0, 0, _("Music"), "music")
75 self.menu.add(item3)
76
77 item4 = Label(size, color, 0, 0, _("Photographs"), "photo")
78 self.menu.add(item4)
79
80 item5 = Label(size, color, 0, 0, _("Headlines"), "rss")
81 self.menu.add(item5)
8280
83 if self.config.display_weather_in_frontend():81 if self.config.display_weather_in_frontend():
84 item6 = Label(size, color, 0, 0, _("Weather"), "weather")82 self.menu.add_item(_("Weather"), "weather")
85 self.menu.add(item6)
8683
87 if self.config.display_cd_eject_in_frontend():84 if self.config.display_cd_eject_in_frontend():
88 item7 = Label(size, color, 0, 0, _("Eject CD"), "eject_cd")85 self.menu.add_item(_("Eject CD"), "eject_cd")
89 self.menu.add(item7)
9086
91 if self.media_player.has_media():87 if self.media_player.has_media():
92 playing = Label(size, color, 0, 0, _("Playing now..."), "playing")88 self.menu.add_item(_("Playing now..."), "playing")
93 self.menu.add(playing)89
9490 self.menu.visible_nb = 5
95 self.menu.set_number_of_visible_items(5)91 self.menu.selected_index = 2
96 menu_clip = self.menu.get_number_of_visible_items() * 7092
9793 menu_clip = self.menu.visible_nb * 70
98 # Menu position94 # Menu position
99 menu_y = int((self.config.get_stage_height() - menu_clip + 10) / 2)95 menu_y = int((self.config.get_stage_height() - menu_clip + 10) / 2)
100 self.menu.set_position(self.get_abs_x(0.75), menu_y)96 self.menu.set_position(self.get_abs_x(0.75), menu_y)
10197
102 self.add(self.menu)98 self.add(self.menu)
10399
104 def show_playing_preview(self):100 def _create_rss_preview_menu(self):
105 """Create a group that displays information on current media."""101 '''Create the RSS preview menu that will show feed highlights. An
102 uninitialized menu will be returned to prevent move errors if there
103 is nothing in the feed library.'''
104 menu = TextMenu(self.theme, self.config.show_effects())
105
106 if self.feed_library.is_empty() is False:
107 menu.set_row_count(1)
108 menu.set_position(self.get_abs_x(0.035), self.get_abs_y(0.12))
109 menu.set_item_size(self.get_abs_x(0.549), self.get_abs_y(0.078))
110
111 # List of latest entries. pack = (Feed object, Entry object)
112 entries = self.feed_library.get_latest_entries(5)
113 for pack in entries:
114 text = pack[0].get_title() + " - " + pack[1].get_title()
115 item = TextMenuItem(0.549, 0.078,
116 FeedEntryParser().strip_tags(text),
117 pack[1].get_date())
118 kwargs = { 'feed' : pack[0], 'entry' : pack[1] }
119 item.set_userdata(kwargs)
120 menu.add_actor(item)
121
122 menu.set_active(False)
123
124 return menu
125
126 def _create_playing_preview(self):
127 '''Create the Now Playing preview sidebar.'''
128 preview = clutter.Group()
129
106 # Video preview of current media130 # Video preview of current media
107 video_texture = self.media_player.get_texture()131 video_texture = self.media_player.get_texture()
108 if video_texture == None:132 if video_texture == None:
@@ -141,75 +165,72 @@
141 rect.set_size(int(new_width + 6), int(new_height + 6))165 rect.set_size(int(new_width + 6), int(new_height + 6))
142 rect.set_position(rect_x, rect_y)166 rect.set_position(rect_x, rect_y)
143 rect.set_color((128, 128, 128, 192))167 rect.set_color((128, 128, 128, 192))
144 self.preview.add(rect)168 preview.add(rect)
145169
146 self.preview.add(video_texture)170 preview.add(video_texture)
147171
148 title_text = _("Now Playing: %(title)s") % \172 title_text = _("Now Playing: %(title)s") % \
149 {'title': self.media_player.get_media_title()}173 {'title': self.media_player.get_media_title()}
150 title = Label(0.03, "text", 0.03, 0.74, title_text)174 title = Label(0.03, "text", 0.03, 0.74, title_text)
151 self.preview.add(title)175 preview.add(title)
152176
153 def show_rss_preview(self):177 return preview
154 """Rss preview"""178
179 def _create_rss_preview(self):
180 '''Create the RSS preview sidebar.'''
181 preview = clutter.Group()
182
183 # RSS Icon
184 icon = Texture(self.theme.getImage("rss_icon"), 0, 0.04)
185 icon.set_scale(0.6, 0.6)
186 preview.add(icon)
187
188 # RSS Feed Title
189 title = Label(0.075, "title", 0.045, 0.03, _("Recent headlines"))
190 preview.add(title)
155191
156 if self.feed_library.is_empty() is False:192 if self.feed_library.is_empty() is False:
157 # RSS Icon193 menu = self._create_rss_preview_menu()
158 icon = Texture(self.theme.getImage("rss_icon"), 0, 0.04)194 preview.add(menu)
159 icon.set_scale(0.6, 0.6)195 self.rss_preview_menu = menu
160196 else:
161 # RSS Feed Title197 # No headlines available in the library
162 title = Label(0.075, "title", 0.045, 0.03, _("Recent headlines"))198 info = Label(0.05, "title", 0.1, 0.35, _("No headlines available"))
163199 preview.add(info)
164 menu = TextMenu(self.theme, self.config.show_effects())200
165 menu.set_row_count(1)201 return preview
166 menu.set_position(self.get_abs_x(0.035), self.get_abs_y(0.12))202
167 menu.set_item_size(self.get_abs_x(0.549), self.get_abs_y(0.078))203 def _update_preview_area(self):
168204 '''Update the preview area to display the current menu item.'''
169 # List of latest entries. pack = (Feed object, Entry object)205 self.preview.remove_all()
170 entries = self.feed_library.get_latest_entries(5)206 item = self.menu.get_selected()
171 for pack in entries:207
172 text = pack[0].get_title() + " - " + pack[1].get_title()208 self.preview.set_opacity(0x00)
173 item = TextMenuItem(0.549, 0.078,209
174 FeedEntryParser().strip_tags(text),210 update = True
175 pack[1].get_date())211
176 kwargs = { 'feed' : pack[0], 'entry' : pack[1] }212 if item.get_name() == "playing":
177 item.set_userdata(kwargs)213 self.preview.add(self._create_playing_preview())
178 menu.add_actor(item)214 elif item.get_name() == "rss":
179215 self.preview.add(self._create_rss_preview())
180 menu.set_active(False)216 else:
181217 update = False
182 # Fade in timeline218
219 # If the preview was updated fade it in
220 if update:
183 fade_in = clutter.Timeline(20, 60)221 fade_in = clutter.Timeline(20, 60)
184 alpha_in = clutter.Alpha(fade_in, clutter.smoothstep_inc_func)222 alpha_in = clutter.Alpha(fade_in, clutter.smoothstep_inc_func)
185 self.in_behaviour = clutter.BehaviourOpacity(0x00, 0xff, alpha_in)223 self.behaviour = clutter.BehaviourOpacity(0x00, 0xff, alpha_in)
186 self.in_behaviour.apply(menu)224 self.behaviour.apply(self.preview)
187 self.in_behaviour.apply(title)
188 self.in_behaviour.apply(icon)
189
190 self.preview.add(icon)
191 self.preview.add(title)
192 self.rss_preview_menu = menu
193 self.preview.add(menu)
194
195 menu.set_opacity(0x00)
196 icon.set_opacity(0x00)
197 title.set_opacity(0x00)
198
199 fade_in.start()225 fade_in.start()
200226
227 def _can_move_horizontally(self):
228 '''Return a boolean indicating if horizontal movement is allowed.'''
229 item = self.menu.get_selected()
230 if self.feed_library.is_empty() or item.get_name() != 'rss':
231 return False
201 else:232 else:
202 # No headlines available in the library233 return True
203 info = Label(0.07, "title", 0.1, 0.35, _("No headlines available"))
204 self.preview.add(info)
205
206 def get_type(self):
207 """Return screen type."""
208 return Screen.NORMAL
209
210 def get_name(self):
211 """Return screen name (human readble)"""
212 return "Main"
213234
214 def update(self):235 def update(self):
215 """236 """
@@ -219,28 +240,13 @@
219 selected_name = self.menu.get_selected().get_name()240 selected_name = self.menu.get_selected().get_name()
220241
221 self.remove(self.menu)242 self.remove(self.menu)
222 self.create_main_menu()243 self._create_main_menu()
223244
224 index = self.menu.get_index(selected_name)245 index = self.menu.get_index(selected_name)
225 if not index == -1:246 if not index == -1:
226 self.menu.select(index)247 self.menu.selected_index = index
227248
228 self.menu.update()249 self._update_preview_area()
229
230 self.update_preview_area(self.menu.get_selected())
231
232 def update_preview_area(self, item):
233 """
234 Update preview area. This area displayes information of currently
235 selected menuitem.
236 @param item: Menuitem (clutter.Actor)
237 """
238 self.preview.remove_all()
239 if item.get_name() == "playing":
240 self.show_playing_preview()
241 #only show the rss menu if it has entries
242 elif item.get_name() == "rss":
243 self.show_rss_preview()
244250
245 def _move_menu(self, menu_direction):251 def _move_menu(self, menu_direction):
246 '''Move the menu in the given direction.'''252 '''Move the menu in the given direction.'''
@@ -258,39 +264,34 @@
258 if selected.get_name() == None:264 if selected.get_name() == None:
259 scroll_direction()265 scroll_direction()
260 selected = self.menu.get_selected()266 selected = self.menu.get_selected()
261 self.update_preview_area(selected)267 self._update_preview_area(selected)
262 else:268 else:
263 self.rss_preview_menu.move(preview_direction)269 self.rss_preview_menu.move(preview_direction)
264270
265 def _handle_up(self):271 def _handle_up(self):
266 '''Handle UserEvent.NAVIGATE_UP.'''272 '''Handle UserEvent.NAVIGATE_UP.'''
267 self._move_menu(self.UP)273 if self.menu.active:
274 self.menu.scroll_up()
275 else:
276 self.rss_preview_menu.move(TextMenu.UP)
268277
269 def _handle_down(self):278 def _handle_down(self):
270 '''Handle UserEvent.NAVIGATE_DOWN.'''279 '''Handle UserEvent.NAVIGATE_DOWN.'''
271 self._move_menu(self.DOWN)280 if self.menu.active:
281 self.menu.scroll_down()
282 else:
283 self.rss_preview_menu.move(TextMenu.DOWN)
272284
273 def _handle_left(self):285 def _handle_left(self):
274 '''Handle UserEvent.NAVIGATE_LEFT.'''286 '''Handle UserEvent.NAVIGATE_LEFT.'''
275 if self.feed_library.is_empty():287 if self._can_move_horizontally():
276 return288 self.preview_activation()
277
278 if self.active_menu == "main":
279 self.active_menu = "preview"
280 self.menu.set_active(False)
281 self.rss_preview_menu.set_active(True)
282289
283 def _handle_right(self):290 def _handle_right(self):
284 '''Handle UserEvent.NAVIGATE_RIGHT.'''291 '''Handle UserEvent.NAVIGATE_RIGHT.'''
285 if self.feed_library.is_empty():292 self.menu.active = True
286 return293
287294 def _handle_select(self, actor=None):
288 if self.active_menu == "preview":
289 self.active_menu = "main"
290 self.menu.set_active(True)
291 self.rss_preview_menu.set_active(False)
292
293 def _handle_select(self):
294 '''Handle UserEvent.NAVIGATE_SELECT.'''295 '''Handle UserEvent.NAVIGATE_SELECT.'''
295 item = self.menu.get_selected()296 item = self.menu.get_selected()
296297
@@ -316,3 +317,22 @@
316 kwargs = menu_item.get_userdata()317 kwargs = menu_item.get_userdata()
317 self.callback("entry", kwargs)318 self.callback("entry", kwargs)
318319
320 def _on_menu_moved(self, actor):
321 '''
322 We update the preview area when the selected item changed on the
323 Main menu.
324 '''
325 self._update_preview_area()
326
327 def main_menu_activation(self, actor=None):
328 '''Handle the main menu activation.'''
329 self.menu.active = True
330 if self.rss_preview_menu:
331 self.rss_preview_menu.set_active(False)
332
333 def preview_activation(self, actor=None):
334 '''Handle the preview activation.'''
335 self.menu.active = False
336 if self.rss_preview_menu:
337 self.rss_preview_menu.set_active(True)
338
319339
=== modified file 'entertainerlib/frontend/gui/user_interface.py'
--- entertainerlib/frontend/gui/user_interface.py 2009-03-22 21:00:43 +0000
+++ entertainerlib/frontend/gui/user_interface.py 2009-04-03 16:27:16 +0000
@@ -76,7 +76,10 @@
76 #problem76 #problem
77 clutter.set_use_mipmapped_text(False)77 clutter.set_use_mipmapped_text(False)
7878
79 self.hide_cursor_timeout_key = None
80
79 self.stage.connect('key-press-event', self.handle_keyboard_event)81 self.stage.connect('key-press-event', self.handle_keyboard_event)
82 self.stage.connect('motion-event', self.handle_motion_event)
80 self.stage.set_color(self.config.theme.get_color("background"))83 self.stage.set_color(self.config.theme.get_color("background"))
81 self.stage.set_size(84 self.stage.set_size(
82 self.config.get_stage_width(), self.config.get_stage_height())85 self.config.get_stage_width(), self.config.get_stage_height())
@@ -177,7 +180,6 @@
177180
178 def _fullscreen(self):181 def _fullscreen(self):
179 '''Set the window, stage, and config to fullscreen dimensions.'''182 '''Set the window, stage, and config to fullscreen dimensions.'''
180 self.stage.hide_cursor()
181 self.window.fullscreen()183 self.window.fullscreen()
182 self.stage.fullscreen()184 self.stage.fullscreen()
183 width, height = self.stage.get_size()185 width, height = self.stage.get_size()
@@ -204,6 +206,7 @@
204 def start_up(self):206 def start_up(self):
205 '''Start the user interface and make it visible.'''207 '''Start the user interface and make it visible.'''
206 self.show()208 self.show()
209 self.stage.hide_cursor()
207 self.current = self.create_screen("main")210 self.current = self.create_screen("main")
208 self.transition.forward_effect(None, self.current)211 self.transition.forward_effect(None, self.current)
209 self.enable_menu_overlay()212 self.enable_menu_overlay()
@@ -217,7 +220,6 @@
217 if self.is_fullscreen:220 if self.is_fullscreen:
218 self.stage.unfullscreen()221 self.stage.unfullscreen()
219 self.window.unfullscreen()222 self.window.unfullscreen()
220 self.stage.show_cursor()
221 self.config.set_stage_width(self.old_width)223 self.config.set_stage_width(self.old_width)
222 self.config.set_stage_height(self.old_height)224 self.config.set_stage_height(self.old_height)
223 self.is_fullscreen = False225 self.is_fullscreen = False
@@ -302,6 +304,22 @@
302 elif direction == Transition.BACKWARD:304 elif direction == Transition.BACKWARD:
303 self.transition.backward_effect(from_screen, screen)305 self.transition.backward_effect(from_screen, screen)
304306
307 def hide_cursor_timeout_callback(self):
308 '''Hide the cursor'''
309 self.stage.hide_cursor()
310 return True
311
312 def handle_motion_event(self, stage, clutter_event):
313 '''
314 Show the cursor if it has been moved and start a timeout to
315 hide it after a few seconds.
316 '''
317 self.stage.show_cursor()
318 if self.hide_cursor_timeout_key is not None:
319 gobject.source_remove(self.hide_cursor_timeout_key)
320 self.hide_cursor_timeout_key = gobject.timeout_add(3000,
321 self.hide_cursor_timeout_callback)
322
305 def handle_keyboard_event(self, stage, clutter_event, event_handler=None):323 def handle_keyboard_event(self, stage, clutter_event, event_handler=None):
306 '''Translate all received keyboard events to UserEvents.'''324 '''Translate all received keyboard events to UserEvents.'''
307 if event_handler is None:325 if event_handler is None:
308326
=== added file 'entertainerlib/frontend/gui/widgets/motion_buffer.py'
--- entertainerlib/frontend/gui/widgets/motion_buffer.py 1970-01-01 00:00:00 +0000
+++ entertainerlib/frontend/gui/widgets/motion_buffer.py 2009-04-03 20:23:10 +0000
@@ -0,0 +1,103 @@
1"""Tools for pointer motion calculations"""
2
3__licence__ = "GPLv2"
4__copyright__ = "2009, Samuel Buffet"
5__author__ = "Samuel Buffet <samuel.buffet@gmail.com>"
6
7import math
8
9class MotionBuffer:
10 """
11 This Class achieves some calculations on the pointer motion
12 """
13 def __init__ (self):
14 # public
15 self.dt_from_start = 0
16 self.dx_from_start = 0
17 self.dy_from_start = 0
18 self.distance_from_start = 0
19 self.speed_x_from_start = 0
20 self.speed_y_from_start = 0
21 self.speed_from_start = 0
22
23 self.dt_from_last_motion_event = 0
24 self.dx_from_last_motion_event = 0
25 self.dy_from_last_motion_event = 0
26 self.distance_from_last_motion_event = 0
27 self.speed_x_from_last_motion_event = 0
28 self.speed_y_from_last_motion_event = 0
29 self.speed_from_last_motion_event = 0
30
31 # private
32 self._start_x = 0
33 self._start_y = 0
34 self._start_time = 0
35
36 self._last_motion_event_x = 0
37 self._last_motion_event_y = 0
38 self._last_motion_event_time = 0
39
40 def start(self, event):
41 '''Has to be called when we start a pointer motion'''
42 self._start_x = event.x
43 self._start_y = event.y
44 self._start_time = event.time
45
46 self._last_motion_event_x = event.x
47 self._last_motion_event_y = event.y
48 self._last_motion_event_time = event.time
49
50 def take_new_motion_event(self, event):
51 '''
52 Has to be called by a method connected to the *motion-event* of an
53 actor
54 '''
55 self._last_motion_event_x = event.x
56 self._last_motion_event_y = event.y
57 self._last_motion_event_time = event.time
58
59 def compute_from_start(self, event):
60 '''compute deltas an speeds from the beginning of the motion'''
61 self.dt_from_start = event.time - self._start_time
62 self.dx_from_start = event.x - self._start_x
63 self.dy_from_start = event.y - self._start_y
64 self.distance_from_start = math.sqrt(self.dx_from_start * \
65 self.dx_from_start + self.dy_from_start * self.dy_from_start)
66
67 if self.dt_from_start != 0 :
68 self.speed_x_from_start = float(self.dx_from_start) / \
69 float(self.dt_from_start)
70 self.speed_y_from_start = float(self.dy_from_start) / \
71 float(self.dt_from_start)
72 self.speed_from_start = float(self.distance_from_start) / \
73 float(self.dt_from_start)
74 else:
75 self.speed_x_from_start = 0
76 self.speed_y_from_start = 0
77 self.speed_from_start = 0
78
79 def compute_from_last_motion_event(self, event):
80 '''compute deltas an speeds from the last motion-event'''
81 self.dt_from_last_motion_event = event.time - \
82 self._last_motion_event_time
83 self.dx_from_last_motion_event = event.x - self._last_motion_event_x
84 self.dy_from_last_motion_event = event.y - self._last_motion_event_y
85 self.distance_from_last_motion_event = math.sqrt( \
86 self.dx_from_last_motion_event * self.dx_from_last_motion_event + \
87 self.dy_from_last_motion_event * self.dy_from_last_motion_event)
88
89 if self.dt_from_last_motion_event != 0 :
90 self.speed_x_from_last_motion_event = \
91 float(self.dx_from_last_motion_event) / \
92 float(self.dt_from_last_motion_event)
93 self.speed_y_from_last_motion_event = \
94 float(self.dy_from_last_motion_event) / \
95 float(self.dt_from_last_motion_event)
96 self.speed_from_last_motion_event = \
97 float(self.distance_from_last_motion_event) / \
98 float(self.dt_from_last_motion_event)
99 else:
100 self.speed_x_from_last_motion_event = 0
101 self.speed_y_from_last_motion_event = 0
102 self.speed_from_last_motion_event = 0
103
0104
=== modified file 'entertainerlib/frontend/gui/widgets/scroll_menu.py'
--- entertainerlib/frontend/gui/widgets/scroll_menu.py 2009-03-02 02:27:27 +0000
+++ entertainerlib/frontend/gui/widgets/scroll_menu.py 2009-04-08 11:23:26 +0000
@@ -2,209 +2,271 @@
22
3__licence__ = "GPLv2"3__licence__ = "GPLv2"
4__copyright__ = "2007, Lauri Taimila"4__copyright__ = "2007, Lauri Taimila"
5__author__ = "Lauri Taimila <lauri@taimila.com>"5__author__ = ("Lauri Taimila <lauri@taimila.com>",
6 "Samuel Buffet <Samuel.Buffet@gmail.com>")
67
7import clutter8import clutter
89import math
9class ScrollMenu(clutter.Group):10import gobject
11
12from entertainerlib.frontend.gui.widgets.label import Label
13from entertainerlib.frontend.gui.widgets.motion_buffer import MotionBuffer
14
15class LoopedPathBehaviour (clutter.Behaviour):
16 """A custome Path behaviour driven by an index [0, 1]"""
17 __gtype_name__ = 'LoopedPathBehaviour'
18
19 def __init__(self, alpha=None):
20 clutter.Behaviour.__init__(self)
21 self.set_alpha(alpha)
22 self.start_index = 0.0
23 self.end_index = 1.0
24 self.start_knot = (0, 0)
25 self.end_knot = (10, 10)
26
27 def do_alpha_notify (self, alpha_value):
28 """
29 We position the actor on the Line defined by the 2 knots according to
30 the alpha value.
31 alpha = 0 position = start_knot
32 alpha = MAX_ALPHA position = end_knot
33 """
34 idx = alpha_value * (self.end_index - self.start_index)
35 idx /= clutter.MAX_ALPHA
36 idx += self.start_index
37
38 if idx >= 0 :
39 idxx = math.modf(idx)[0]
40 else:
41 idxx = 1 + math.modf(idx)[0]
42
43 # Calculation of new coordinates
44 x = idxx * (self.end_knot[0] - self.start_knot[0]) + self.start_knot[0]
45 y = idxx * (self.end_knot[1] - self.start_knot[1]) + self.start_knot[1]
46
47 for actor in self.get_actors():
48 actor.set_position(int(x), int(y))
49
50 @ property
51 def path_length(self):
52 """Calculation of the path's length in pixels'"""
53 dx = self.end_knot[0] - self.start_knot[0]
54 dy = self.end_knot[1] - self.start_knot[1]
55 return math.sqrt(dx * dx + dy * dy)
56
57class ScrollMenuItem(clutter.Group):
58 """
59 This is a Group containing a Label to which a LoopedPathBehaviour is applied
60 """
61 __gtype_name__ = 'ScrollMenuItem'
62
63 def __init__(self, alpha, text, item_height, font_size, color_name):
64 clutter.Group.__init__(self)
65
66 self.label = Label(font_size, color_name, 0, 0)
67 self.label.set_text(text)
68
69 self.behaviour = LoopedPathBehaviour(alpha)
70 self.behaviour.apply(self)
71
72 clutter.Group.add(self, self.label)
73
74class ScrollMenu(clutter.Group, object):
10 """75 """
11 Menu widget that contains text items. Menu can be scrolled up and down.76 Menu widget that contains text items. Menu can be scrolled up and down.
12
13 - Options: zoom, opacity, display N items, display arrows top+down
14 - add Clutter labels
15 - go around (how to implement this) (On state_change move items from
16 start->back)
17 """77 """
1878 __gtype_name__ = 'ScrollMenu'
19 DOWN = 079 __gsignals__ = {
20 UP = 180 'activated' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
2181 'selected' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
22 def __init__(self, item_gap, item_height, animated=False):82 'moved' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
23 """83 }
24 Initialize Menu84
25 @param item_gap: Gap between menuitems in pixels85 MODE_SELECTION = 0
26 """86 MODE_MOTION = 1
87 MODE_STOP = 2
88
89 def __init__(self, item_gap, item_height, font_size, color_name):
27 clutter.Group.__init__(self)90 clutter.Group.__init__(self)
28 self.__active = True91 self._motion_buffer = MotionBuffer()
29 self.__clip_size = -192 self._items = []
30 self.__items = []93 self._item_gap = item_gap
31 self.__gap = item_gap94 self._item_height = item_height
32 self.__item_height = item_height95 self._item_font_size = font_size
33 self.__animated = animated96 self._item_color_name = color_name
3497 self._selected_index = 1
35 self.scroll_timeline = clutter.Timeline(7, 26)98 self._visible_nb = 5
36 self.scroll_timeline.connect('new-frame', self.scroll_items_cb)99 self._event_mode = -1
37100 self._animation_progression = 0
38 def add(self, actor):101 self._animation_start_index = 1
39 """102 self._animation_end_index = 1
40 Add new menuitem to the menu.103 self._active = False
41 @param item: Label104 self._motion_handler = 0
42 """105
43 clutter.Group.add(self, actor)106 self._timeline = clutter.Timeline(fps=200, duration=300)
44 actor.connect('notify::y', self.actor_notify_y)107 self._alpha = clutter.Alpha(self._timeline, clutter.sine_inc_func)
45108
46 y = ((len(self.__items) * self.__item_height) +109 # preparation to pointer events handling
47 (self.__gap * len(self.__items)))110 self.set_reactive(True)
48111 self.connect('scroll-event', self._on_scroll_event)
49 if y == 0 :112 self.connect('button-press-event', self._on_button_press_event)
50 # need to do that to update opacity of the first item added113 self.connect('button-release-event', self._on_button_release_event)
51 self.actor_notify_y(actor, None)114
52 actor.set_position(0, y)115 def add_item(self, text, name):
53116 """Creation of a new MenuItem and addition to the ScrollMenu"""
54 self.__items.append(actor)117 item = ScrollMenuItem(self._alpha, text, self._item_height,
55118 self._item_font_size, self._item_color_name)
56 def remove(self, actor):119
57 """120 item.set_name(name)
58 Remove actor from the menu.121 item.connect('notify::y', self._on_item_notify_y)
59 """122 clutter.Group.add(self, item)
60 clutter.Group.add(self, actor)123
61 for item in self.__items:124 self._items.append(item)
62 if item is actor:125
63 del item126 def _get_active(self):
64
65 def remove_all(self):
66 """
67 Remove all actors from the menu.
68 """
69 del self.__items
70 self.__items = []
71
72 def get_index(self, text):
73 """
74 Returns index of label with the text as passed, returns -1 if not found
75 @author Joshua Scotton
76 """
77 for item in self.__items:
78 if item.get_name() == text:
79 return self.__items.index(item)
80 return -1
81
82 def select(self, index):
83 """
84 Selects the item at the passed index, does not support animations
85 @author Joshua Scotton
86 """
87 #split list so that item at the index will now be at position 2
88 #which is the selected index in a scroll menu
89 items = self.__items[index-2:] + self.__items[0:index-2]
90
91 #Basically remove all then add in the correct order
92 self.remove_all()
93 for item in items:
94 #Remove all doesn't remove the parent object. So in order to avoid
95 #adding two parent clutter items to the item we need to remove the
96 #old one first
97 clutter.Group.remove(self, item)
98 self.add(item)
99
100 def get_number_of_items(self):
101 """Get number of menuitems in this Menu"""
102 return len(self.__items)
103
104 def set_number_of_visible_items(self, number):
105 """
106 Set how many menuitems are displayed on the screen at the same time.
107 @param number: Number of items
108 """
109 self.__clip_size = number
110 self.set_clip(0,
111 0,
112 self.get_width(),
113 (self.__item_height + self.__gap) * number)
114
115 def get_number_of_visible_items(self):
116 """
117 Get number of menuitems that are displayed on the screen at the same
118 time.
119
120 @return: Ingeter
121 """
122 return self.__clip_size
123
124 def get_selected(self):
125 """
126 Get currently selected menuitem
127 @return Clutter actor object
128 """
129 return self.__items[2]
130
131 def scroll_down(self):
132 """Scroll menu down by one menuitem."""
133 if not self.scroll_timeline.is_playing():
134 if self.__animated:
135 self.__scroll_direction = self.DOWN
136 self.scroll_timeline.start()
137 else:
138 for item in self.__items:
139 item.move_by(0, self.__item_height + self.__gap)
140
141 # Set last item to first (menu scrolls)
142 last = self.__items[-1]
143 last.set_position(
144 0, self.__items[0].get_y() - self.__gap - self.__item_height)
145 self.__items = [last] + self.__items
146 del self.__items[-1]
147
148 def scroll_up(self):
149 """Scroll menu up by one menuitem."""
150 if not self.scroll_timeline.is_playing():
151 if self.__animated:
152 self.__scroll_direction = self.UP
153 self.scroll_timeline.start()
154 else:
155 for item in self.__items:
156 item.move_by(0, -(self.__item_height + self.__gap))
157
158 # Set first item to last (menu scrolls)
159 first = self.__items[0]
160 first.set_position(
161 0, self.__items[-1].get_y() + self.__gap + self.__item_height)
162 self.__items.append(first)
163 del self.__items[0]
164
165 def scroll_items_cb(self, timeline, frame_num):
166 """Callback function of scroll_down_timeline"""
167 delta = timeline.get_delta()[0]
168 offset = int((self.__item_height + self.__gap) / 7 * delta)
169 if self.__scroll_direction == 1:
170 offset = - offset
171 for item in self.__items:
172 item.move_by(0, offset)
173
174 def is_active(self):
175 """127 """
176 Is this menu active.128 Is this menu active.
177 @return boolean, True if menu is currently active, otherwise False129 @return boolean, True if menu is currently active, otherwise False
178 """130 """
179 return self.__active131 return self._active
180132
181 def set_active(self, boolean):133 def _set_active(self, boolean):
182 """134 """
183 Set this widget active or inactive135 Set this widget active or inactive
184 @param boolean: True to set widget active, False to set inactive136 @param boolean: True to set widget active, False to set inactive
185 """137 """
186 self.__active = boolean138 if self._active == boolean:
139 return
140
141 self._active = boolean
187 if boolean:142 if boolean:
188 #if self.__selector is not None:
189 # self.__selector.show()
190 self.set_opacity(255)143 self.set_opacity(255)
144 self.emit('activated')
191 else:145 else:
192 #if self.__selector is not None:
193 # self.__selector.hide()
194 self.set_opacity(128)146 self.set_opacity(128)
195147
196 def update(self):148 active = property(_get_active, _set_active)
197 """Update the whole menu"""149
198 for item in self.__items:150 def _update_behaviours(self, target):
199 opacity = self.get_opacity_for_y(item.get_y())151 """
200 item.set_opacity(opacity)152 Preparation of looped behaviours applied to menu items before
153 we move them.
154 """
155 nbi = len(self._items)
156 step = 1.0 / nbi
157 step_pix = self._item_gap + self._item_height
158 a = int(self._visible_nb / 2) + 1
159 for x, item in enumerate(self._items):
160 item.behaviour.start_index = (x + a - self._selected_index) * step
161 item.behaviour.end_index = (x + a - target) * step
162
163 item.behaviour.start_knot = (0.0, -step_pix)
164 item.behaviour.end_knot = (0.0, (nbi - 1.0) * step_pix)
165
166 def _display_items_at_target(self, target):
167 """
168 This method draws the menu for a particular targeted value of the
169 driving index.
170 """
171 nbi = len(self._items)
172 step = 1.0 / nbi
173
174 a = int(self._visible_nb / 2) + 1
175 for x, item in enumerate(self._items):
176 index = (x + a - target) * step
177
178 if index >= 0 :
179 idxx = math.modf(index)[0]
180 else:
181 idxx = 1 + math.modf(index)[0]
182
183 # Calculation of new coordinates
184 xx = idxx * (item.behaviour.end_knot[0] - \
185 item.behaviour.start_knot[0]) + item.behaviour.start_knot[0]
186 yy = idxx * (item.behaviour.end_knot[1] - \
187 item.behaviour.start_knot[1]) + item.behaviour.start_knot[1]
188
189 item.set_position(int(xx), int(yy))
190
191 def _get_visible_nb(self):
192 """Return the visible_nb property"""
193 return self._visible_nb
194
195 def _set_visible_nb(self, visible_nb):
196 """Set the visible_nb property"""
197 self._visible_nb = visible_nb
198 h = visible_nb * self._item_height + (visible_nb - 1) * self._item_gap
199 self.set_clip(0, 0, self.get_width(), h)
200
201 visible_nb = property(_get_visible_nb, _set_visible_nb)
202
203 def get_selected(self):
204 """
205 Get currently selected menuitem
206 @return Clutter actor object
207 """
208 return self._items[int(self._selected_index)]
209
210 def _get_selected_index(self):
211 """Return the selected_index property"""
212 return self._selected_index
213
214 def _set_selected_index(self, selected_index, duration=300):
215 """Set the selected_index property"""
216 if not self._timeline.is_playing():
217 nb = len(self._items)
218 self._update_behaviours(selected_index)
219
220 # those 2 variables are used if we want to stop the timeline
221 # we use them + timeline progression to calculate the current index
222 # when (if) we stop
223 self._animation_start_index = self._selected_index
224 self._animation_end_index = selected_index
225
226 # compute selected_index to be between 0 and nb
227 if selected_index >= 0:
228 self._selected_index = selected_index - \
229 math.modf(selected_index / nb)[1] * nb
230 else:
231 self._selected_index = selected_index + \
232 (math.modf(-(selected_index + 1) / nb)[1] + 1) * nb
233
234 self._timeline.set_duration(duration)
235 self._timeline.start()
236
237 self.emit('moved')
238
239 selected_index = property(_get_selected_index, _set_selected_index)
240
241 def get_index(self, text):
242 """
243 Returns index of label with the text as passed, returns -1 if not found
244 @author Joshua Scotton
245 """
246 for item in self._items:
247 if item.get_name() == text:
248 return self._items.index(item)
249 return -1
250
251 def scroll_by(self, nb, duration=300):
252 """Set the selected_index property"""
253 self._set_selected_index(self._selected_index + nb, duration)
254
255 def scroll_up(self, duration=300):
256 """Set the selected_index property"""
257 self.scroll_by(-1, duration)
258
259 def scroll_down(self, duration=300):
260 """Set the selected_index property"""
261 self.scroll_by(1, duration)
201262
202 def get_opacity_for_y(self, y):263 def get_opacity_for_y(self, y):
203 """Calculation of actor's opacity as a function of its y coordinate"""264 """Calculation of actor's opacity as a function of its y coordinates"""
204 opacity_first_item = 30265 opacity_first_item = 40
205 opacity_selected_item = 255266 opacity_selected_item = 255
267 o = int(self._visible_nb / 2)
206268
207 y_medium_item = 2 * (self.__item_height + self.__gap)269 y_medium_item = o * (self._item_height + self._item_gap)
208 a = float(opacity_selected_item - opacity_first_item)270 a = float(opacity_selected_item - opacity_first_item)
209 a /= float(y_medium_item)271 a /= float(y_medium_item)
210 if y <= y_medium_item :272 if y <= y_medium_item :
@@ -217,8 +279,116 @@
217279
218 return int(opacity)280 return int(opacity)
219281
220 def actor_notify_y(self, actor, stage):282 def _on_item_notify_y(self, item, stage):
221 """Set opacity to actors when they are moving. Opacity is f(y)"""283 """Set opacity to actors when they are moving. Opacity is f(y)"""
222 opacity = self.get_opacity_for_y(actor.get_y())284 opacity = self.get_opacity_for_y(item.get_y())
223 actor.set_opacity(opacity)285 item.set_opacity(opacity)
286
287 def _on_button_press_event(self, actor, event):
288 """button-press-event handler"""
289 clutter.grab_pointer(self)
290 if not self.handler_is_connected(self._motion_handler):
291 self._motion_handler = self.connect('motion-event',
292 self._on_motion_event)
293 self.active = True
294
295 if self._timeline.is_playing():
296 # before we stop the timeline, store its progression
297 self._animation_progression = self._timeline.get_progress()
298
299 # A click with an animation pending should stop the animation
300 self._timeline.stop()
301
302 # go to MODE_STOP to handle correctly next button-release event
303 self._event_mode = self.MODE_STOP
304 else:
305 # no animation pending so we're going to do either a menu_item
306 # selection or a menu motion. This will be decided later, right now
307 # we just take a snapshot of this button-press-event as a start.
308 self._motion_buffer.start(event)
309 self._event_mode = self.MODE_SELECTION
310
311 return False
312
313 def _on_button_release_event(self, actor, event):
314 """button-release-event handler"""
315 clutter.ungrab_pointer()
316 if self.handler_is_connected(self._motion_handler):
317 self.disconnect_by_func(self._on_motion_event)
318 self._motion_buffer.compute_from_last_motion_event(event)
319
320 y = event.y - self.get_y()
321
322 if self._event_mode == self.MODE_SELECTION:
323 # if we are in MODE_SELECTION it means that we want to select
324 # the menu item bellow the pointer
325
326 for idx, item in enumerate(self._items):
327 b = item.get_y()
328 h = item.get_height()
329 if (y >= b) and (y <= (b + h)):
330 delta1 = idx - self._selected_index
331 delta2 = idx - self._selected_index + len(self._items)
332 delta3 = idx - self._selected_index - len(self._items)
333
334 delta = 99999
335 for i in [delta1, delta2, delta3]:
336 if math.fabs(i)<math.fabs(delta):
337 delta = i
338
339 self.scroll_by(delta)
340
341 # if delta = 0 it means we've clicked on the selected item
342 if delta == 0:
343 self.emit('selected')
344
345 elif self._event_mode == self.MODE_MOTION:
346 speed = self._motion_buffer.speed_y_from_last_motion_event
347 target = self._selected_index - \
348 self._motion_buffer.dy_from_start / \
349 self._items[0].behaviour.path_length * len(self._items)
350
351 new_index = int(target - 5 * speed)
352 self._selected_index = target
353 self._set_selected_index(new_index, 1000)
354
355 else:
356 # If we have stopped the pending animation. Now we have to do
357 # a small other one to select the closest menu-item
358 current_index = self._animation_start_index + \
359 (self._animation_end_index - self._animation_start_index) * \
360 self._animation_progression
361 self._selected_index = current_index
362 target_index = int(current_index)
363 self._set_selected_index(target_index, 1000)
364
365 return False
366
367 def _on_motion_event(self, actor, event):
368 """motion-event handler"""
369 self._motion_buffer.compute_from_start(event)
370 if self._motion_buffer.distance_from_start > 10:
371 self._motion_buffer.take_new_motion_event(event)
372 self._event_mode = self.MODE_MOTION
373 target = self._selected_index - \
374 self._motion_buffer.dy_from_start / \
375 self._items[0].behaviour.path_length * len(self._items)
376 self._display_items_at_target(target)
377
378 self.get_stage().show_cursor()
379
380 return False
381
382 def _on_scroll_event(self, actor, event):
383 """scroll-event handler (mouse's wheel)"""
384 self.active = True
385
386 if event.direction == clutter.SCROLL_DOWN:
387 self.scroll_down(150)
388 else:
389 self.scroll_up(150)
390
391 self.get_stage().show_cursor()
392
393 return False
224394
225395
=== added file 'entertainerlib/tests/test_scrollmenu.py'
--- entertainerlib/tests/test_scrollmenu.py 1970-01-01 00:00:00 +0000
+++ entertainerlib/tests/test_scrollmenu.py 2009-04-07 11:31:48 +0000
@@ -0,0 +1,36 @@
1"""Tests ScrollMenu"""
2
3__license__ = "GPLv2"
4__copyright__ = "2009, Samuel Buffet"
5__author__ = "Samuel Buffet <samuel.buffet@gmail.com>"
6
7import clutter
8
9from entertainerlib.frontend.gui.widgets.scroll_menu import ScrollMenu
10from entertainerlib.tests import EntertainerTest
11
12class ScrollMenuTest(EntertainerTest):
13 """Test for entertainerlib.frontend.gui.widgets.scroll_menu"""
14
15 def setUp(self):
16 """Set up the test"""
17 EntertainerTest.setUp(self)
18
19
20 self.menu = ScrollMenu(10, 60, 0.045, "menuitem_active")
21 self.menu.set_name("mainmenu")
22
23 self.menu.add_item(_("Play CD"), "disc")
24 self.menu.add_item(_("Videos"), "videos")
25 self.menu.add_item(_("Music"), "music")
26 self.menu.add_item(_("Photographs"), "photo")
27 self.menu.add_item(_("Headlines"), "rss")
28
29 def tearDown(self):
30 """Clean up after the test"""
31 EntertainerTest.tearDown(self)
32
33 def testCreate(self):
34 """Test correct ScrollArea initialization"""
35 self.assertTrue(isinstance(self.menu, (clutter.Group, object)))
36

Subscribers

People subscribed via source and target branches