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
1=== modified file 'entertainerlib/frontend/gui/screens/main.py'
2--- entertainerlib/frontend/gui/screens/main.py 2009-03-02 03:11:24 +0000
3+++ entertainerlib/frontend/gui/screens/main.py 2009-04-08 11:23:26 +0000
4@@ -44,65 +44,89 @@
5 self.preview = clutter.Group() # Group that contains preview actors
6 self.preview.set_position(self.get_abs_x(0.07), self.get_abs_y(0.1))
7 self.preview.show()
8+ self.preview.set_opacity(0x00)
9 self.add(self.preview)
10
11- self.active_menu = "main"
12- self.rss_preview_menu = None # Pointer to the current preview menu
13-
14- self.menu = None
15- self.playing_item = None
16- self.create_main_menu()
17-
18- clock = ClockLabel(0.13, "screentitle", 0, 0.87)
19- self.add(clock)
20-
21- def create_main_menu(self):
22+
23+ self.rss_preview_menu = self._create_rss_preview_menu()
24+ self.preview.add(self.rss_preview_menu)
25+
26+ self._create_main_menu()
27+ self.menu.active = True
28+
29+ self.add(ClockLabel(0.13, "screentitle", 0, 0.87))
30+
31+ def get_type(self):
32+ """Return screen type."""
33+ return Screen.NORMAL
34+
35+ def get_name(self):
36+ """Return screen name (human readble)"""
37+ return "Main"
38+
39+ def _create_main_menu(self):
40 """Create main menu of the home screen"""
41- self.menu = ScrollMenu(10, 60, self.config.show_effects())
42+ self.menu = ScrollMenu(10, 60, 0.045, "menuitem_active")
43 self.menu.set_name("mainmenu")
44-
45- # Common values for all ScrollMenu items
46- size = 0.045
47- color = "menuitem_active"
48-
49- item0 = Label(size, color, 0, 0, _("Play CD"), "disc")
50- self.menu.add(item0)
51-
52- item2 = Label(size, color, 0, 0, _("Videos"), "videos")
53- self.menu.add(item2)
54-
55- item3 = Label(size, color, 0, 0, _("Music"), "music")
56- self.menu.add(item3)
57-
58- item4 = Label(size, color, 0, 0, _("Photographs"), "photo")
59- self.menu.add(item4)
60-
61- item5 = Label(size, color, 0, 0, _("Headlines"), "rss")
62- self.menu.add(item5)
63+ self.menu.connect('selected', self._handle_select)
64+ self.menu.connect('moved', self._on_menu_moved)
65+ self.menu.connect('activated', self.main_menu_activation)
66+
67+ self.menu.add_item(_("Play CD"), "disc")
68+ self.menu.add_item(_("Videos"), "videos")
69+ self.menu.add_item(_("Music"), "music")
70+ self.menu.add_item(_("Photographs"), "photo")
71+ self.menu.add_item(_("Headlines"), "rss")
72
73 if self.config.display_weather_in_frontend():
74- item6 = Label(size, color, 0, 0, _("Weather"), "weather")
75- self.menu.add(item6)
76+ self.menu.add_item(_("Weather"), "weather")
77
78 if self.config.display_cd_eject_in_frontend():
79- item7 = Label(size, color, 0, 0, _("Eject CD"), "eject_cd")
80- self.menu.add(item7)
81+ self.menu.add_item(_("Eject CD"), "eject_cd")
82
83 if self.media_player.has_media():
84- playing = Label(size, color, 0, 0, _("Playing now..."), "playing")
85- self.menu.add(playing)
86-
87- self.menu.set_number_of_visible_items(5)
88- menu_clip = self.menu.get_number_of_visible_items() * 70
89-
90+ self.menu.add_item(_("Playing now..."), "playing")
91+
92+ self.menu.visible_nb = 5
93+ self.menu.selected_index = 2
94+
95+ menu_clip = self.menu.visible_nb * 70
96 # Menu position
97 menu_y = int((self.config.get_stage_height() - menu_clip + 10) / 2)
98 self.menu.set_position(self.get_abs_x(0.75), menu_y)
99
100 self.add(self.menu)
101
102- def show_playing_preview(self):
103- """Create a group that displays information on current media."""
104+ def _create_rss_preview_menu(self):
105+ '''Create the RSS preview menu that will show feed highlights. An
106+ uninitialized menu will be returned to prevent move errors if there
107+ is nothing in the feed library.'''
108+ menu = TextMenu(self.theme, self.config.show_effects())
109+
110+ if self.feed_library.is_empty() is False:
111+ menu.set_row_count(1)
112+ menu.set_position(self.get_abs_x(0.035), self.get_abs_y(0.12))
113+ menu.set_item_size(self.get_abs_x(0.549), self.get_abs_y(0.078))
114+
115+ # List of latest entries. pack = (Feed object, Entry object)
116+ entries = self.feed_library.get_latest_entries(5)
117+ for pack in entries:
118+ text = pack[0].get_title() + " - " + pack[1].get_title()
119+ item = TextMenuItem(0.549, 0.078,
120+ FeedEntryParser().strip_tags(text),
121+ pack[1].get_date())
122+ kwargs = { 'feed' : pack[0], 'entry' : pack[1] }
123+ item.set_userdata(kwargs)
124+ menu.add_actor(item)
125+
126+ menu.set_active(False)
127+
128+ return menu
129+
130+ def _create_playing_preview(self):
131+ '''Create the Now Playing preview sidebar.'''
132+ preview = clutter.Group()
133+
134 # Video preview of current media
135 video_texture = self.media_player.get_texture()
136 if video_texture == None:
137@@ -141,75 +165,72 @@
138 rect.set_size(int(new_width + 6), int(new_height + 6))
139 rect.set_position(rect_x, rect_y)
140 rect.set_color((128, 128, 128, 192))
141- self.preview.add(rect)
142+ preview.add(rect)
143
144- self.preview.add(video_texture)
145+ preview.add(video_texture)
146
147 title_text = _("Now Playing: %(title)s") % \
148 {'title': self.media_player.get_media_title()}
149 title = Label(0.03, "text", 0.03, 0.74, title_text)
150- self.preview.add(title)
151-
152- def show_rss_preview(self):
153- """Rss preview"""
154+ preview.add(title)
155+
156+ return preview
157+
158+ def _create_rss_preview(self):
159+ '''Create the RSS preview sidebar.'''
160+ preview = clutter.Group()
161+
162+ # RSS Icon
163+ icon = Texture(self.theme.getImage("rss_icon"), 0, 0.04)
164+ icon.set_scale(0.6, 0.6)
165+ preview.add(icon)
166+
167+ # RSS Feed Title
168+ title = Label(0.075, "title", 0.045, 0.03, _("Recent headlines"))
169+ preview.add(title)
170
171 if self.feed_library.is_empty() is False:
172- # RSS Icon
173- icon = Texture(self.theme.getImage("rss_icon"), 0, 0.04)
174- icon.set_scale(0.6, 0.6)
175-
176- # RSS Feed Title
177- title = Label(0.075, "title", 0.045, 0.03, _("Recent headlines"))
178-
179- menu = TextMenu(self.theme, self.config.show_effects())
180- menu.set_row_count(1)
181- menu.set_position(self.get_abs_x(0.035), self.get_abs_y(0.12))
182- menu.set_item_size(self.get_abs_x(0.549), self.get_abs_y(0.078))
183-
184- # List of latest entries. pack = (Feed object, Entry object)
185- entries = self.feed_library.get_latest_entries(5)
186- for pack in entries:
187- text = pack[0].get_title() + " - " + pack[1].get_title()
188- item = TextMenuItem(0.549, 0.078,
189- FeedEntryParser().strip_tags(text),
190- pack[1].get_date())
191- kwargs = { 'feed' : pack[0], 'entry' : pack[1] }
192- item.set_userdata(kwargs)
193- menu.add_actor(item)
194-
195- menu.set_active(False)
196-
197- # Fade in timeline
198+ menu = self._create_rss_preview_menu()
199+ preview.add(menu)
200+ self.rss_preview_menu = menu
201+ else:
202+ # No headlines available in the library
203+ info = Label(0.05, "title", 0.1, 0.35, _("No headlines available"))
204+ preview.add(info)
205+
206+ return preview
207+
208+ def _update_preview_area(self):
209+ '''Update the preview area to display the current menu item.'''
210+ self.preview.remove_all()
211+ item = self.menu.get_selected()
212+
213+ self.preview.set_opacity(0x00)
214+
215+ update = True
216+
217+ if item.get_name() == "playing":
218+ self.preview.add(self._create_playing_preview())
219+ elif item.get_name() == "rss":
220+ self.preview.add(self._create_rss_preview())
221+ else:
222+ update = False
223+
224+ # If the preview was updated fade it in
225+ if update:
226 fade_in = clutter.Timeline(20, 60)
227 alpha_in = clutter.Alpha(fade_in, clutter.smoothstep_inc_func)
228- self.in_behaviour = clutter.BehaviourOpacity(0x00, 0xff, alpha_in)
229- self.in_behaviour.apply(menu)
230- self.in_behaviour.apply(title)
231- self.in_behaviour.apply(icon)
232-
233- self.preview.add(icon)
234- self.preview.add(title)
235- self.rss_preview_menu = menu
236- self.preview.add(menu)
237-
238- menu.set_opacity(0x00)
239- icon.set_opacity(0x00)
240- title.set_opacity(0x00)
241-
242+ self.behaviour = clutter.BehaviourOpacity(0x00, 0xff, alpha_in)
243+ self.behaviour.apply(self.preview)
244 fade_in.start()
245
246+ def _can_move_horizontally(self):
247+ '''Return a boolean indicating if horizontal movement is allowed.'''
248+ item = self.menu.get_selected()
249+ if self.feed_library.is_empty() or item.get_name() != 'rss':
250+ return False
251 else:
252- # No headlines available in the library
253- info = Label(0.07, "title", 0.1, 0.35, _("No headlines available"))
254- self.preview.add(info)
255-
256- def get_type(self):
257- """Return screen type."""
258- return Screen.NORMAL
259-
260- def get_name(self):
261- """Return screen name (human readble)"""
262- return "Main"
263+ return True
264
265 def update(self):
266 """
267@@ -219,28 +240,13 @@
268 selected_name = self.menu.get_selected().get_name()
269
270 self.remove(self.menu)
271- self.create_main_menu()
272+ self._create_main_menu()
273
274 index = self.menu.get_index(selected_name)
275 if not index == -1:
276- self.menu.select(index)
277-
278- self.menu.update()
279-
280- self.update_preview_area(self.menu.get_selected())
281-
282- def update_preview_area(self, item):
283- """
284- Update preview area. This area displayes information of currently
285- selected menuitem.
286- @param item: Menuitem (clutter.Actor)
287- """
288- self.preview.remove_all()
289- if item.get_name() == "playing":
290- self.show_playing_preview()
291- #only show the rss menu if it has entries
292- elif item.get_name() == "rss":
293- self.show_rss_preview()
294+ self.menu.selected_index = index
295+
296+ self._update_preview_area()
297
298 def _move_menu(self, menu_direction):
299 '''Move the menu in the given direction.'''
300@@ -258,39 +264,34 @@
301 if selected.get_name() == None:
302 scroll_direction()
303 selected = self.menu.get_selected()
304- self.update_preview_area(selected)
305+ self._update_preview_area(selected)
306 else:
307 self.rss_preview_menu.move(preview_direction)
308
309 def _handle_up(self):
310 '''Handle UserEvent.NAVIGATE_UP.'''
311- self._move_menu(self.UP)
312+ if self.menu.active:
313+ self.menu.scroll_up()
314+ else:
315+ self.rss_preview_menu.move(TextMenu.UP)
316
317 def _handle_down(self):
318 '''Handle UserEvent.NAVIGATE_DOWN.'''
319- self._move_menu(self.DOWN)
320+ if self.menu.active:
321+ self.menu.scroll_down()
322+ else:
323+ self.rss_preview_menu.move(TextMenu.DOWN)
324
325 def _handle_left(self):
326 '''Handle UserEvent.NAVIGATE_LEFT.'''
327- if self.feed_library.is_empty():
328- return
329-
330- if self.active_menu == "main":
331- self.active_menu = "preview"
332- self.menu.set_active(False)
333- self.rss_preview_menu.set_active(True)
334+ if self._can_move_horizontally():
335+ self.preview_activation()
336
337 def _handle_right(self):
338 '''Handle UserEvent.NAVIGATE_RIGHT.'''
339- if self.feed_library.is_empty():
340- return
341-
342- if self.active_menu == "preview":
343- self.active_menu = "main"
344- self.menu.set_active(True)
345- self.rss_preview_menu.set_active(False)
346-
347- def _handle_select(self):
348+ self.menu.active = True
349+
350+ def _handle_select(self, actor=None):
351 '''Handle UserEvent.NAVIGATE_SELECT.'''
352 item = self.menu.get_selected()
353
354@@ -316,3 +317,22 @@
355 kwargs = menu_item.get_userdata()
356 self.callback("entry", kwargs)
357
358+ def _on_menu_moved(self, actor):
359+ '''
360+ We update the preview area when the selected item changed on the
361+ Main menu.
362+ '''
363+ self._update_preview_area()
364+
365+ def main_menu_activation(self, actor=None):
366+ '''Handle the main menu activation.'''
367+ self.menu.active = True
368+ if self.rss_preview_menu:
369+ self.rss_preview_menu.set_active(False)
370+
371+ def preview_activation(self, actor=None):
372+ '''Handle the preview activation.'''
373+ self.menu.active = False
374+ if self.rss_preview_menu:
375+ self.rss_preview_menu.set_active(True)
376+
377
378=== modified file 'entertainerlib/frontend/gui/user_interface.py'
379--- entertainerlib/frontend/gui/user_interface.py 2009-03-22 21:00:43 +0000
380+++ entertainerlib/frontend/gui/user_interface.py 2009-04-03 16:27:16 +0000
381@@ -76,7 +76,10 @@
382 #problem
383 clutter.set_use_mipmapped_text(False)
384
385+ self.hide_cursor_timeout_key = None
386+
387 self.stage.connect('key-press-event', self.handle_keyboard_event)
388+ self.stage.connect('motion-event', self.handle_motion_event)
389 self.stage.set_color(self.config.theme.get_color("background"))
390 self.stage.set_size(
391 self.config.get_stage_width(), self.config.get_stage_height())
392@@ -177,7 +180,6 @@
393
394 def _fullscreen(self):
395 '''Set the window, stage, and config to fullscreen dimensions.'''
396- self.stage.hide_cursor()
397 self.window.fullscreen()
398 self.stage.fullscreen()
399 width, height = self.stage.get_size()
400@@ -204,6 +206,7 @@
401 def start_up(self):
402 '''Start the user interface and make it visible.'''
403 self.show()
404+ self.stage.hide_cursor()
405 self.current = self.create_screen("main")
406 self.transition.forward_effect(None, self.current)
407 self.enable_menu_overlay()
408@@ -217,7 +220,6 @@
409 if self.is_fullscreen:
410 self.stage.unfullscreen()
411 self.window.unfullscreen()
412- self.stage.show_cursor()
413 self.config.set_stage_width(self.old_width)
414 self.config.set_stage_height(self.old_height)
415 self.is_fullscreen = False
416@@ -302,6 +304,22 @@
417 elif direction == Transition.BACKWARD:
418 self.transition.backward_effect(from_screen, screen)
419
420+ def hide_cursor_timeout_callback(self):
421+ '''Hide the cursor'''
422+ self.stage.hide_cursor()
423+ return True
424+
425+ def handle_motion_event(self, stage, clutter_event):
426+ '''
427+ Show the cursor if it has been moved and start a timeout to
428+ hide it after a few seconds.
429+ '''
430+ self.stage.show_cursor()
431+ if self.hide_cursor_timeout_key is not None:
432+ gobject.source_remove(self.hide_cursor_timeout_key)
433+ self.hide_cursor_timeout_key = gobject.timeout_add(3000,
434+ self.hide_cursor_timeout_callback)
435+
436 def handle_keyboard_event(self, stage, clutter_event, event_handler=None):
437 '''Translate all received keyboard events to UserEvents.'''
438 if event_handler is None:
439
440=== added file 'entertainerlib/frontend/gui/widgets/motion_buffer.py'
441--- entertainerlib/frontend/gui/widgets/motion_buffer.py 1970-01-01 00:00:00 +0000
442+++ entertainerlib/frontend/gui/widgets/motion_buffer.py 2009-04-03 20:23:10 +0000
443@@ -0,0 +1,103 @@
444+"""Tools for pointer motion calculations"""
445+
446+__licence__ = "GPLv2"
447+__copyright__ = "2009, Samuel Buffet"
448+__author__ = "Samuel Buffet <samuel.buffet@gmail.com>"
449+
450+import math
451+
452+class MotionBuffer:
453+ """
454+ This Class achieves some calculations on the pointer motion
455+ """
456+ def __init__ (self):
457+ # public
458+ self.dt_from_start = 0
459+ self.dx_from_start = 0
460+ self.dy_from_start = 0
461+ self.distance_from_start = 0
462+ self.speed_x_from_start = 0
463+ self.speed_y_from_start = 0
464+ self.speed_from_start = 0
465+
466+ self.dt_from_last_motion_event = 0
467+ self.dx_from_last_motion_event = 0
468+ self.dy_from_last_motion_event = 0
469+ self.distance_from_last_motion_event = 0
470+ self.speed_x_from_last_motion_event = 0
471+ self.speed_y_from_last_motion_event = 0
472+ self.speed_from_last_motion_event = 0
473+
474+ # private
475+ self._start_x = 0
476+ self._start_y = 0
477+ self._start_time = 0
478+
479+ self._last_motion_event_x = 0
480+ self._last_motion_event_y = 0
481+ self._last_motion_event_time = 0
482+
483+ def start(self, event):
484+ '''Has to be called when we start a pointer motion'''
485+ self._start_x = event.x
486+ self._start_y = event.y
487+ self._start_time = event.time
488+
489+ self._last_motion_event_x = event.x
490+ self._last_motion_event_y = event.y
491+ self._last_motion_event_time = event.time
492+
493+ def take_new_motion_event(self, event):
494+ '''
495+ Has to be called by a method connected to the *motion-event* of an
496+ actor
497+ '''
498+ self._last_motion_event_x = event.x
499+ self._last_motion_event_y = event.y
500+ self._last_motion_event_time = event.time
501+
502+ def compute_from_start(self, event):
503+ '''compute deltas an speeds from the beginning of the motion'''
504+ self.dt_from_start = event.time - self._start_time
505+ self.dx_from_start = event.x - self._start_x
506+ self.dy_from_start = event.y - self._start_y
507+ self.distance_from_start = math.sqrt(self.dx_from_start * \
508+ self.dx_from_start + self.dy_from_start * self.dy_from_start)
509+
510+ if self.dt_from_start != 0 :
511+ self.speed_x_from_start = float(self.dx_from_start) / \
512+ float(self.dt_from_start)
513+ self.speed_y_from_start = float(self.dy_from_start) / \
514+ float(self.dt_from_start)
515+ self.speed_from_start = float(self.distance_from_start) / \
516+ float(self.dt_from_start)
517+ else:
518+ self.speed_x_from_start = 0
519+ self.speed_y_from_start = 0
520+ self.speed_from_start = 0
521+
522+ def compute_from_last_motion_event(self, event):
523+ '''compute deltas an speeds from the last motion-event'''
524+ self.dt_from_last_motion_event = event.time - \
525+ self._last_motion_event_time
526+ self.dx_from_last_motion_event = event.x - self._last_motion_event_x
527+ self.dy_from_last_motion_event = event.y - self._last_motion_event_y
528+ self.distance_from_last_motion_event = math.sqrt( \
529+ self.dx_from_last_motion_event * self.dx_from_last_motion_event + \
530+ self.dy_from_last_motion_event * self.dy_from_last_motion_event)
531+
532+ if self.dt_from_last_motion_event != 0 :
533+ self.speed_x_from_last_motion_event = \
534+ float(self.dx_from_last_motion_event) / \
535+ float(self.dt_from_last_motion_event)
536+ self.speed_y_from_last_motion_event = \
537+ float(self.dy_from_last_motion_event) / \
538+ float(self.dt_from_last_motion_event)
539+ self.speed_from_last_motion_event = \
540+ float(self.distance_from_last_motion_event) / \
541+ float(self.dt_from_last_motion_event)
542+ else:
543+ self.speed_x_from_last_motion_event = 0
544+ self.speed_y_from_last_motion_event = 0
545+ self.speed_from_last_motion_event = 0
546+
547
548=== modified file 'entertainerlib/frontend/gui/widgets/scroll_menu.py'
549--- entertainerlib/frontend/gui/widgets/scroll_menu.py 2009-03-02 02:27:27 +0000
550+++ entertainerlib/frontend/gui/widgets/scroll_menu.py 2009-04-08 11:23:26 +0000
551@@ -2,209 +2,271 @@
552
553 __licence__ = "GPLv2"
554 __copyright__ = "2007, Lauri Taimila"
555-__author__ = "Lauri Taimila <lauri@taimila.com>"
556+__author__ = ("Lauri Taimila <lauri@taimila.com>",
557+ "Samuel Buffet <Samuel.Buffet@gmail.com>")
558
559 import clutter
560-
561-class ScrollMenu(clutter.Group):
562+import math
563+import gobject
564+
565+from entertainerlib.frontend.gui.widgets.label import Label
566+from entertainerlib.frontend.gui.widgets.motion_buffer import MotionBuffer
567+
568+class LoopedPathBehaviour (clutter.Behaviour):
569+ """A custome Path behaviour driven by an index [0, 1]"""
570+ __gtype_name__ = 'LoopedPathBehaviour'
571+
572+ def __init__(self, alpha=None):
573+ clutter.Behaviour.__init__(self)
574+ self.set_alpha(alpha)
575+ self.start_index = 0.0
576+ self.end_index = 1.0
577+ self.start_knot = (0, 0)
578+ self.end_knot = (10, 10)
579+
580+ def do_alpha_notify (self, alpha_value):
581+ """
582+ We position the actor on the Line defined by the 2 knots according to
583+ the alpha value.
584+ alpha = 0 position = start_knot
585+ alpha = MAX_ALPHA position = end_knot
586+ """
587+ idx = alpha_value * (self.end_index - self.start_index)
588+ idx /= clutter.MAX_ALPHA
589+ idx += self.start_index
590+
591+ if idx >= 0 :
592+ idxx = math.modf(idx)[0]
593+ else:
594+ idxx = 1 + math.modf(idx)[0]
595+
596+ # Calculation of new coordinates
597+ x = idxx * (self.end_knot[0] - self.start_knot[0]) + self.start_knot[0]
598+ y = idxx * (self.end_knot[1] - self.start_knot[1]) + self.start_knot[1]
599+
600+ for actor in self.get_actors():
601+ actor.set_position(int(x), int(y))
602+
603+ @ property
604+ def path_length(self):
605+ """Calculation of the path's length in pixels'"""
606+ dx = self.end_knot[0] - self.start_knot[0]
607+ dy = self.end_knot[1] - self.start_knot[1]
608+ return math.sqrt(dx * dx + dy * dy)
609+
610+class ScrollMenuItem(clutter.Group):
611+ """
612+ This is a Group containing a Label to which a LoopedPathBehaviour is applied
613+ """
614+ __gtype_name__ = 'ScrollMenuItem'
615+
616+ def __init__(self, alpha, text, item_height, font_size, color_name):
617+ clutter.Group.__init__(self)
618+
619+ self.label = Label(font_size, color_name, 0, 0)
620+ self.label.set_text(text)
621+
622+ self.behaviour = LoopedPathBehaviour(alpha)
623+ self.behaviour.apply(self)
624+
625+ clutter.Group.add(self, self.label)
626+
627+class ScrollMenu(clutter.Group, object):
628 """
629 Menu widget that contains text items. Menu can be scrolled up and down.
630-
631- - Options: zoom, opacity, display N items, display arrows top+down
632- - add Clutter labels
633- - go around (how to implement this) (On state_change move items from
634- start->back)
635 """
636-
637- DOWN = 0
638- UP = 1
639-
640- def __init__(self, item_gap, item_height, animated=False):
641- """
642- Initialize Menu
643- @param item_gap: Gap between menuitems in pixels
644- """
645+ __gtype_name__ = 'ScrollMenu'
646+ __gsignals__ = {
647+ 'activated' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
648+ 'selected' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
649+ 'moved' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
650+ }
651+
652+ MODE_SELECTION = 0
653+ MODE_MOTION = 1
654+ MODE_STOP = 2
655+
656+ def __init__(self, item_gap, item_height, font_size, color_name):
657 clutter.Group.__init__(self)
658- self.__active = True
659- self.__clip_size = -1
660- self.__items = []
661- self.__gap = item_gap
662- self.__item_height = item_height
663- self.__animated = animated
664-
665- self.scroll_timeline = clutter.Timeline(7, 26)
666- self.scroll_timeline.connect('new-frame', self.scroll_items_cb)
667-
668- def add(self, actor):
669- """
670- Add new menuitem to the menu.
671- @param item: Label
672- """
673- clutter.Group.add(self, actor)
674- actor.connect('notify::y', self.actor_notify_y)
675-
676- y = ((len(self.__items) * self.__item_height) +
677- (self.__gap * len(self.__items)))
678-
679- if y == 0 :
680- # need to do that to update opacity of the first item added
681- self.actor_notify_y(actor, None)
682- actor.set_position(0, y)
683-
684- self.__items.append(actor)
685-
686- def remove(self, actor):
687- """
688- Remove actor from the menu.
689- """
690- clutter.Group.add(self, actor)
691- for item in self.__items:
692- if item is actor:
693- del item
694-
695- def remove_all(self):
696- """
697- Remove all actors from the menu.
698- """
699- del self.__items
700- self.__items = []
701-
702- def get_index(self, text):
703- """
704- Returns index of label with the text as passed, returns -1 if not found
705- @author Joshua Scotton
706- """
707- for item in self.__items:
708- if item.get_name() == text:
709- return self.__items.index(item)
710- return -1
711-
712- def select(self, index):
713- """
714- Selects the item at the passed index, does not support animations
715- @author Joshua Scotton
716- """
717- #split list so that item at the index will now be at position 2
718- #which is the selected index in a scroll menu
719- items = self.__items[index-2:] + self.__items[0:index-2]
720-
721- #Basically remove all then add in the correct order
722- self.remove_all()
723- for item in items:
724- #Remove all doesn't remove the parent object. So in order to avoid
725- #adding two parent clutter items to the item we need to remove the
726- #old one first
727- clutter.Group.remove(self, item)
728- self.add(item)
729-
730- def get_number_of_items(self):
731- """Get number of menuitems in this Menu"""
732- return len(self.__items)
733-
734- def set_number_of_visible_items(self, number):
735- """
736- Set how many menuitems are displayed on the screen at the same time.
737- @param number: Number of items
738- """
739- self.__clip_size = number
740- self.set_clip(0,
741- 0,
742- self.get_width(),
743- (self.__item_height + self.__gap) * number)
744-
745- def get_number_of_visible_items(self):
746- """
747- Get number of menuitems that are displayed on the screen at the same
748- time.
749-
750- @return: Ingeter
751- """
752- return self.__clip_size
753-
754- def get_selected(self):
755- """
756- Get currently selected menuitem
757- @return Clutter actor object
758- """
759- return self.__items[2]
760-
761- def scroll_down(self):
762- """Scroll menu down by one menuitem."""
763- if not self.scroll_timeline.is_playing():
764- if self.__animated:
765- self.__scroll_direction = self.DOWN
766- self.scroll_timeline.start()
767- else:
768- for item in self.__items:
769- item.move_by(0, self.__item_height + self.__gap)
770-
771- # Set last item to first (menu scrolls)
772- last = self.__items[-1]
773- last.set_position(
774- 0, self.__items[0].get_y() - self.__gap - self.__item_height)
775- self.__items = [last] + self.__items
776- del self.__items[-1]
777-
778- def scroll_up(self):
779- """Scroll menu up by one menuitem."""
780- if not self.scroll_timeline.is_playing():
781- if self.__animated:
782- self.__scroll_direction = self.UP
783- self.scroll_timeline.start()
784- else:
785- for item in self.__items:
786- item.move_by(0, -(self.__item_height + self.__gap))
787-
788- # Set first item to last (menu scrolls)
789- first = self.__items[0]
790- first.set_position(
791- 0, self.__items[-1].get_y() + self.__gap + self.__item_height)
792- self.__items.append(first)
793- del self.__items[0]
794-
795- def scroll_items_cb(self, timeline, frame_num):
796- """Callback function of scroll_down_timeline"""
797- delta = timeline.get_delta()[0]
798- offset = int((self.__item_height + self.__gap) / 7 * delta)
799- if self.__scroll_direction == 1:
800- offset = - offset
801- for item in self.__items:
802- item.move_by(0, offset)
803-
804- def is_active(self):
805+ self._motion_buffer = MotionBuffer()
806+ self._items = []
807+ self._item_gap = item_gap
808+ self._item_height = item_height
809+ self._item_font_size = font_size
810+ self._item_color_name = color_name
811+ self._selected_index = 1
812+ self._visible_nb = 5
813+ self._event_mode = -1
814+ self._animation_progression = 0
815+ self._animation_start_index = 1
816+ self._animation_end_index = 1
817+ self._active = False
818+ self._motion_handler = 0
819+
820+ self._timeline = clutter.Timeline(fps=200, duration=300)
821+ self._alpha = clutter.Alpha(self._timeline, clutter.sine_inc_func)
822+
823+ # preparation to pointer events handling
824+ self.set_reactive(True)
825+ self.connect('scroll-event', self._on_scroll_event)
826+ self.connect('button-press-event', self._on_button_press_event)
827+ self.connect('button-release-event', self._on_button_release_event)
828+
829+ def add_item(self, text, name):
830+ """Creation of a new MenuItem and addition to the ScrollMenu"""
831+ item = ScrollMenuItem(self._alpha, text, self._item_height,
832+ self._item_font_size, self._item_color_name)
833+
834+ item.set_name(name)
835+ item.connect('notify::y', self._on_item_notify_y)
836+ clutter.Group.add(self, item)
837+
838+ self._items.append(item)
839+
840+ def _get_active(self):
841 """
842 Is this menu active.
843 @return boolean, True if menu is currently active, otherwise False
844 """
845- return self.__active
846+ return self._active
847
848- def set_active(self, boolean):
849+ def _set_active(self, boolean):
850 """
851 Set this widget active or inactive
852 @param boolean: True to set widget active, False to set inactive
853 """
854- self.__active = boolean
855+ if self._active == boolean:
856+ return
857+
858+ self._active = boolean
859 if boolean:
860- #if self.__selector is not None:
861- # self.__selector.show()
862 self.set_opacity(255)
863+ self.emit('activated')
864 else:
865- #if self.__selector is not None:
866- # self.__selector.hide()
867 self.set_opacity(128)
868
869- def update(self):
870- """Update the whole menu"""
871- for item in self.__items:
872- opacity = self.get_opacity_for_y(item.get_y())
873- item.set_opacity(opacity)
874+ active = property(_get_active, _set_active)
875+
876+ def _update_behaviours(self, target):
877+ """
878+ Preparation of looped behaviours applied to menu items before
879+ we move them.
880+ """
881+ nbi = len(self._items)
882+ step = 1.0 / nbi
883+ step_pix = self._item_gap + self._item_height
884+ a = int(self._visible_nb / 2) + 1
885+ for x, item in enumerate(self._items):
886+ item.behaviour.start_index = (x + a - self._selected_index) * step
887+ item.behaviour.end_index = (x + a - target) * step
888+
889+ item.behaviour.start_knot = (0.0, -step_pix)
890+ item.behaviour.end_knot = (0.0, (nbi - 1.0) * step_pix)
891+
892+ def _display_items_at_target(self, target):
893+ """
894+ This method draws the menu for a particular targeted value of the
895+ driving index.
896+ """
897+ nbi = len(self._items)
898+ step = 1.0 / nbi
899+
900+ a = int(self._visible_nb / 2) + 1
901+ for x, item in enumerate(self._items):
902+ index = (x + a - target) * step
903+
904+ if index >= 0 :
905+ idxx = math.modf(index)[0]
906+ else:
907+ idxx = 1 + math.modf(index)[0]
908+
909+ # Calculation of new coordinates
910+ xx = idxx * (item.behaviour.end_knot[0] - \
911+ item.behaviour.start_knot[0]) + item.behaviour.start_knot[0]
912+ yy = idxx * (item.behaviour.end_knot[1] - \
913+ item.behaviour.start_knot[1]) + item.behaviour.start_knot[1]
914+
915+ item.set_position(int(xx), int(yy))
916+
917+ def _get_visible_nb(self):
918+ """Return the visible_nb property"""
919+ return self._visible_nb
920+
921+ def _set_visible_nb(self, visible_nb):
922+ """Set the visible_nb property"""
923+ self._visible_nb = visible_nb
924+ h = visible_nb * self._item_height + (visible_nb - 1) * self._item_gap
925+ self.set_clip(0, 0, self.get_width(), h)
926+
927+ visible_nb = property(_get_visible_nb, _set_visible_nb)
928+
929+ def get_selected(self):
930+ """
931+ Get currently selected menuitem
932+ @return Clutter actor object
933+ """
934+ return self._items[int(self._selected_index)]
935+
936+ def _get_selected_index(self):
937+ """Return the selected_index property"""
938+ return self._selected_index
939+
940+ def _set_selected_index(self, selected_index, duration=300):
941+ """Set the selected_index property"""
942+ if not self._timeline.is_playing():
943+ nb = len(self._items)
944+ self._update_behaviours(selected_index)
945+
946+ # those 2 variables are used if we want to stop the timeline
947+ # we use them + timeline progression to calculate the current index
948+ # when (if) we stop
949+ self._animation_start_index = self._selected_index
950+ self._animation_end_index = selected_index
951+
952+ # compute selected_index to be between 0 and nb
953+ if selected_index >= 0:
954+ self._selected_index = selected_index - \
955+ math.modf(selected_index / nb)[1] * nb
956+ else:
957+ self._selected_index = selected_index + \
958+ (math.modf(-(selected_index + 1) / nb)[1] + 1) * nb
959+
960+ self._timeline.set_duration(duration)
961+ self._timeline.start()
962+
963+ self.emit('moved')
964+
965+ selected_index = property(_get_selected_index, _set_selected_index)
966+
967+ def get_index(self, text):
968+ """
969+ Returns index of label with the text as passed, returns -1 if not found
970+ @author Joshua Scotton
971+ """
972+ for item in self._items:
973+ if item.get_name() == text:
974+ return self._items.index(item)
975+ return -1
976+
977+ def scroll_by(self, nb, duration=300):
978+ """Set the selected_index property"""
979+ self._set_selected_index(self._selected_index + nb, duration)
980+
981+ def scroll_up(self, duration=300):
982+ """Set the selected_index property"""
983+ self.scroll_by(-1, duration)
984+
985+ def scroll_down(self, duration=300):
986+ """Set the selected_index property"""
987+ self.scroll_by(1, duration)
988
989 def get_opacity_for_y(self, y):
990- """Calculation of actor's opacity as a function of its y coordinate"""
991- opacity_first_item = 30
992+ """Calculation of actor's opacity as a function of its y coordinates"""
993+ opacity_first_item = 40
994 opacity_selected_item = 255
995+ o = int(self._visible_nb / 2)
996
997- y_medium_item = 2 * (self.__item_height + self.__gap)
998+ y_medium_item = o * (self._item_height + self._item_gap)
999 a = float(opacity_selected_item - opacity_first_item)
1000 a /= float(y_medium_item)
1001 if y <= y_medium_item :
1002@@ -217,8 +279,116 @@
1003
1004 return int(opacity)
1005
1006- def actor_notify_y(self, actor, stage):
1007+ def _on_item_notify_y(self, item, stage):
1008 """Set opacity to actors when they are moving. Opacity is f(y)"""
1009- opacity = self.get_opacity_for_y(actor.get_y())
1010- actor.set_opacity(opacity)
1011+ opacity = self.get_opacity_for_y(item.get_y())
1012+ item.set_opacity(opacity)
1013+
1014+ def _on_button_press_event(self, actor, event):
1015+ """button-press-event handler"""
1016+ clutter.grab_pointer(self)
1017+ if not self.handler_is_connected(self._motion_handler):
1018+ self._motion_handler = self.connect('motion-event',
1019+ self._on_motion_event)
1020+ self.active = True
1021+
1022+ if self._timeline.is_playing():
1023+ # before we stop the timeline, store its progression
1024+ self._animation_progression = self._timeline.get_progress()
1025+
1026+ # A click with an animation pending should stop the animation
1027+ self._timeline.stop()
1028+
1029+ # go to MODE_STOP to handle correctly next button-release event
1030+ self._event_mode = self.MODE_STOP
1031+ else:
1032+ # no animation pending so we're going to do either a menu_item
1033+ # selection or a menu motion. This will be decided later, right now
1034+ # we just take a snapshot of this button-press-event as a start.
1035+ self._motion_buffer.start(event)
1036+ self._event_mode = self.MODE_SELECTION
1037+
1038+ return False
1039+
1040+ def _on_button_release_event(self, actor, event):
1041+ """button-release-event handler"""
1042+ clutter.ungrab_pointer()
1043+ if self.handler_is_connected(self._motion_handler):
1044+ self.disconnect_by_func(self._on_motion_event)
1045+ self._motion_buffer.compute_from_last_motion_event(event)
1046+
1047+ y = event.y - self.get_y()
1048+
1049+ if self._event_mode == self.MODE_SELECTION:
1050+ # if we are in MODE_SELECTION it means that we want to select
1051+ # the menu item bellow the pointer
1052+
1053+ for idx, item in enumerate(self._items):
1054+ b = item.get_y()
1055+ h = item.get_height()
1056+ if (y >= b) and (y <= (b + h)):
1057+ delta1 = idx - self._selected_index
1058+ delta2 = idx - self._selected_index + len(self._items)
1059+ delta3 = idx - self._selected_index - len(self._items)
1060+
1061+ delta = 99999
1062+ for i in [delta1, delta2, delta3]:
1063+ if math.fabs(i)<math.fabs(delta):
1064+ delta = i
1065+
1066+ self.scroll_by(delta)
1067+
1068+ # if delta = 0 it means we've clicked on the selected item
1069+ if delta == 0:
1070+ self.emit('selected')
1071+
1072+ elif self._event_mode == self.MODE_MOTION:
1073+ speed = self._motion_buffer.speed_y_from_last_motion_event
1074+ target = self._selected_index - \
1075+ self._motion_buffer.dy_from_start / \
1076+ self._items[0].behaviour.path_length * len(self._items)
1077+
1078+ new_index = int(target - 5 * speed)
1079+ self._selected_index = target
1080+ self._set_selected_index(new_index, 1000)
1081+
1082+ else:
1083+ # If we have stopped the pending animation. Now we have to do
1084+ # a small other one to select the closest menu-item
1085+ current_index = self._animation_start_index + \
1086+ (self._animation_end_index - self._animation_start_index) * \
1087+ self._animation_progression
1088+ self._selected_index = current_index
1089+ target_index = int(current_index)
1090+ self._set_selected_index(target_index, 1000)
1091+
1092+ return False
1093+
1094+ def _on_motion_event(self, actor, event):
1095+ """motion-event handler"""
1096+ self._motion_buffer.compute_from_start(event)
1097+ if self._motion_buffer.distance_from_start > 10:
1098+ self._motion_buffer.take_new_motion_event(event)
1099+ self._event_mode = self.MODE_MOTION
1100+ target = self._selected_index - \
1101+ self._motion_buffer.dy_from_start / \
1102+ self._items[0].behaviour.path_length * len(self._items)
1103+ self._display_items_at_target(target)
1104+
1105+ self.get_stage().show_cursor()
1106+
1107+ return False
1108+
1109+ def _on_scroll_event(self, actor, event):
1110+ """scroll-event handler (mouse's wheel)"""
1111+ self.active = True
1112+
1113+ if event.direction == clutter.SCROLL_DOWN:
1114+ self.scroll_down(150)
1115+ else:
1116+ self.scroll_up(150)
1117+
1118+ self.get_stage().show_cursor()
1119+
1120+ return False
1121
1122
1123=== added file 'entertainerlib/tests/test_scrollmenu.py'
1124--- entertainerlib/tests/test_scrollmenu.py 1970-01-01 00:00:00 +0000
1125+++ entertainerlib/tests/test_scrollmenu.py 2009-04-07 11:31:48 +0000
1126@@ -0,0 +1,36 @@
1127+"""Tests ScrollMenu"""
1128+
1129+__license__ = "GPLv2"
1130+__copyright__ = "2009, Samuel Buffet"
1131+__author__ = "Samuel Buffet <samuel.buffet@gmail.com>"
1132+
1133+import clutter
1134+
1135+from entertainerlib.frontend.gui.widgets.scroll_menu import ScrollMenu
1136+from entertainerlib.tests import EntertainerTest
1137+
1138+class ScrollMenuTest(EntertainerTest):
1139+ """Test for entertainerlib.frontend.gui.widgets.scroll_menu"""
1140+
1141+ def setUp(self):
1142+ """Set up the test"""
1143+ EntertainerTest.setUp(self)
1144+
1145+
1146+ self.menu = ScrollMenu(10, 60, 0.045, "menuitem_active")
1147+ self.menu.set_name("mainmenu")
1148+
1149+ self.menu.add_item(_("Play CD"), "disc")
1150+ self.menu.add_item(_("Videos"), "videos")
1151+ self.menu.add_item(_("Music"), "music")
1152+ self.menu.add_item(_("Photographs"), "photo")
1153+ self.menu.add_item(_("Headlines"), "rss")
1154+
1155+ def tearDown(self):
1156+ """Clean up after the test"""
1157+ EntertainerTest.tearDown(self)
1158+
1159+ def testCreate(self):
1160+ """Test correct ScrollArea initialization"""
1161+ self.assertTrue(isinstance(self.menu, (clutter.Group, object)))
1162+

Subscribers

People subscribed via source and target branches