Hi Gonzalo,

thanks for the new patch.

On 01/26/2012 10:18 PM, godi...@sugarlabs.org wrote:
From: Gonzalo Odiard<godi...@gmail.com>

Changes:

* Fixes in schema descriptions.
* Frame position value.
* UI fixes according to different reviews.
* Use a SugarMenuItem to mimic MenuItem behaviour because in the gtk3
palettes can coexist menues and other widgets. This is not mandatory now
but will do easier migration.

As discussed with you today, we should for now keep the old-style menu as the Shell is still using the gtk2-toolkit and we can not change API there anymore.

* Fixed max/min values in configurations. I was using values from espeak
documentation but should use from gst-plugins-espeak

Signed-off-by: Gonzalo Odiard<gonz...@laptop.org>
---
  data/sugar.schemas.in             |   24 ++++
  extensions/deviceicon/Makefile.am |    1 +
  extensions/deviceicon/speech.py   |  230 ++++++++++++++++++++++++++++++++++++
  extensions/globalkey/Makefile.am  |    1 +
  extensions/globalkey/speech.py    |   23 ++++
  src/jarabe/model/Makefile.am      |    1 +
  src/jarabe/model/speech.py        |  235 +++++++++++++++++++++++++++++++++++++
  src/jarabe/view/keyhandler.py     |   29 +-----
  8 files changed, 516 insertions(+), 28 deletions(-)
  create mode 100644 extensions/deviceicon/speech.py
  create mode 100644 extensions/globalkey/speech.py
  create mode 100644 src/jarabe/model/speech.py

diff --git a/data/sugar.schemas.in b/data/sugar.schemas.in
index aaef381..226b41f 100644
--- a/data/sugar.schemas.in
+++ b/data/sugar.schemas.in
@@ -368,5 +368,29 @@
        </locale>
      </schema>

+<schema>
+<key>/schemas/desktop/sugar/speech/pitch</key>
+<applyto>/desktop/sugar/speech/pitch</applyto>
+<owner>sugar</owner>
+<type>int</type>
+<default>0</default>
+<locale name="C">
+<short>Pitch value for the speech sugar service</short>
+<long>Pitch value used by the speech service in Sugar</long>
+</locale>
+</schema>
+
+<schema>
+<key>/schemas/desktop/sugar/speech/rate</key>
+<applyto>/desktop/sugar/speech/rate</applyto>
+<owner>sugar</owner>
+<type>int</type>
+<default>0</default>
+<locale name="C">
+<short>Rate value for the speech sugar service</short>
+<long>Rate value used by the speech service in Sugar</long>
+</locale>
+</schema>
+
    </schemalist>
  </gconfschemafile>
diff --git a/extensions/deviceicon/Makefile.am 
b/extensions/deviceicon/Makefile.am
index 118d866..7ed1f77 100644
--- a/extensions/deviceicon/Makefile.am
+++ b/extensions/deviceicon/Makefile.am
@@ -5,5 +5,6 @@ sugar_PYTHON =          \
        battery.py      \
        network.py      \
        speaker.py      \
+       speech.py       \
        touchpad.py     \
        volume.py
diff --git a/extensions/deviceicon/speech.py b/extensions/deviceicon/speech.py
new file mode 100644
index 0000000..6311fd6
--- /dev/null
+++ b/extensions/deviceicon/speech.py
@@ -0,0 +1,230 @@
+# Copyright (C) 2011 One Laptop Per Child
+# Copyright (C) 2011 Gonzalo Odiard

That should be "Copyright (C) 2011 One Laptop Per Child" only.

+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+from gettext import gettext as _
+
+import glib
+import gtk
+import gconf
+import gobject
+
+from sugar.graphics.icon import Icon
+from sugar.graphics.tray import TrayIcon
+from sugar.graphics.palette import Palette
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics import style
+
+from jarabe.frame.frameinvoker import FrameWidgetInvoker
+from jarabe.model import speech
+
+
+_ICON_NAME = 'microphone'
+
+
+class SpeechDeviceView(TrayIcon):
+
+    FRAME_POSITION_RELATIVE = 150
+
+    def __init__(self):
+        client = gconf.client_get_default()
+        self._color = XoColor(client.get_string('/desktop/sugar/user/color'))
+        TrayIcon.__init__(self, icon_name=_ICON_NAME, xo_color=self._color)
+        self.set_palette_invoker(FrameWidgetInvoker(self))
+        self._manager = speech.get_speech_manager()
+        self._icon_widget.connect('button-release-event',
+                                  self.__button_release_event_cb)
+
+    def create_palette(self):
+        label = glib.markup_escape_text(_('Speech'))
+        palette = SpeechPalette(label, manager=self._manager)
+        palette.set_group_id('frame')
+        return palette
+
+    def __button_release_event_cb(self, widget, event):
+        self.palette_invoker.notify_right_click()
+        return True
+
+
+class SugarMenuItem(gtk.EventBox):
+
+    __gsignals__ = {
+        'clicked': (gobject.SIGNAL_RUN_FIRST, None, [])
+    }
+
+    def __init__(self, icon_name, label_text):
+        gtk.EventBox.__init__(self)
+        self._sensitive = True
+        vbox = gtk.VBox()
+        hbox = gtk.HBox()
+        vbox.set_border_width(style.DEFAULT_PADDING)
+        self.icon = Icon()
+        self.icon.props.icon_name = icon_name
+        hbox.pack_start(self.icon, expand=False, fill=False,
+                padding=style.DEFAULT_PADDING)
+        align = gtk.Alignment(xalign=0.0, yalign=0.5, xscale=0.0, yscale=0.0)
+        text = '<span foreground="%s">' % style.COLOR_WHITE.get_html() + \
+                    label_text + '</span>'
+        self.label = gtk.Label()
+        self.label.set_use_markup(True)
+        self.label.set_markup(text)
+        align.add(self.label)
+        hbox.pack_start(align, expand=True, fill=True,
+                padding=style.DEFAULT_PADDING)
+        vbox.pack_start(hbox, expand=False, fill=False,
+                padding=style.DEFAULT_PADDING)
+        self.add(vbox)
+        self.id_bt_release_cb = self.connect('button-release-event',
+                self.__button_release_cb)
+        self.id_enter_notify_cb = self.connect('enter-notify-event',
+                self.__enter_notify_cb)
+        self.id_leave_notify_cb = self.connect('leave-notify-event',
+                self.__leave_notify_cb)
+        self.modify_bg(gtk.STATE_NORMAL, style.COLOR_BLACK.get_gdk_color())
+        self.show_all()
+        self.set_above_child(True)
+
+    def __button_release_cb(self, widget, event):
+        self.emit('clicked')
+
+    def __enter_notify_cb(self, widget, event):
+        self.modify_bg(gtk.STATE_NORMAL,
+                style.COLOR_BUTTON_GREY.get_gdk_color())
+
+    def __leave_notify_cb(self, widget, event):
+        self.modify_bg(gtk.STATE_NORMAL, style.COLOR_BLACK.get_gdk_color())
+
+    def set_icon(self, icon_name):
+        self.icon.props.icon_name = icon_name
+
+    def set_label(self, label_text):
+        text = '<span foreground="%s">' % style.COLOR_WHITE.get_html() + \
+                    label_text + '</span>'
+        self.label.set_markup(text)
+
+    def set_sensitive(self, sensitive):
+        if self._sensitive == sensitive:
+            return
+
+        self._sensitive = sensitive
+        if sensitive:
+            self.handler_unblock(self.id_bt_release_cb)
+            self.handler_unblock(self.id_enter_notify_cb)
+            self.handler_unblock(self.id_leave_notify_cb)
+        else:
+            self.handler_block(self.id_bt_release_cb)
+            self.handler_block(self.id_enter_notify_cb)
+            self.handler_block(self.id_leave_notify_cb)
+            self.modify_bg(gtk.STATE_NORMAL, style.COLOR_BLACK.get_gdk_color())
+
+
+class SpeechPalette(Palette):
+
+    def __init__(self, primary_text, manager):
+        Palette.__init__(self, label=primary_text)
+
+        self._manager = manager
+        self._manager.connect('play', self._set_buttons_state, 'play')
+        self._manager.connect('stop', self._set_buttons_state, 'stop')
+        self._manager.connect('pause', self._set_buttons_state, 'pause')
+
+        vbox = gtk.VBox()
+        self.set_content(vbox)
+
+        vbox_menu = gtk.VBox()
+        # TODO: private!!!
+        self._content.set_border_width(0)
+
+        self._play_pause_button = SugarMenuItem('player_play',
+                _('Say selected text'))
+        self._play_pause_button.connect('clicked', self.__play_clicked_cb)
+        vbox_menu.add(self._play_pause_button)
+
+        self._stop_button = SugarMenuItem('player_stop', _('Stop playback'))
+        self._stop_button.connect('clicked', self.__stop_clicked_cb)
+        self._stop_button.set_sensitive(False)
+        vbox_menu.add(self._stop_button)
+
+        vbox.pack_start(vbox_menu, expand=True, fill=True,
+                padding=style.DEFAULT_SPACING)
+
+        vbox.pack_start(gtk.HSeparator(), expand=True, fill=True, padding=0)
+
+        hbox_controls = gtk.HBox()
+        vbox_controls = gtk.VBox()
+        self._adj_pitch = gtk.Adjustment(value=self._manager.get_pitch(),
+                                          lower=self._manager.MIN_PITCH,
+                                          upper=self._manager.MAX_PITCH)
+        self._hscale_pitch = gtk.HScale(self._adj_pitch)
+        self._hscale_pitch.set_draw_value(False)
+
+        vbox_controls.pack_start(gtk.Label(_('Pitch')))
+        vbox_controls.pack_start(self._hscale_pitch)
+
+        self._adj_rate = gtk.Adjustment(value=self._manager.get_rate(),
+                                          lower=self._manager.MIN_RATE,
+                                          upper=self._manager.MAX_RATE)
+        self._hscale_rate = gtk.HScale(self._adj_rate)
+        self._hscale_rate.set_draw_value(False)
+
+        vbox_controls.pack_start(gtk.Label(_('Rate')))
+        vbox_controls.pack_start(self._hscale_rate)
+        hbox_controls.pack_start(vbox_controls, expand=True, fill=True,
+                padding=style.DEFAULT_PADDING)
+        vbox.pack_start(hbox_controls, expand=True, fill=True,
+                padding=style.DEFAULT_PADDING)
+        hbox_controls.set_border_width(style.DEFAULT_SPACING)
+        vbox.show_all()
+
+        self._adj_pitch.connect('value_changed', self.__adj_pitch_changed_cb)
+        self._adj_rate.connect('value_changed', self.__adj_rate_changed_cb)
+
+    def __adj_pitch_changed_cb(self, adjustement):
+        self._manager.set_pitch(int(adjustement.value))
+
+    def __adj_rate_changed_cb(self, adjustement):
+        self._manager.set_rate(int(adjustement.value))
+
+    def __play_clicked_cb(self, widget):
+        if self._manager.is_paused:
+            self._manager.restart()
+        elif not self._manager.is_playing:
+            self._manager.say_selected_text()
+        else:
+            self._manager.pause()
+
+    def __stop_clicked_cb(self, widget):
+        self._manager.stop()
+
+    def _set_buttons_state(self, manager, signal):
+        if signal == 'play':
+            self._play_pause_button.set_icon('player_pause')
+            self._play_pause_button.set_label(_('Pause playback'))
+            self._stop_button.set_sensitive(True)
+
+        elif signal == 'pause':
+            self._play_pause_button.set_icon('player_play')
+            self._play_pause_button.set_label(_('Say selected text'))
+            self._stop_button.set_sensitive(True)
+
+        elif signal == 'stop':
+            self._play_pause_button.set_icon('player_play')
+            self._play_pause_button.set_label(_('Say selected text'))
+            self._stop_button.set_sensitive(False)
+
+
+def setup(tray):
+    tray.add_device(SpeechDeviceView())
diff --git a/extensions/globalkey/Makefile.am b/extensions/globalkey/Makefile.am
index 69afac2..b6cbbd6 100644
--- a/extensions/globalkey/Makefile.am
+++ b/extensions/globalkey/Makefile.am
@@ -3,4 +3,5 @@ sugardir = $(pkgdatadir)/extensions/globalkey
  sugar_PYTHON =                \
        __init__.py     \
        screenshot.py   \
+       speech.py       \
        viewsource.py
diff --git a/extensions/globalkey/speech.py b/extensions/globalkey/speech.py
new file mode 100644
index 0000000..020fa81
--- /dev/null
+++ b/extensions/globalkey/speech.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2011 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+from jarabe.model import speech
+
+BOUND_KEYS = ['<alt>s']

As stated in the other mail, I am happy with going back to use <alt><shift>s.

+
+def handle_key_press(key):
+    speech.get_speech_manager().say_selected_text()
diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am
index 92e8712..2fc6b1c 100644
--- a/src/jarabe/model/Makefile.am
+++ b/src/jarabe/model/Makefile.am
@@ -16,4 +16,5 @@ sugar_PYTHON =                        \
        screen.py               \
          session.py            \
        sound.py                \
+       speech.py               \
        telepathyclient.py
diff --git a/src/jarabe/model/speech.py b/src/jarabe/model/speech.py
new file mode 100644
index 0000000..b9e3e25
--- /dev/null
+++ b/src/jarabe/model/speech.py
@@ -0,0 +1,235 @@
+# Copyright (C) 2011 One Laptop Per Child
+# Copyright (C) 2011 Gonzalo Odiard

That should be "Copyright (C) 2011 One Laptop Per Child" only.

+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+import os
+import logging
+
+import gconf
+import gst
+import gtk
+import gobject
+
+
+DEFAULT_PITCH = 0
+
+
+DEFAULT_RATE = 0
+
+_speech_manager = None
+
+
+class SpeechManager(gobject.GObject):
+
+    __gtype_name__ = 'SpeechManager'
+
+    __gsignals__ = {
+        'play': (gobject.SIGNAL_RUN_FIRST, None, []),
+        'pause': (gobject.SIGNAL_RUN_FIRST, None, []),
+        'stop': (gobject.SIGNAL_RUN_FIRST, None, [])
+    }
+
+    MIN_PITCH = -100
+    MAX_PITCH = 100
+
+    MIN_RATE = -100
+    MAX_RATE = 100
+
+    def __init__(self, **kwargs):
+        gobject.GObject.__init__(self, **kwargs)
+        self._player = _GstSpeechPlayer()
+        self._player.connect('play', self._update_state, 'play')
+        self._player.connect('stop', self._update_state, 'stop')
+        self._player.connect('pause', self._update_state, 'pause')
+        self._voice_name = self._player.get_default_voice()
+        self._pitch = DEFAULT_PITCH
+        self._rate = DEFAULT_RATE
+        self._is_playing = False
+        self._is_paused = False
+        self.restore()
+
+    def _update_state(self, player, signal):
+        self._is_playing = (signal == 'play')
+        self._is_paused = (signal == 'pause')
+        self.emit(signal)
+
+    def get_is_playing(self):
+        return self._is_playing
+
+    is_playing = gobject.property(type=bool, getter=get_is_playing,
+            setter=None, default=False)
+
+    def get_is_paused(self):
+        return self._is_paused
+
+    is_paused = gobject.property(type=bool, getter=get_is_paused,
+            setter=None, default=False)
+
+    def get_pitch(self):
+        return self._pitch
+
+    def get_rate(self):
+        return self._rate
+
+    def set_pitch(self, pitch):
+        self._pitch = pitch
+        self.save()
+
+    def set_rate(self, rate):
+        self._rate = rate
+        self.save()
+
+    def say_text(self, text):
+        if text:
+            self._player.speak(self._pitch, self._rate, self._voice_name, text)
+
+    def say_selected_text(self):
+        clipboard = gtk.clipboard_get(selection='PRIMARY')
+        clipboard.request_text(self._primary_selection_cb)

Two '_' for the callback name.

+
+    def pause(self):
+        self._player.pause_sound_device()
+
+    def restart(self):
+        self._player.restart_sound_device()
+
+    def stop(self):
+        self._player.stop_sound_device()
+
+    def _primary_selection_cb(self, clipboard, text, user_data):
+        logging.debug('SpeechManager._primary_selection_cb: %r', text)
+        self.say_text(text)

The logging is not needed.

+    def save(self):
+        client = gconf.client_get_default()
+        client.set_int('/desktop/sugar/speech/pitch', self._pitch)
+        client.set_int('/desktop/sugar/speech/rate', self._rate)
+        logging.debug('saving speech configuration pitch %s rate %s',
+                self._pitch, self._rate)
+
+    def restore(self):
+        client = gconf.client_get_default()
+        self._pitch = client.get_int('/desktop/sugar/speech/pitch')
+        self._rate = client.get_int('/desktop/sugar/speech/rate')
+        logging.debug('loading speech configuration pitch %s rate %s',
+                self._pitch, self._rate)
+
+
+class _GstSpeechPlayer(gobject.GObject):
+
+    __gsignals__ = {
+        'play': (gobject.SIGNAL_RUN_FIRST, None, []),
+        'pause': (gobject.SIGNAL_RUN_FIRST, None, []),
+        'stop': (gobject.SIGNAL_RUN_FIRST, None, [])
+    }
+
+    def __init__(self):
+        gobject.GObject.__init__(self)
+        self._pipeline = None
+
+    def restart_sound_device(self):
+        if self._pipeline is None:
+            logging.debug('Trying to restart not initialized sound device')
+            return
+
+        self._pipeline.set_state(gst.STATE_PLAYING)
+        self.emit('play')
+
+    def pause_sound_device(self):
+        if self._pipeline is None:
+            return
+
+        self._pipeline.set_state(gst.STATE_PAUSED)
+        self.emit('pause')
+
+    def stop_sound_device(self):
+        if self._pipeline is None:
+            return
+
+        self._pipeline.set_state(gst.STATE_NULL)
+        self.emit('stop')
+
+    def make_pipeline(self, command):
+        if self._pipeline is not None:
+            self.stop_sound_device()
+            del self._pipeline
+
+        self._pipeline = gst.parse_launch(command)
+
+        bus = self._pipeline.get_bus()
+        bus.add_signal_watch()
+        bus.connect('message::element', self.__pipe_message_cb)
+
+    def __pipe_message_cb(self, bus, message):
+        if message.structure.get_name() == 'espeak-mark' and \
+                message.structure['mark'] == 'end':
+            self.emit('stop')
+
+    def speak(self, pitch, rate, voice_name, text):
+        # TODO workaround for http://bugs.sugarlabs.org/ticket/1801
+        if not [i for i in text if i.isalnum()]:
+            return
+        text = text + '<mark name="end>"></mark>'
+
+        self.make_pipeline('espeak name=espeak ! autoaudiosink')
+        src = self._pipeline.get_by_name('espeak')
+
+        logging.debug('pitch=%d rate=%d voice=%s text=%s', pitch, rate,
+                voice_name, text)

The logging is not needed here anymore.

Regards,
   Simon
_______________________________________________
Sugar-devel mailing list
Sugar-devel@lists.sugarlabs.org
http://lists.sugarlabs.org/listinfo/sugar-devel

Reply via email to