From ae5ce06ccb1f604fa1e4eaeb16d9ba8122b4923d Mon Sep 17 00:00:00 2001 From: Marco Pesenti Gritti Date: Mon, 04 Feb 2008 22:36:12 +0000 Subject: Refactor directory structure a bit, preliminary to the library split-out. --- (limited to 'src') diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..4acd06b --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +config.py diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..7b45960 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = controlpanel hardware model view intro + +sugardir = $(pkgdatadir)/shell +sugar_PYTHON = \ + config.py \ + logsmanager.py \ + main.py \ + shellservice.py + +EXTRA_DIST = $(bin_SCRIPTS) $(conf_DATA) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..41b4b1c --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,26 @@ +"""OLPC Sugar Graphical "Shell" Interface + +Provides the shell-level operations for managing +the OLPC laptop computers. It interacts heavily +with (and depends upon) the Sugar UI libraries. + +This is a "graphical" shell, the name does not +refer to a command-line "shell" interface. +""" + +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 + diff --git a/src/config.py.in b/src/config.py.in new file mode 100644 index 0000000..c1a7a4a --- /dev/null +++ b/src/config.py.in @@ -0,0 +1,20 @@ +# Copyright (C) 2008 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +prefix = '@prefix@' +bin_path = '@prefix@/bin' +data_path = '@prefix@/share/sugar/data' diff --git a/src/controlpanel/Makefile.am b/src/controlpanel/Makefile.am new file mode 100644 index 0000000..f89132c --- /dev/null +++ b/src/controlpanel/Makefile.am @@ -0,0 +1,5 @@ +sugardir = $(pkgdatadir)/shell/controlpanel +sugar_PYTHON = \ + __init__.py \ + cmd.py \ + control.py diff --git a/src/controlpanel/__init__.py b/src/controlpanel/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/controlpanel/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 + diff --git a/src/controlpanel/cmd.py b/src/controlpanel/cmd.py new file mode 100644 index 0000000..634faa9 --- /dev/null +++ b/src/controlpanel/cmd.py @@ -0,0 +1,80 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import sys +import getopt +from gettext import gettext as _ + +from sugar import env + +from controlpanel import control + +def cmd_help(): + print _('Usage: sugar-control-panel [ option ] key [ args ... ] \n\ + Control for the sugar environment. \n\ + Options: \n\ + -h show this help message and exit \n\ + -l list all the available options \n\ + -h key show information about this key \n\ + -g key get the current value of the key \n\ + -s key set the current value for the key \n\ + ') + +def main(): + try: + opts, args = getopt.getopt(sys.argv[1:], "h:s:g:l", []) + except getopt.GetoptError: + cmd_help() + sys.exit(2) + + output = None + verbose = False + + if not opts: + cmd_help() + sys.exit() + + for opt, key in opts: + if opt in ("-h"): + method = getattr(control, 'set_' + key, None) + if method is None: + print _("sugar-control-panel: key=%s not an available option"% key) + sys.exit() + else: + print method.__doc__ + if opt in ("-l"): + elems = dir(control) + for elem in elems: + if elem.startswith('set_'): + print elem[4:] + if opt in ("-g"): + method = getattr(control, 'print_' + key, None) + if method is None: + print _("sugar-control-panel: key=%s not an available option"% key) + sys.exit() + else: + method() + if opt in ("-s"): + method = getattr(control, 'set_' + key, None) + if method is None: + print _("sugar-control-panel: key=%s not an available option"% key) + sys.exit() + else: + try: + method(*args) + except Exception, e: + print _("sugar-control-panel: %s"% e) diff --git a/src/controlpanel/control.py b/src/controlpanel/control.py new file mode 100644 index 0000000..a26132e --- /dev/null +++ b/src/controlpanel/control.py @@ -0,0 +1,481 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# +# The language config is based on the system-config-language +# (http://fedoraproject.org/wiki/SystemConfig/language) tool +# and the timezone config on the system-config-date +# (http://fedoraproject.org/wiki/SystemConfig/date) tool. +# Parts of the code were reused. +# + +import os +import string +import shutil +from gettext import gettext as _ +import dbus + +from sugar import profile +from sugar.graphics.xocolor import XoColor + +NM_SERVICE_NAME = 'org.freedesktop.NetworkManager' +NM_SERVICE_PATH = '/org/freedesktop/NetworkManager' +NM_SERVICE_IFACE = 'org.freedesktop.NetworkManager' +NM_ASLEEP = 1 + +_COLORS = {'red': {'dark':'#b20008', 'medium':'#e6000a', 'light':'#ffadce'}, + 'orange': {'dark':'#9a5200', 'medium':'#c97e00', 'light':'#ffc169'}, + 'yellow': {'dark':'#807500', 'medium':'#be9e00', 'light':'#fffa00'}, + 'green': {'dark':'#008009', 'medium':'#00b20d', 'light':'#8bff7a'}, + 'blue': {'dark':'#00588c', 'medium':'#005fe4', 'light':'#bccdff'}, + 'purple': {'dark':'#5e008c', 'medium':'#7f00bf', 'light':'#d1a3ff'} + } + +_MODIFIERS = ('dark', 'medium', 'light') + +_TIMEZONE_CONFIG = '/etc/sysconfig/clock' + +_LANGUAGES = { + 'Afrikaans/South_Africa': 'af_ZA', + 'Albanian': 'sq_AL.UTF-8', + 'Amharic/Ethiopian': 'am_ET.UTF-8', + 'Arabic/Algeria': 'ar_DZ.UTF-8', + 'Arabic/Bahrain': 'ar_BH.UTF-8', + 'Arabic/Egypt': 'ar_EG.UTF-8', + 'Arabic/India': 'ar_IN.UTF-8', + 'Arabic/Iraq': 'ar_IQ.UTF-8', + 'Arabic/Jordan': 'ar_JO.UTF-8', + 'Arabic/Kuwait': 'ar_KW.UTF-8', + 'Arabic/Lebanon': 'ar_LB.UTF-8', + 'Arabic/Libyan_Arab_Jamahiriya': 'ar_LY.UTF-8', + 'Arabic/Morocco': 'ar_MA.UTF-8', + 'Arabic/Oman': 'ar_OM.UTF-8', + 'Arabic/Qatar': 'ar_QA.UTF-8', + 'Arabic/Saudi_Arabia': 'ar_SA.UTF-8', + 'Arabic/Sudan': 'ar_SD.UTF-8', + 'Arabic/Syrian_Arab_Republic': 'ar_SY.UTF-8', + 'Arabic/Tunisia': 'ar_TN.UTF-8', + 'Arabic/United_Arab_Emirates': 'ar_AE.UTF-8', + 'Arabic/Yemen': 'ar_YE.UTF-8', + 'Basque/Spain': 'eu_ES.UTF-8', + 'Belarusian': 'be_BY.UTF-8', + 'Bengali/BD': 'bn_BD.UTF-8', + 'Bengali/India': 'bn_IN.UTF-8', + 'Bosnian/Bosnia_and_Herzegowina': 'bs_BA', + 'Breton/France': 'br_FR', + 'Bulgarian': 'bg_BG.UTF-8', + 'Catalan/Spain': 'ca_ES.UTF-8', + 'Chinese/Hong_Kong': 'zh_HK.UTF-8', + 'Chinese/P.R._of_China': 'zh_CN.UTF-8', + 'Chinese/Taiwan': 'zh_TW.UTF-8', + 'Cornish/Britain': 'kw_GB.UTF-8', + 'Croatian': 'hr_HR.UTF-8', + 'Czech': 'cs_CZ.UTF-8', + 'Danish': 'da_DK.UTF-8', + 'Dutch/Belgium': 'nl_BE.UTF-8', + 'Dutch/Netherlands': 'nl_NL.UTF-8', + 'English/Australia': 'en_AU.UTF-8', + 'English/Botswana': 'en_BW.UTF-8', + 'English/Canada': 'en_CA.UTF-8', + 'English/Denmark': 'en_DK.UTF-8', + 'English/Great_Britain': 'en_GB.UTF-8', + 'English/Hong_Kong': 'en_HK.UTF-8', + 'English/India': 'en_IN.UTF-8', + 'English/Ireland': 'en_IE.UTF-8', + 'English/New_Zealand': 'en_NZ.UTF-8', + 'English/Philippines': 'en_PH.UTF-8', + 'English/Singapore': 'en_SG.UTF-8', + 'English/South_Africa': 'en_ZA.UTF-8', + 'English/USA': 'en_US.UTF-8', + 'English/Zimbabwe': 'en_ZW.UTF-8', + 'Estonian': 'et_EE.UTF-8', + 'Faroese/Faroe_Islands': 'fo_FO.UTF-8', + 'Finnish': 'fi_FI.UTF-8', + 'French/Belgium': 'fr_BE.UTF-8', + 'French/Canada': 'fr_CA.UTF-8', + 'French/France': 'fr_FR.UTF-8', + 'French/Luxemburg': 'fr_LU.UTF-8', + 'French/Switzerland': 'fr_CH.UTF-8', + 'Galician/Spain': 'gl_ES.UTF-8', + 'German/Austria': 'de_AT.UTF-8', + 'German/Belgium': 'de_BE.UTF-8', + 'German/Germany': 'de_DE.UTF-8', + 'German/Luxemburg': 'de_LU.UTF-8', + 'German/Switzerland': 'de_CH.UTF-8', + 'Greek': 'el_GR.UTF-8', + 'Greenlandic/Greenland': 'kl_GL.UTF-8', + 'Gujarati/India': 'gu_IN.UTF-8', + 'Hausa/Nigeria': 'ha_NG.UTF-8', + 'Hebrew/Israel': 'he_IL.UTF-8', + 'Hindi/India': 'hi_IN.UTF-8', + 'Hungarian': 'hu_HU.UTF-8', + 'Icelandic': 'is_IS.UTF-8', + 'Igbo/Nigeria': 'ig_NG.UTF-8', + 'Indonesian': 'id_ID.UTF-8', + 'Irish': 'ga_IE.UTF-8', + 'Italian/Italy': 'it_IT.UTF-8', + 'Italian/Switzerland': 'it_CH.UTF-8', + 'Japanese': 'ja_JP.UTF-8', + 'Korean/Republic_of_Korea': 'ko_KR.UTF-8', + 'Lao/Laos': 'lo_LA.UTF-8', + 'Latvian/Latvia': 'lv_LV.UTF-8', + 'Lithuanian': 'lt_LT.UTF-8', + 'Macedonian': 'mk_MK.UTF-8', + 'Malay/Malaysia': 'ms_MY.UTF-8', + 'Maltese/malta': 'mt_MT.UTF-8', + 'Manx/Britain': 'gv_GB.UTF-8', + 'Marathi/India': 'mr_IN.UTF-8', + 'Mongolian': 'mn_MN.UTF-8', + 'Nepali': 'ne_NP.UTF-8', + 'Northern/Norway': 'se_NO', + 'Norwegian': 'nb_NO.UTF-8', + 'Norwegian,/Norway': 'nn_NO.UTF-8', + 'Occitan/France': 'oc_FR', + 'Oriya/India': 'or_IN.UTF-8', + 'Persian/Iran': 'fa_IR.UTF-8', + 'Polish': 'pl_PL.UTF-8', + 'Portuguese/Brasil': 'pt_BR.UTF-8', + 'Portuguese/Portugal': 'pt_PT.UTF-8', + 'Punjabi/India': 'pa_IN.UTF-8', + 'Romanian': 'ro_RO.UTF-8', + 'Russian': 'ru_RU.UTF-8', + 'Russian/Ukraine': 'ru_UA.UTF-8', + 'Serbian': 'sr_CS.UTF-8', + 'Serbian/Latin': 'sr_CS.UTF-8@Latn', + 'Slovak': 'sk_SK.UTF-8', + 'Slovenian/Slovenia': 'sl_SI.UTF-8', + 'Spanish/Argentina': 'es_AR.UTF-8', + 'Spanish/Bolivia': 'es_BO.UTF-8', + 'Spanish/Chile': 'es_CL.UTF-8', + 'Spanish/Colombia': 'es_CO.UTF-8', + 'Spanish/Costa_Rica': 'es_CR.UTF-8', + 'Spanish/Dominican_Republic': 'es_DO.UTF-8', + 'Spanish/El_Salvador': 'es_SV.UTF-8', + 'Spanish/Equador': 'es_EC.UTF-8', + 'Spanish/Guatemala': 'es_GT.UTF-8', + 'Spanish/Honduras': 'es_HN.UTF-8', + 'Spanish/Mexico': 'es_MX.UTF-8', + 'Spanish/Nicaragua': 'es_NI.UTF-8', + 'Spanish/Panama': 'es_PA.UTF-8', + 'Spanish/Paraguay': 'es_PY.UTF-8', + 'Spanish/Peru': 'es_PE.UTF-8', + 'Spanish/Puerto_Rico': 'es_PR.UTF-8', + 'Spanish/Spain': 'es_ES.UTF-8', + 'Spanish/USA': 'es_US.UTF-8', + 'Spanish/Uruguay': 'es_UY.UTF-8', + 'Spanish/Venezuela': 'es_VE.UTF-8', + 'Swedish/Finland': 'sv_FI.UTF-8', + 'Swedish/Sweden': 'sv_SE.UTF-8', + 'Tagalog/Philippines': 'tl_PH', + 'Tamil/India': 'ta_IN.UTF-8', + 'Telugu/India': 'te_IN.UTF-8', + 'Thai': 'th_TH.UTF-8', + 'Turkish': 'tr_TR.UTF-8', + 'Ukrainian': 'uk_UA.UTF-8', + 'Urdu/Pakistan': 'ur_PK', + 'Uzbek/Uzbekistan': 'uz_UZ', + 'Walloon/Belgium': 'wa_BE@euro', + 'Welsh/Great_Britain': 'cy_GB.UTF-8', + 'Xhosa/South_Africa': 'xh_ZA.UTF-8', + 'Yoruba/Nigeria': 'yo_NG.UTF-8', + 'Zulu/South_Africa': 'zu_ZA.UTF-8' + } + + +def _initialize(): + timezones = _read_zonetab() + + j=0 + for timezone in timezones: + set_timezone.__doc__ += timezone+', ' + j+=1 + if j%3 == 0: + set_timezone.__doc__ += '\n' + + keys = _LANGUAGES.keys() + keys.sort() + i = 0 + for key in keys: + set_language.__doc__ += key+', ' + i+=1 + if i%3 == 0: + set_language.__doc__ += '\n' + +def _note_restart(): + print _('To apply your changes you have to restart sugar.\n' + + 'Hit at the same time ctrl+alt+erase on the keyboard to do this.') + +def get_jabber(): + pro = profile.get_profile() + return pro.jabber_server + +def print_jabber(): + print get_jabber() + +def set_jabber(server): + """Set the jabber server + server : e.g. 'olpc.collabora.co.uk' + """ + pro = profile.get_profile() + pro.jabber_server = server + pro.jabber_registered = False + pro.save() + _note_restart() + +def get_color(): + return profile.get_color() + +def print_color(): + color = get_color().to_string() + str = color.split(',') + + stroke = None + fill = None + for color in _COLORS: + for hue in _COLORS[color]: + if _COLORS[color][hue] == str[0]: + stroke = (color, hue) + if _COLORS[color][hue] == str[1]: + fill = (color, hue) + + if stroke is not None: + print 'stroke: color=%s hue=%s'%(stroke[0], stroke[1]) + else: + print 'stroke: %s'%(str[0]) + if fill is not None: + print 'fill: color=%s hue=%s'%(fill[0], fill[1]) + else: + print 'fill: %s'%(str[1]) + +def set_color(stroke, fill, modstroke='medium', modfill='medium'): + """Set the system color by setting a fill and stroke color. + fill : [red, orange, yellow, blue, purple] + stroke : [red, orange, yellow, blue, purple] + hue stroke : [dark, medium, light] (optional) + hue fill : [dark, medium, light] (optional) + """ + + if modstroke not in _MODIFIERS or modfill not in _MODIFIERS: + print (_("Error in specified color modifiers.")) + return + if stroke not in _COLORS or fill not in _COLORS: + print (_("Error in specified colors.")) + return + + if modstroke == modfill: + if modfill == 'medium': + modfill = 'light' + else: + modfill = 'medium' + + color = _COLORS[stroke][modstroke] + ',' + _COLORS[fill][modfill] + pro = profile.get_profile() + pro.color = XoColor(color) + pro.save() + _note_restart() + +def get_nick(): + return profile.get_nick_name() + +def print_nick(): + print get_nick() + +def set_nick(nick): + """Set the nickname. + nick : e.g. 'walter' + """ + pro = profile.get_profile() + pro.nick_name = nick + pro.save() + _note_restart() + +def get_radio(): + bus = dbus.SystemBus() + proxy = bus.get_object(NM_SERVICE_NAME, NM_SERVICE_PATH) + nm = dbus.Interface(proxy, NM_SERVICE_IFACE) + state = nm.getWirelessEnabled() + if state == 0: + return _('off') + elif state == 1: + return _('on') + else: + return _('State is unknown.') + +def print_radio(): + print get_radio() + +def set_radio(state): + """Turn Radio 'on' or 'off' + state : 'on/off' + """ + if state == 'on': + bus = dbus.SystemBus() + proxy = bus.get_object(NM_SERVICE_NAME, NM_SERVICE_PATH) + nm = dbus.Interface(proxy, NM_SERVICE_IFACE) + nm.setWirelessEnabled(True) + elif state == 'off': + bus = dbus.SystemBus() + proxy = bus.get_object(NM_SERVICE_NAME, NM_SERVICE_PATH) + nm = dbus.Interface(proxy, NM_SERVICE_IFACE) + nm.setWirelessEnabled(False) + else: + print (_("Error in specified radio argument use on/off.")) + +def _check_for_superuser(): + if os.getuid(): + print _("Permission denied. You need to be root to run this method.") + return False + return True + +def get_timezone(): + if not os.access(_TIMEZONE_CONFIG, os.R_OK): + # this is what the default is for the /etc/localtime + return "America/New_York" + fd = open(_TIMEZONE_CONFIG, "r") + lines = fd.readlines() + fd.close() + try: + for line in lines: + line = string.strip(line) + if len (line) and line[0] == '#': + continue + try: + tokens = string.split(line, "=") + if tokens[0] == "ZONE": + timezone = string.replace(tokens[1], '"', '') + return timezone + except Exception, e: + print "get_timezone: %s" % e + except Exception, e: + print "get_timezone: %s" % e + return None + +def print_timezone(): + timezone = get_timezone() + if timezone is None: + print (_("Error in reading timezone")) + else: + print timezone + +def _read_zonetab(fn='/usr/share/zoneinfo/zone.tab'): + fd = open (fn, 'r') + lines = fd.readlines() + fd.close() + timezones = [] + for line in lines: + if line.startswith('#'): + continue + line = line.split() + if len(line) > 1: + timezones.append(line[2]) + timezones.sort() + return timezones + +def set_timezone(timezone): + """Set the system timezone + timezone : + """ + if not _check_for_superuser(): + return + + timezones = _read_zonetab() + if timezone in timezones: + fromfile = os.path.join("/usr/share/zoneinfo/", timezone) + try: + shutil.copyfile(fromfile, "/etc/localtime") + except OSError, (errno, msg): + print (_("Error copying timezone (from %s): %s") % (fromfile, msg)) + return + try: + os.chmod("/etc/localtime", 0644) + except OSError, (errno, msg): + print (_("Changing permission of timezone: %s") % (msg)) + return + + # Write info to the /etc/sysconfig/clock file + fd = open(_TIMEZONE_CONFIG, "w") + fd.write('# use sugar-control-panel to change this\n') + fd.write('ZONE="%s"\n' % timezone) + fd.write('UTC=true\n') + fd.close() + else: + print (_("Error timezone does not exist.")) + +def _writeI18N(lang): + path = os.path.join(os.environ.get("HOME"), '.i18n') + if os.access(path, os.W_OK) == 0: + print(_("Could not access %s. Create standard settings.") % path) + fd = open(path, 'w') + fd.write('LANG="en_US.UTF-8"\n') + fd.close() + else: + fd = open(path, 'r') + lines = fd.readlines() + fd.close() + for i in range(len(lines)): + if lines[i][:5] == "LANG=": + lines[i] = 'LANG="' + lang + '"\n' + fd = open(path, 'w') + fd.writelines(lines) + fd.close() + +def get_language(): + originalFile = None + path = os.path.join(os.environ.get("HOME"), '.i18n') + if os.access(path, os.R_OK) == 0: + print(_("Could not access %s. Create standard settings.") % path) + fd = open(path, 'w') + default = 'en_US.UTF-8' + fd.write('LANG="%s"\n'%default) + fd.close() + return default + + fd = open(path, "r") + lines = fd.readlines() + fd.close() + + lang = None + + for line in lines: + if line[:5] == "LANG=": + lang = line[5:].replace('"', '') + lang = lang.strip() + + return lang + +def print_language(): + code = get_language() + + for lang in _LANGUAGES: + if _LANGUAGES[lang] == code: + print lang + return + print (_("Language for code=%s could not be determined.") % code) + +def set_language(language): + """Set the system language. + languages : + """ + if language in _LANGUAGES: + _writeI18N(_LANGUAGES[language]) + _note_restart() + else: + print (_("Sorry I do not speak \'%s\'.") % language) + +# inilialize the docstrings for the timezone and language +_initialize() + diff --git a/src/hardware/Makefile.am b/src/hardware/Makefile.am new file mode 100644 index 0000000..8cd9c77 --- /dev/null +++ b/src/hardware/Makefile.am @@ -0,0 +1,13 @@ +sugardir = $(pkgdatadir)/shell/hardware +sugar_PYTHON = \ + __init__.py \ + hardwaremanager.py \ + keydialog.py \ + nmclient.py \ + nminfo.py \ + schoolserver.py + +dbusservicedir = $(sysconfdir)/dbus-1/system.d/ +dbusservice_DATA = NetworkManagerInfo.conf + +EXTRA_DIST = $(dbusservice_DATA) diff --git a/src/hardware/NetworkManagerInfo.conf b/src/hardware/NetworkManagerInfo.conf new file mode 100644 index 0000000..4fb8270 --- /dev/null +++ b/src/hardware/NetworkManagerInfo.conf @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + 512 + + diff --git a/src/hardware/__init__.py b/src/hardware/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/hardware/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 + diff --git a/src/hardware/hardwaremanager.py b/src/hardware/hardwaremanager.py new file mode 100644 index 0000000..4eeac03 --- /dev/null +++ b/src/hardware/hardwaremanager.py @@ -0,0 +1,143 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 logging + +import dbus +import gst +import gst.interfaces + +from hardware.nmclient import NMClient +from sugar.profile import get_profile +from sugar import env + +_HARDWARE_MANAGER_INTERFACE = 'org.laptop.HardwareManager' +_HARDWARE_MANAGER_SERVICE = 'org.laptop.HardwareManager' +_HARDWARE_MANAGER_OBJECT_PATH = '/org/laptop/HardwareManager' + +COLOR_MODE = 0 +B_AND_W_MODE = 1 + +class HardwareManager(object): + def __init__(self): + try: + bus = dbus.SystemBus() + proxy = bus.get_object(_HARDWARE_MANAGER_SERVICE, + _HARDWARE_MANAGER_OBJECT_PATH) + self._service = dbus.Interface(proxy, _HARDWARE_MANAGER_INTERFACE) + except dbus.DBusException, e: + self._service = None + logging.info('Hardware manager service not found.') + + self._mixer = gst.element_factory_make('alsamixer') + self._mixer.set_state(gst.STATE_PAUSED) + + self._master = None + for track in self._mixer.list_tracks(): + if track.flags & gst.interfaces.MIXER_TRACK_MASTER: + self._master = track + + def get_volume(self): + if not self._mixer or not self._master: + logging.error('Cannot get the volume') + return self._convert_volume(0) + + max_volume = self._master.max_volume + min_volume = self._master.min_volume + volume = self._mixer.get_volume(self._master)[0] + + return volume * 100.0 / (max_volume - min_volume) + min_volume + + def set_volume(self, volume): + if not self._mixer or not self._master: + logging.error('Cannot set the volume') + + if volume < 0 or volume > 100: + logging.error('Trying to set an invalid volume value.') + return + + max_volume = self._master.max_volume + min_volume = self._master.min_volume + + volume = volume * (max_volume - min_volume) / 100.0 + min_volume + volume_list = [ volume ] * self._master.num_channels + + self._mixer.set_volume(self._master, tuple(volume_list)) + + def set_mute(self, mute): + if not self._mixer or not self._master: + logging.error('Cannot mute the audio channel') + self._mixer.set_mute(self._master, mute) + + def startup(self): + if env.is_emulator() is False: + profile = get_profile() + self.set_volume(profile.sound_volume) + + def shutdown(self): + if env.is_emulator() is False: + profile = get_profile() + profile.sound_volume = self.get_volume() + profile.save() + + def set_dcon_freeze(self, frozen): + if not self._service: + return + + self._service.set_dcon_freeze(frozen) + + def set_display_mode(self, mode): + if not self._service: + return + + self._service.set_display_mode(mode) + + def set_display_brightness(self, level): + if not self._service: + logging.error('Cannot set display brightness') + return + + self._service.set_display_brightness(level) + + def get_display_brightness(self): + if not self._service: + logging.error('Cannot get display brightness') + return + + return self._service.get_display_brightness() + + def toggle_keyboard_brightness(self): + if not self._service: + return + + if self._service.get_keyboard_brightness(): + self._service.set_keyboard_brightness(False) + else: + self._service.set_keyboard_brightness(True) + +def get_manager(): + return _manager + +def get_network_manager(): + return _network_manager + +_manager = HardwareManager() + +try: + _network_manager = NMClient() +except dbus.DBusException, e: + _network_manager = None + logging.info('Network manager service not found.') diff --git a/src/hardware/keydialog.py b/src/hardware/keydialog.py new file mode 100644 index 0000000..d336ab9 --- /dev/null +++ b/src/hardware/keydialog.py @@ -0,0 +1,351 @@ +# vi: ts=4 ai noet +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 md5 +from gettext import gettext as _ + +import gobject, gtk + +IW_AUTH_ALG_OPEN_SYSTEM = 0x00000001 +IW_AUTH_ALG_SHARED_KEY = 0x00000002 + +IW_AUTH_WPA_VERSION_DISABLED = 0x00000001 +IW_AUTH_WPA_VERSION_WPA = 0x00000002 +IW_AUTH_WPA_VERSION_WPA2 = 0x00000004 + +NM_802_11_CAP_NONE = 0x00000000 +NM_802_11_CAP_PROTO_NONE = 0x00000001 +NM_802_11_CAP_PROTO_WEP = 0x00000002 +NM_802_11_CAP_PROTO_WPA = 0x00000004 +NM_802_11_CAP_PROTO_WPA2 = 0x00000008 +NM_802_11_CAP_KEY_MGMT_PSK = 0x00000040 +NM_802_11_CAP_KEY_MGMT_802_1X = 0x00000080 +NM_802_11_CAP_CIPHER_WEP40 = 0x00001000 +NM_802_11_CAP_CIPHER_WEP104 = 0x00002000 +NM_802_11_CAP_CIPHER_TKIP = 0x00004000 +NM_802_11_CAP_CIPHER_CCMP = 0x00008000 + +NM_AUTH_TYPE_WPA_PSK_AUTO = 0x00000000 +IW_AUTH_CIPHER_NONE = 0x00000001 +IW_AUTH_CIPHER_WEP40 = 0x00000002 +IW_AUTH_CIPHER_TKIP = 0x00000004 +IW_AUTH_CIPHER_CCMP = 0x00000008 +IW_AUTH_CIPHER_WEP104 = 0x00000010 + +IW_AUTH_KEY_MGMT_802_1X = 0x1 +IW_AUTH_KEY_MGMT_PSK = 0x2 + +def string_is_hex(key): + is_hex = True + for c in key: + if not 'a' <= c.lower() <= 'f' and not '0' <= c <= '9': + is_hex = False + return is_hex + +def string_is_ascii(string): + try: + string.encode('ascii') + return True + except: + return False + +def string_to_hex(passphrase): + key = '' + for c in passphrase: + key += '%02x' % ord(c) + return key + +def hash_passphrase(passphrase): + # passphrase must have a length of 64 + if len(passphrase) > 64: + passphrase = passphrase[:64] + elif len(passphrase) < 64: + while len(passphrase) < 64: + passphrase += passphrase[:64 - len(passphrase)] + passphrase = md5.new(passphrase).digest() + return string_to_hex(passphrase)[:26] + +class KeyDialog(gtk.Dialog): + def __init__(self, net, async_cb, async_err_cb): + gtk.Dialog.__init__(self, flags=gtk.DIALOG_MODAL) + self.set_title("Wireless Key Required") + + self._net = net + self._async_cb = async_cb + self._async_err_cb = async_err_cb + + self.set_has_separator(False) + + label = gtk.Label("A wireless encryption key is required for\n" \ + " the wireless network '%s'." % net.get_ssid()) + self.vbox.pack_start(label) + + self.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK) + self.set_default_response(gtk.RESPONSE_OK) + self.set_has_separator(True) + + def add_key_entry(self): + self._entry = gtk.Entry() + #self._entry.props.visibility = False + self._entry.connect('changed', self._update_response_sensitivity) + self._entry.connect('activate', self._entry_activate_cb) + self.vbox.pack_start(self._entry) + self.vbox.set_spacing(6) + self.vbox.show_all() + + self._update_response_sensitivity() + self._entry.grab_focus() + + def _entry_activate_cb(self, entry): + self.response(gtk.RESPONSE_OK) + + def create_security(self): + raise NotImplementedError + + def get_network(self): + return self._net + + def get_callbacks(self): + return (self._async_cb, self._async_err_cb) + +WEP_PASSPHRASE = 1 +WEP_HEX = 2 +WEP_ASCII = 3 + +class WEPKeyDialog(KeyDialog): + def __init__(self, net, async_cb, async_err_cb): + KeyDialog.__init__(self, net, async_cb, async_err_cb) + + # WEP key type + self.key_store = gtk.ListStore(str, int) + self.key_store.append(["Passphrase (128-bit)", WEP_PASSPHRASE]) + self.key_store.append(["Hex (40/128-bit)", WEP_HEX]) + self.key_store.append(["ASCII (40/128-bit)", WEP_ASCII]) + + self.key_combo = gtk.ComboBox(self.key_store) + cell = gtk.CellRendererText() + self.key_combo.pack_start(cell, True) + self.key_combo.add_attribute(cell, 'text', 0) + self.key_combo.set_active(0) + self.key_combo.connect('changed', self._key_combo_changed_cb) + + hbox = gtk.HBox() + hbox.pack_start(gtk.Label(_("Key Type:"))) + hbox.pack_start(self.key_combo) + hbox.show_all() + self.vbox.pack_start(hbox) + + # Key entry field + self.add_key_entry() + + # WEP authentication mode + self.auth_store = gtk.ListStore(str, int) + self.auth_store.append(["Open System", IW_AUTH_ALG_OPEN_SYSTEM]) + self.auth_store.append(["Shared Key", IW_AUTH_ALG_SHARED_KEY]) + + self.auth_combo = gtk.ComboBox(self.auth_store) + cell = gtk.CellRendererText() + self.auth_combo.pack_start(cell, True) + self.auth_combo.add_attribute(cell, 'text', 0) + self.auth_combo.set_active(0) + + hbox = gtk.HBox() + hbox.pack_start(gtk.Label(_("Authentication Type:"))) + hbox.pack_start(self.auth_combo) + hbox.show_all() + + self.vbox.pack_start(hbox) + + def _key_combo_changed_cb(self, widget): + self._update_response_sensitivity() + + def _get_security(self): + key = self._entry.get_text() + + it = self.key_combo.get_active_iter() + (key_type, ) = self.key_store.get(it, 1) + + if key_type == WEP_PASSPHRASE: + key = hash_passphrase(key) + elif key_type == WEP_ASCII: + key = string_to_hex(key) + + it = self.auth_combo.get_active_iter() + (auth_alg, ) = self.auth_store.get(it, 1) + + we_cipher = None + if len(key) == 26: + we_cipher = IW_AUTH_CIPHER_WEP104 + elif len(key) == 10: + we_cipher = IW_AUTH_CIPHER_WEP40 + + return (we_cipher, key, auth_alg) + + def print_security(self): + (we_cipher, key, auth_alg) = self._get_security() + print "Cipher: %d" % we_cipher + print "Key: %s" % key + print "Auth: %d" % auth_alg + + def create_security(self): + (we_cipher, key, auth_alg) = self._get_security() + from nminfo import Security + return Security.new_from_args(we_cipher, (key, auth_alg)) + + def _update_response_sensitivity(self, ignored=None): + key = self._entry.get_text() + it = self.key_combo.get_active_iter() + (key_type, ) = self.key_store.get(it, 1) + + valid = False + if key_type == WEP_PASSPHRASE: + # As the md5 passphrase can be of any length and has no indicator, + # we cannot check for the validity of the input. + if len(key) > 0: + valid = True + elif key_type == WEP_ASCII: + if len(key) == 5 or len(key) == 13: + valid = string_is_ascii(key) + elif key_type == WEP_HEX: + if len(key) == 10 or len(key) == 26: + valid = string_is_hex(key) + + self.set_response_sensitive(gtk.RESPONSE_OK, valid) + +class WPAKeyDialog(KeyDialog): + def __init__(self, net, async_cb, async_err_cb): + KeyDialog.__init__(self, net, async_cb, async_err_cb) + self.add_key_entry() + + self.store = gtk.ListStore(str, int) + self.store.append(["Automatic", NM_AUTH_TYPE_WPA_PSK_AUTO]) + if net.get_caps() & NM_802_11_CAP_CIPHER_CCMP: + self.store.append(["AES-CCMP", IW_AUTH_CIPHER_CCMP]) + if net.get_caps() & NM_802_11_CAP_CIPHER_TKIP: + self.store.append(["TKIP", IW_AUTH_CIPHER_TKIP]) + + self.combo = gtk.ComboBox(self.store) + cell = gtk.CellRendererText() + self.combo.pack_start(cell, True) + self.combo.add_attribute(cell, 'text', 0) + self.combo.set_active(0) + + self.hbox = gtk.HBox() + self.hbox.pack_start(gtk.Label(_("Encryption Type:"))) + self.hbox.pack_start(self.combo) + self.hbox.show_all() + + self.vbox.pack_start(self.hbox) + + def _get_security(self): + ssid = self.get_network().get_ssid() + key = self._entry.get_text() + is_hex = string_is_hex(key) + + real_key = None + if len(key) == 64 and is_hex: + # Hex key + real_key = key + elif len(key) >= 8 and len(key) <= 63: + # passphrase + import commands + (s, o) = commands.getstatusoutput("/usr/sbin/wpa_passphrase '%s' '%s'" % (ssid, key)) + if s != 0: + raise RuntimeError("Error hashing passphrase: %s" % o) + lines = o.split("\n") + for line in lines: + if line.strip().startswith("psk="): + real_key = line.strip()[4:] + if real_key and len(real_key) != 64: + real_key = None + + if not real_key: + raise RuntimeError("Invalid key") + + it = self.combo.get_active_iter() + (we_cipher, ) = self.store.get(it, 1) + + wpa_ver = IW_AUTH_WPA_VERSION_WPA + caps = self.get_network().get_caps() + if caps & NM_802_11_CAP_PROTO_WPA2: + wpa_ver = IW_AUTH_WPA_VERSION_WPA2 + + return (we_cipher, real_key, wpa_ver) + + def print_security(self): + (we_cipher, key, wpa_ver) = self._get_security() + print "Cipher: %d" % we_cipher + print "Key: %s" % key + print "WPA Ver: %d" % wpa_ver + + def create_security(self): + (we_cipher, key, wpa_ver) = self._get_security() + from nminfo import Security + return Security.new_from_args(we_cipher, (key, wpa_ver, IW_AUTH_KEY_MGMT_PSK)) + + def _update_response_sensitivity(self, ignored=None): + key = self._entry.get_text() + is_hex = string_is_hex(key) + + valid = False + if len(key) == 64 and is_hex: + # hex key + valid = True + elif len(key) >= 8 and len(key) <= 63: + # passphrase + valid = True + self.set_response_sensitive(gtk.RESPONSE_OK, valid) + return False + +def new_key_dialog(net, async_cb, async_err_cb): + caps = net.get_caps() + if (caps & NM_802_11_CAP_CIPHER_TKIP or caps & NM_802_11_CAP_CIPHER_CCMP) and \ + (caps & NM_802_11_CAP_PROTO_WPA or caps & NM_802_11_CAP_PROTO_WPA2): + return WPAKeyDialog(net, async_cb, async_err_cb) + elif (caps & NM_802_11_CAP_CIPHER_WEP40 or caps & NM_802_11_CAP_CIPHER_WEP104) and \ + (caps & NM_802_11_CAP_PROTO_WEP): + return WEPKeyDialog(net, async_cb, async_err_cb) + else: + raise RuntimeError("Unhandled network capabilities %x" % caps) + + + +class FakeNet(object): + def get_ssid(self): + return "olpcwpa" + + def get_caps(self): +# return NM_802_11_CAP_CIPHER_WEP104 | NM_802_11_CAP_PROTO_WEP + return NM_802_11_CAP_CIPHER_CCMP | NM_802_11_CAP_CIPHER_TKIP | NM_802_11_CAP_PROTO_WPA + +def response_cb(widget, response_id): + if response_id == gtk.RESPONSE_OK: + print dialog.print_security() + else: + print "canceled" + widget.hide() + widget.destroy() + + +if __name__ == "__main__": + net = FakeNet() + dialog = new_key_dialog(net, None, None) + dialog.connect("response", response_cb) + dialog.run() + diff --git a/src/hardware/nmclient.py b/src/hardware/nmclient.py new file mode 100644 index 0000000..d23a206 --- /dev/null +++ b/src/hardware/nmclient.py @@ -0,0 +1,721 @@ +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 logging +import os + +import dbus +import dbus.glib +import dbus.decorators +import gobject +import gtk + +from hardware import nminfo +from sugar.graphics import xocolor + +IW_AUTH_ALG_OPEN_SYSTEM = 0x00000001 +IW_AUTH_ALG_SHARED_KEY = 0x00000002 + +NM_DEVICE_STAGE_STRINGS=("Unknown", + "Prepare", + "Config", + "Need Users Key", + "IP Config", + "IP Config Get", + "IP Config Commit", + "Activated", + "Failed", + "Canceled" +) + +NM_SERVICE = 'org.freedesktop.NetworkManager' +NM_IFACE = 'org.freedesktop.NetworkManager' +NM_IFACE_DEVICES = 'org.freedesktop.NetworkManager.Devices' +NM_PATH = '/org/freedesktop/NetworkManager' + +DEVICE_TYPE_UNKNOWN = 0 +DEVICE_TYPE_802_3_ETHERNET = 1 +DEVICE_TYPE_802_11_WIRELESS = 2 +DEVICE_TYPE_802_11_MESH_OLPC = 3 + +NM_DEVICE_CAP_NONE = 0x00000000 +NM_DEVICE_CAP_NM_SUPPORTED = 0x00000001 +NM_DEVICE_CAP_CARRIER_DETECT = 0x00000002 +NM_DEVICE_CAP_WIRELESS_SCAN = 0x00000004 + +sys_bus = dbus.SystemBus() + +NM_802_11_CAP_NONE = 0x00000000 +NM_802_11_CAP_PROTO_NONE = 0x00000001 +NM_802_11_CAP_PROTO_WEP = 0x00000002 +NM_802_11_CAP_PROTO_WPA = 0x00000004 +NM_802_11_CAP_PROTO_WPA2 = 0x00000008 +NM_802_11_CAP_KEY_MGMT_PSK = 0x00000040 +NM_802_11_CAP_KEY_MGMT_802_1X = 0x00000080 +NM_802_11_CAP_CIPHER_WEP40 = 0x00001000 +NM_802_11_CAP_CIPHER_WEP104 = 0x00002000 +NM_802_11_CAP_CIPHER_TKIP = 0x00004000 +NM_802_11_CAP_CIPHER_CCMP = 0x00008000 + +NETWORK_STATE_CONNECTING = 0 +NETWORK_STATE_CONNECTED = 1 +NETWORK_STATE_NOTCONNECTED = 2 + +DEVICE_STATE_ACTIVATING = 0 +DEVICE_STATE_ACTIVATED = 1 +DEVICE_STATE_INACTIVE = 2 + +IW_MODE_ADHOC = 1 +IW_MODE_INFRA = 2 + +class Network(gobject.GObject): + __gsignals__ = { + 'initialized' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_BOOLEAN])), + 'strength-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'state-changed' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self, client, op): + gobject.GObject.__init__(self) + self._client = client + self._op = op + self._ssid = None + self._mode = None + self._strength = 0 + self._caps = 0 + self._valid = False + self._favorite = False + self._state = NETWORK_STATE_NOTCONNECTED + + obj = sys_bus.get_object(NM_SERVICE, self._op) + net = dbus.Interface(obj, NM_IFACE_DEVICES) + net.getProperties(reply_handler=self._update_reply_cb, + error_handler=self._update_error_cb) + + def _update_reply_cb(self, *props): + self._ssid = props[1] + self._strength = props[3] + self._mode = props[6] + self._caps = props[7] + if self._caps & NM_802_11_CAP_PROTO_WPA or self._caps & NM_802_11_CAP_PROTO_WPA2: + if not (self._caps & NM_802_11_CAP_KEY_MGMT_PSK): + # 802.1x is not supported at this time + logging.debug("Net(%s): ssid '%s' dropping because 802.1x is unsupported" % (self._op, + self._ssid)) + self._valid = False + self.emit('initialized', self._valid) + return + if self._mode != IW_MODE_INFRA: + # Don't show Ad-Hoc networks; they usually don't DHCP and therefore + # won't work well here. This also works around the bug where we show + # our own mesh SSID on the Mesh view when in mesh mode + logging.debug("Net(%s): ssid '%s' is adhoc; not showing" % (self._op, + self._ssid)) + self._valid = False + self.emit('initialized', self._valid) + return + + fav_nets = [] + if self._client.nminfo: + fav_nets = self._client.nminfo.get_networks(nminfo.NETWORK_TYPE_ALLOWED) + if self._ssid in fav_nets: + self._favorite = True + + self._valid = True + logging.debug("Net(%s): caps 0x%X" % (self._ssid, self._caps)) + self.emit('initialized', self._valid) + + def _update_error_cb(self, err): + logging.debug("Net(%s): failed to update. (%s)" % (self._op, err)) + self._valid = False + self.emit('initialized', self._valid) + + def get_colors(self): + import sha + sh = sha.new() + data = self._ssid + hex(self._caps) + hex(self._mode) + sh.update(data) + h = hash(sh.digest()) + idx = h % len(xocolor._colors) + # stroke, fill + return (xocolor._colors[idx][0], xocolor._colors[idx][1]) + + def get_ssid(self): + return self._ssid + + def get_caps(self): + return self._caps + + def get_mode(self): + return self._mode + + def get_state(self): + return self._state + + def set_state(self, state): + if state == self._state: + return + self._state = state + if self._valid: + self.emit('state-changed') + + def get_op(self): + return self._op + + def get_strength(self): + return self._strength + + def set_strength(self, strength): + if strength == self._strength: + return + self._strength = strength + if self._valid: + self.emit('strength-changed') + + def is_valid(self): + return self._valid + + def is_favorite(self): + return self._favorite + +class Device(gobject.GObject): + __gsignals__ = { + 'initialized': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'init-failed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'ssid-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'strength-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'state-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'activation-stage-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'network-appeared': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'network-disappeared': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self, client, op): + gobject.GObject.__init__(self) + self._client = client + self._op = op + self._iface = None + self._type = DEVICE_TYPE_UNKNOWN + self._udi = None + self._active = False + self._act_stage = 0 + self._strength = 0 + self._freq = 0.0 + self._link = False + self._valid = False + self._networks = {} + self._caps = 0 + self._state = DEVICE_STATE_INACTIVE + self._active_network = None + self._active_net_sigid = 0 + + obj = sys_bus.get_object(NM_SERVICE, self._op) + self.dev = dbus.Interface(obj, NM_IFACE_DEVICES) + self.dev.getProperties(reply_handler=self._update_reply_cb, + error_handler=self._update_error_cb) + + def _is_activating(self): + if self._active and self._act_stage >= 1 and self._act_stage <= 6: + return True + return False + + def _is_activated(self): + if self._active and self._act_stage == 7: + return True + return False + + def _update_reply_cb(self, *props): + self._iface = props[1] + self._type = props[2] + self._udi = props[3] + self._active = props[4] + self._act_stage = props[5] + self._link = props[15] + self._caps = props[17] + + if self._type == DEVICE_TYPE_802_11_WIRELESS: + old_strength = self._strength + self._strength = props[14] + if self._strength != old_strength: + if self._valid: + self.emit('strength-changed') + self._update_networks(props[20], props[19]) + elif self._type == DEVICE_TYPE_802_11_MESH_OLPC: + old_strength = self._strength + self._strength = props[14] + if self._strength != old_strength: + if self._valid: + self.emit('strength-changed') + + self._valid = True + + if self._is_activating(): + self.set_state(DEVICE_STATE_ACTIVATING) + elif self._is_activated(): + self.set_state(DEVICE_STATE_ACTIVATED) + else: + self.set_state(DEVICE_STATE_INACTIVE) + + self.emit('initialized') + + def _update_networks(self, net_ops, active_op): + for op in net_ops: + net = Network(self._client, op) + self._networks[op] = net + net.connect('initialized', lambda *args: self._net_initialized_cb(active_op, *args)) + + def _update_error_cb(self, err): + logging.debug("Device(%s): failed to update. (%s)" % (self._op, err)) + self._valid = False + self.emit('init-failed') + + def _net_initialized_cb(self, active_op, net, valid): + net_op = net.get_op() + if not self._networks.has_key(net_op): + return + + if not valid: + # init failure + del self._networks[net_op] + return + + # init success + if self._valid: + self.emit('network-appeared', net) + if active_op and net_op == active_op: + self.set_active_network(net) + + def get_op(self): + return self._op + + def get_networks(self): + ret = [] + for net in self._networks.values(): + if net.is_valid(): + ret.append(net) + return ret + + def get_network(self, op): + if self._networks.has_key(op) and self._networks[op].is_valid(): + return self._networks[op] + return None + + def get_network_ops(self): + ret = [] + for net in self._networks.values(): + if net.is_valid(): + ret.append(net.get_op()) + return ret + + def get_mesh_step(self): + if self._type != DEVICE_TYPE_802_11_MESH_OLPC: + raise RuntimeError("Only valid for mesh devices") + try: + step = self.dev.getMeshStep(timeout=3) + except dbus.DBusException, e: + step = 0 + return step + + def get_frequency(self): + freq = 0.0 + try: + freq = self.dev.getFrequency(timeout=3) + except dbus.DBusException, e: + pass + # Hz -> GHz + self._freq = freq / 1000000000.0 + return self._freq + + def get_strength(self): + return self._strength + + def set_strength(self, strength): + if strength == self._strength: + return False + + if strength >= 0 and strength <= 100: + self._strength = strength + else: + self._strength = 0 + + if self._valid: + self.emit('strength-changed') + + def network_appeared(self, network): + if self._networks.has_key(network): + return + net = Network(self._client, network) + self._networks[network] = net + net.connect('initialized', lambda *args: self._net_initialized_cb(None, *args)) + + def network_disappeared(self, network): + if not self._networks.has_key(network): + return + + if self._valid: + self.emit('network-disappeared', self._networks[network]) + + del self._networks[network] + + def set_active_network(self, network): + if self._active_network == network: + return + + # Make sure the old one doesn't get a stuck state + if self._active_network: + self._active_network.set_state(NETWORK_STATE_NOTCONNECTED) + self._active_network.disconnect(self._active_net_sigid) + + self._active_network = network + + if self._active_network: + self._active_net_sigid = self._active_network.connect("initialized", + self._active_net_initialized); + + # don't emit ssid-changed for networks that are not yet valid + if self._valid: + if self._active_network and self._active_network.is_valid(): + self.emit('ssid-changed') + elif not self._active_network: + self.emit('ssid-changed') + + def _active_net_initialized(self, net, user_data=None): + if self._active_network and self._active_network.is_valid(): + self.emit('ssid-changed') + + def _get_active_net_cb(self, state, net_op): + if not self._networks.has_key(net_op): + self.set_active_network(None) + return + + self.set_active_network(self._networks[net_op]) + + _device_to_network_state = { + DEVICE_STATE_ACTIVATING : NETWORK_STATE_CONNECTING, + DEVICE_STATE_ACTIVATED : NETWORK_STATE_CONNECTED, + DEVICE_STATE_INACTIVE : NETWORK_STATE_NOTCONNECTED + } + + network_state = _device_to_network_state[state] + self._active_network.set_state(network_state) + + def _get_active_net_error_cb(self, err): + logging.debug("Couldn't get active network: %s" % err) + self.set_active_network(None) + + def get_state(self): + return self._state + + def set_state(self, state): + if state == self._state: + return + + if state == DEVICE_STATE_INACTIVE: + self._act_stage = 0 + + self._state = state + if self._valid: + self.emit('state-changed') + + if self._type == DEVICE_TYPE_802_11_WIRELESS: + if state == DEVICE_STATE_INACTIVE: + self.set_active_network(None) + else: + self.dev.getActiveNetwork(reply_handler=lambda *args: self._get_active_net_cb(state, *args), + error_handler=self._get_active_net_error_cb) + + def set_activation_stage(self, stage): + if stage == self._act_stage: + return + self._act_stage = stage + if self._valid: + self.emit('activation-stage-changed') + + def get_activation_stage(self): + return self._act_stage + + def get_ssid(self): + if self._active_network and self._active_network.is_valid(): + return self._active_network.get_ssid() + elif not self._active_network: + return None + + def get_active_network(self): + return self._active_network + + def get_type(self): + return self._type + + def is_valid(self): + return self._valid + + def set_carrier(self, on): + self._link = on + + def get_capabilities(self): + return self._caps + +class NMClient(gobject.GObject): + __gsignals__ = { + 'device-added' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'device-activated' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'device-activating': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'device-removed' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self.nminfo = None + self._nm_present = False + self._update_timer = 0 + self._devices = {} + + try: + self.nminfo = nminfo.NMInfo(self) + except RuntimeError: + pass + self._setup_dbus() + if self._nm_present: + self._get_initial_devices() + + def get_devices(self): + return self._devices.values() + + def _get_initial_devices_reply_cb(self, ops): + for op in ops: + self._add_device(op) + + def _dev_initialized_cb(self, dev): + self.emit('device-added', dev) + + def _dev_init_failed_cb(self, dev): + # Device failed to initialize, likely due to dbus errors or something + op = dev.get_op() + self._remove_device(op) + + def _get_initial_devices_error_cb(self, err): + logging.debug("Error updating devices (%s)" % err) + + def _get_initial_devices(self): + self._nm_obj.getDevices(reply_handler=self._get_initial_devices_reply_cb, \ + error_handler=self._get_initial_devices_error_cb) + + def _add_device(self, dev_op): + if self._devices.has_key(dev_op): + return + dev = Device(self, dev_op) + self._devices[dev_op] = dev + dev.connect('init-failed', self._dev_init_failed_cb) + dev.connect('initialized', self._dev_initialized_cb) + dev.connect('state-changed', self._dev_state_changed_cb) + + def _remove_device(self, dev_op): + if not self._devices.has_key(dev_op): + return + dev = self._devices[dev_op] + if dev.is_valid(): + self.emit('device-removed', dev) + del self._devices[dev_op] + + def _dev_state_changed_cb(self, dev): + op = dev.get_op() + if not self._devices.has_key(op) or not dev.is_valid(): + return + if dev.get_state() == DEVICE_STATE_ACTIVATING: + self.emit('device-activating', dev) + elif dev.get_state() == DEVICE_STATE_ACTIVATED: + self.emit('device-activated', dev) + + def get_device(self, dev_op): + if not self._devices.has_key(dev_op): + return None + return self._devices[dev_op] + + def _setup_dbus(self): + self._sig_handlers = { + 'StateChange': self.state_changed_sig_handler, + 'DeviceAdded': self.device_added_sig_handler, + 'DeviceRemoved': self.device_removed_sig_handler, + 'DeviceActivationStage': self.device_activation_stage_sig_handler, + 'DeviceActivating': self.device_activating_sig_handler, + 'DeviceNowActive': self.device_now_active_sig_handler, + 'DeviceNoLongerActive': self.device_no_longer_active_sig_handler, + 'DeviceActivationFailed': self.device_activation_failed_sig_handler, + 'DeviceCarrierOn': self.device_carrier_on_sig_handler, + 'DeviceCarrierOff': self.device_carrier_off_sig_handler, + 'DeviceStrengthChanged': self.wireless_device_strength_changed_sig_handler, + 'WirelessNetworkAppeared': self.wireless_network_appeared_sig_handler, + 'WirelessNetworkDisappeared': self.wireless_network_disappeared_sig_handler, + 'WirelessNetworkStrengthChanged': self.wireless_network_strength_changed_sig_handler + } + + try: + self._nm_proxy = sys_bus.get_object(NM_SERVICE, NM_PATH) + self._nm_obj = dbus.Interface(self._nm_proxy, NM_IFACE) + except dbus.DBusException, e: + logging.debug("Could not connect to NetworkManager!") + self._nm_present = False + return + + sys_bus.add_signal_receiver(self.name_owner_changed_sig_handler, + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus") + + for (signal, handler) in self._sig_handlers.items(): + sys_bus.add_signal_receiver(handler, signal_name=signal, dbus_interface=NM_IFACE) + + # Find out whether or not NMI is running + try: + bus_object = sys_bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') + name = bus_object.GetNameOwner("org.freedesktop.NetworkManagerInfo", \ + dbus_interface='org.freedesktop.DBus') + if name: + self._nm_present = True + except dbus.DBusException: + pass + + def set_active_device(self, device, network=None, mesh_freq=None, mesh_start=None): + ssid = "" + if network: + ssid = network.get_ssid() + if device.get_type() == DEVICE_TYPE_802_11_MESH_OLPC: + if mesh_freq or mesh_start: + if mesh_freq and not mesh_start: + self._nm_obj.setActiveDevice(device.get_op(), dbus.Double(mesh_freq)) + elif mesh_start and not mesh_freq: + self._nm_obj.setActiveDevice(device.get_op(), dbus.Double(0.0), dbus.UInt32(mesh_start)) + else: + self._nm_obj.setActiveDevice(device.get_op(), dbus.Double(mesh_freq), dbus.UInt32(mesh_start)) + else: + self._nm_obj.setActiveDevice(device.get_op()) + else: + self._nm_obj.setActiveDevice(device.get_op(), ssid) + + def state_changed_sig_handler(self, new_state): + logging.debug('NM State Changed to %d' % new_state) + + def device_activation_stage_sig_handler(self, device, stage): + logging.debug('Device Activation Stage "%s" for device %s' % (NM_DEVICE_STAGE_STRINGS[stage], device)) + if not self._devices.has_key(device): + logging.debug('DeviceActivationStage, device %s does not exist' % (device)) + return + self._devices[device].set_activation_stage(stage) + + def device_activating_sig_handler(self, device): + logging.debug('DeviceActivating for %s' % (device)) + if not self._devices.has_key(device): + logging.debug('DeviceActivating, device %s does not exist' % (device)) + return + self._devices[device].set_state(DEVICE_STATE_ACTIVATING) + + def device_now_active_sig_handler(self, device, ssid=None): + logging.debug('DeviceNowActive for %s' % (device)) + if not self._devices.has_key(device): + logging.debug('DeviceNowActive, device %s does not exist' % (device)) + return + self._devices[device].set_state(DEVICE_STATE_ACTIVATED) + + def device_no_longer_active_sig_handler(self, device): + logging.debug('DeviceNoLongerActive for %s' % (device)) + if not self._devices.has_key(device): + logging.debug('DeviceNoLongerActive, device %s does not exist' % (device)) + return + self._devices[device].set_state(DEVICE_STATE_INACTIVE) + + def device_activation_failed_sig_handler(self, device, ssid=None): + logging.debug('DeviceActivationFailed for %s' % (device)) + if not self._devices.has_key(device): + logging.debug('DeviceActivationFailed, device %s does not exist' % (device)) + return + self._devices[device].set_state(DEVICE_STATE_INACTIVE) + + def name_owner_changed_sig_handler(self, name, old, new): + if name != NM_SERVICE: + return + if (old and len(old)) and (not new and not len(new)): + # NM went away + self._nm_present = False + devs = self._devices.keys() + for op in devs: + self._remove_device(op) + self._devices = {} + elif (not old and not len(old)) and (new and len(new)): + # NM started up + self._nm_present = True + self._get_initial_devices() + + def device_added_sig_handler(self, device): + logging.debug('DeviceAdded for %s' % (device)) + self._add_device(device) + + def device_removed_sig_handler(self, device): + logging.debug('DeviceRemoved for %s' % (device)) + self._remove_device(device) + + def wireless_network_appeared_sig_handler(self, device, network): + if not self._devices.has_key(device): + return + self._devices[device].network_appeared(network) + + def wireless_network_disappeared_sig_handler(self, device, network): + if not self._devices.has_key(device): + return + self._devices[device].network_disappeared(network) + + def wireless_device_strength_changed_sig_handler(self, device, strength): + if not self._devices.has_key(device): + return + self._devices[device].set_strength(strength) + + def wireless_network_strength_changed_sig_handler(self, device, network, strength): + if not self._devices.has_key(device): + return + net = self._devices[device].get_network(network) + if net: + net.set_strength(strength) + + def device_carrier_on_sig_handler(self, device): + if not self._devices.has_key(device): + return + self._devices[device].set_carrier(True) + + def device_carrier_off_sig_handler(self, device): + if not self._devices.has_key(device): + return + self._devices[device].set_carrier(False) diff --git a/src/hardware/nminfo.py b/src/hardware/nminfo.py new file mode 100644 index 0000000..3a93120 --- /dev/null +++ b/src/hardware/nminfo.py @@ -0,0 +1,525 @@ +# vi: ts=4 ai noet +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 dbus +import dbus.service +import time +import os +import binascii +import ConfigParser +import logging + +import nmclient +import keydialog +import gtk +from sugar import env + +IW_AUTH_KEY_MGMT_802_1X = 0x1 +IW_AUTH_KEY_MGMT_PSK = 0x2 + +IW_AUTH_WPA_VERSION_DISABLED = 0x00000001 +IW_AUTH_WPA_VERSION_WPA = 0x00000002 +IW_AUTH_WPA_VERSION_WPA2 = 0x00000004 + +NM_AUTH_TYPE_WPA_PSK_AUTO = 0x00000000 +IW_AUTH_CIPHER_NONE = 0x00000001 +IW_AUTH_CIPHER_WEP40 = 0x00000002 +IW_AUTH_CIPHER_TKIP = 0x00000004 +IW_AUTH_CIPHER_CCMP = 0x00000008 +IW_AUTH_CIPHER_WEP104 = 0x00000010 + +IW_AUTH_ALG_OPEN_SYSTEM = 0x00000001 +IW_AUTH_ALG_SHARED_KEY = 0x00000002 + +NM_INFO_IFACE='org.freedesktop.NetworkManagerInfo' +NM_INFO_PATH='/org/freedesktop/NetworkManagerInfo' + + +class NoNetworks(dbus.DBusException): + def __init__(self): + dbus.DBusException.__init__(self) + self._dbus_error_name = NM_INFO_IFACE + '.NoNetworks' + +class CanceledKeyRequestError(dbus.DBusException): + def __init__(self): + dbus.DBusException.__init__(self) + self._dbus_error_name = NM_INFO_IFACE + '.CanceledError' + + +class NetworkInvalidError(Exception): + pass + + +class NMConfig(ConfigParser.ConfigParser): + def get_bool(self, section, name): + opt = self.get(section, name) + if type(opt) == type(""): + if opt.lower() == 'yes' or opt.lower() == 'true': + return True + elif opt.lower() == 'no' or opt.lower() == 'false': + return False + raise ValueError("Invalid format for %s/%s. Should be one of [yes, no, true, false]." % (section, name)) + + def get_list(self, section, name): + opt = self.get(section, name) + if type(opt) == type(""): + if not len(opt): + return [] + try: + return opt.split() + except Exception: + pass + raise ValueError("Invalid format for %s/%s. Should be a space-separate list." % (section, name)) + + def get_int(self, section, name): + opt = self.get(section, name) + try: + return int(opt) + except Exception: + pass + raise ValueError("Invalid format for %s/%s. Should be a valid integer." % (section, name)) + + def get_float(self, section, name): + opt = self.get(section, name) + try: + return float(opt) + except Exception: + pass + raise ValueError("Invalid format for %s/%s. Should be a valid float." % (section, name)) + + +NETWORK_TYPE_UNKNOWN = 0 +NETWORK_TYPE_ALLOWED = 1 +NETWORK_TYPE_INVALID = 2 + + +class Security(object): + def __init__(self, we_cipher): + self._we_cipher = we_cipher + + def read_from_config(self, cfg, name): + pass + + def read_from_args(self, args): + pass + + def new_from_config(cfg, name): + security = None + we_cipher = cfg.get_int(name, "we_cipher") + if we_cipher == IW_AUTH_CIPHER_NONE: + security = Security(we_cipher) + elif we_cipher == IW_AUTH_CIPHER_WEP40 or we_cipher == IW_AUTH_CIPHER_WEP104: + security = WEPSecurity(we_cipher) + elif we_cipher == NM_AUTH_TYPE_WPA_PSK_AUTO or we_cipher == IW_AUTH_CIPHER_CCMP or we_cipher == IW_AUTH_CIPHER_TKIP: + security = WPASecurity(we_cipher) + else: + raise ValueError("Unsupported security combo") + security.read_from_config(cfg, name) + return security + new_from_config = staticmethod(new_from_config) + + def new_from_args(we_cipher, args): + security = None + try: + if we_cipher == IW_AUTH_CIPHER_NONE: + security = Security(we_cipher) + elif we_cipher == IW_AUTH_CIPHER_WEP40 or we_cipher == IW_AUTH_CIPHER_WEP104: + security = WEPSecurity(we_cipher) + elif we_cipher == NM_AUTH_TYPE_WPA_PSK_AUTO or we_cipher == IW_AUTH_CIPHER_CCMP or we_cipher == IW_AUTH_CIPHER_TKIP: + security = WPASecurity(we_cipher) + else: + raise ValueError("Unsupported security combo") + security.read_from_args(args) + except ValueError, e: + logging.debug("Error reading security information: %s" % e) + del security + return None + return security + new_from_args = staticmethod(new_from_args) + + def get_properties(self): + return [dbus.Int32(self._we_cipher)] + + def write_to_config(self, section, config): + config.set(section, "we_cipher", self._we_cipher) + + +class WEPSecurity(Security): + def read_from_args(self, args): + if len(args) != 2: + raise ValueError("not enough arguments") + key = args[0] + auth_alg = args[1] + if isinstance(key, unicode): + key = key.encode() + if not isinstance(key, str): + raise ValueError("wrong argument type for key") + if not isinstance(auth_alg, int): + raise ValueError("wrong argument type for auth_alg") + self._key = key + self._auth_alg = auth_alg + + def read_from_config(self, cfg, name): + # Key should be a hex encoded string + self._key = cfg.get(name, "key") + if self._we_cipher == IW_AUTH_CIPHER_WEP40 and len(self._key) != 10: + raise ValueError("Key length not right for 40-bit WEP") + if self._we_cipher == IW_AUTH_CIPHER_WEP104 and len(self._key) != 26: + raise ValueError("Key length not right for 104-bit WEP") + + try: + a = binascii.a2b_hex(self._key) + except TypeError: + raise ValueError("Key was not a hexadecimal string.") + + self._auth_alg = cfg.get_int(name, "auth_alg") + if self._auth_alg != IW_AUTH_ALG_OPEN_SYSTEM and self._auth_alg != IW_AUTH_ALG_SHARED_KEY: + raise ValueError("Invalid authentication algorithm %d" % self._auth_alg) + + def get_properties(self): + args = Security.get_properties(self) + args.append(dbus.String(self._key)) + args.append(dbus.Int32(self._auth_alg)) + return args + + def write_to_config(self, section, config): + Security.write_to_config(self, section, config) + config.set(section, "key", self._key) + config.set(section, "auth_alg", self._auth_alg) + +class WPASecurity(Security): + def read_from_args(self, args): + if len(args) != 3: + raise ValueError("not enough arguments") + key = args[0] + if isinstance(key, unicode): + key = key.encode() + if not isinstance(key, str): + raise ValueError("wrong argument type for key") + + wpa_ver = args[1] + if not isinstance(wpa_ver, int): + raise ValueError("wrong argument type for WPA version") + + key_mgmt = args[2] + if not isinstance(key_mgmt, int): + raise ValueError("wrong argument type for WPA key management") + if not key_mgmt & IW_AUTH_KEY_MGMT_PSK: + raise ValueError("Key management types other than PSK are not supported") + + self._key = key + self._wpa_ver = wpa_ver + self._key_mgmt = key_mgmt + + def read_from_config(self, cfg, name): + # Key should be a hex encoded string + self._key = cfg.get(name, "key") + if len(self._key) != 64: + raise ValueError("Key length not right for WPA-PSK") + + try: + a = binascii.a2b_hex(self._key) + except TypeError: + raise ValueError("Key was not a hexadecimal string.") + + self._wpa_ver = cfg.get_int(name, "wpa_ver") + if self._wpa_ver != IW_AUTH_WPA_VERSION_WPA and self._wpa_ver != IW_AUTH_WPA_VERSION_WPA2: + raise ValueError("Invalid WPA version %d" % self._wpa_ver) + + self._key_mgmt = cfg.get_int(name, "key_mgmt") + if not self._key_mgmt & IW_AUTH_KEY_MGMT_PSK: + raise ValueError("Invalid WPA key management option %d" % self._key_mgmt) + + def get_properties(self): + args = Security.get_properties(self) + args.append(dbus.String(self._key)) + args.append(dbus.Int32(self._wpa_ver)) + args.append(dbus.Int32(self._key_mgmt)) + return args + + def write_to_config(self, section, config): + Security.write_to_config(self, section, config) + config.set(section, "key", self._key) + config.set(section, "wpa_ver", self._wpa_ver) + config.set(section, "key_mgmt", self._key_mgmt) + + +class Network: + def __init__(self, ssid): + self.ssid = ssid + self.timestamp = int(time.time()) + self.bssids = [] + self.we_cipher = 0 + self._security = None + + def get_properties(self): + bssid_list = dbus.Array([], signature="s") + for item in self.bssids: + bssid_list.append(dbus.String(item)) + args = [dbus.String(self.ssid), dbus.Int32(self.timestamp), dbus.Boolean(True), bssid_list] + args += self._security.get_properties() + return tuple(args) + + def get_security(self): + return self._security.get_properties() + + def set_security(self, security): + self._security = security + + def read_from_args(self, auto, bssid, we_cipher, args): + if auto == False: + self.timestamp = int(time.time()) + if not bssid in self.bssids: + self.bssids.append(bssid) + + self._security = Security.new_from_args(we_cipher, args) + if not self._security: + raise NetworkInvalidError("Invalid security information") + + def read_from_config(self, config): + try: + self.timestamp = config.get_int(self.ssid, "timestamp") + except (ConfigParser.NoOptionError, ValueError), e: + raise NetworkInvalidError(e) + + try: + self._security = Security.new_from_config(config, self.ssid) + except Exception, e: + raise NetworkInvalidError(e) + + # The following don't need to be present + try: + self.bssids = config.get_list(self.ssid, "bssids") + except (ConfigParser.NoOptionError, ValueError), e: + pass + + def write_to_config(self, config): + try: + config.add_section(self.ssid) + config.set(self.ssid, "timestamp", self.timestamp) + if len(self.bssids) > 0: + opt = " " + opt.join(self.bssids) + config.set(self.ssid, "bssids", opt) + self._security.write_to_config(self.ssid, config) + except Exception, e: + logging.debug("Error writing '%s': %s" % (self.ssid, e)) + + +class NotFoundError(dbus.DBusException): + pass +class UnsupportedError(dbus.DBusException): + pass + +class NMInfoDBusServiceHelper(dbus.service.Object): + def __init__(self, parent): + self._parent = parent + bus = dbus.SystemBus() + + # If NMI is already around, don't grab the NMI service + bus_object = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') + name = None + try: + name = bus_object.GetNameOwner("org.freedesktop.NetworkManagerInfo", \ + dbus_interface='org.freedesktop.DBus') + except dbus.DBusException: + pass + if name: + logging.debug("NMI service already owned by %s, won't claim it." % name) + raise RuntimeError + + bus_name = dbus.service.BusName(NM_INFO_IFACE, bus=bus) + dbus.service.Object.__init__(self, bus_name, NM_INFO_PATH) + + @dbus.service.method(NM_INFO_IFACE, in_signature='i', out_signature='as') + def getNetworks(self, net_type): + ssids = self._parent.get_networks(net_type) + if len(ssids) > 0: + return dbus.Array(ssids) + + raise NoNetworks() + + @dbus.service.method(NM_INFO_IFACE, in_signature='si', async_callbacks=('async_cb', 'async_err_cb')) + def getNetworkProperties(self, ssid, net_type, async_cb, async_err_cb): + self._parent.get_network_properties(ssid, net_type, async_cb, async_err_cb) + + @dbus.service.method(NM_INFO_IFACE) + def updateNetworkInfo(self, ssid, bauto, bssid, cipher, *args): + self._parent.update_network_info(ssid, bauto, bssid, cipher, args) + + @dbus.service.method(NM_INFO_IFACE, async_callbacks=('async_cb', 'async_err_cb')) + def getKeyForNetwork(self, dev_path, net_path, ssid, attempt, new_key, async_cb, async_err_cb): + self._parent.get_key_for_network(dev_path, net_path, ssid, + attempt, new_key, async_cb, async_err_cb) + + @dbus.service.method(NM_INFO_IFACE) + def cancelGetKeyForNetwork(self): + self._parent.cancel_get_key_for_network() + +class NMInfo(object): + def __init__(self, client): + profile_path = env.get_profile_path() + self._cfg_file = os.path.join(profile_path, "nm", "networks.cfg") + self._nmclient = client + self._allowed_networks = self._read_config() + self._dbus_helper = NMInfoDBusServiceHelper(self) + self._key_dialog = None + + def save_config(self): + self._write_config(self._allowed_networks) + + def _read_config(self): + if not os.path.exists(os.path.dirname(self._cfg_file)): + os.makedirs(os.path.dirname(self._cfg_file), 0755) + if not os.path.exists(self._cfg_file): + self._write_config({}) + return {} + + config = NMConfig() + config.read(self._cfg_file) + networks = {} + for name in config.sections(): + try: + net = Network(name) + net.read_from_config(config) + networks[name] = net + except Exception, e: + logging.error("Error when processing config for the network %s: %r" % (name, e)) + + del config + return networks + + def _write_config(self, networks): + fp = open(self._cfg_file, 'w') + config = NMConfig() + for net in networks.values(): + net.write_to_config(config) + config.write(fp) + fp.close() + del config + + def get_networks(self, net_type): + if net_type != NETWORK_TYPE_ALLOWED: + raise ValueError("Bad network type") + nets = [] + for net in self._allowed_networks.values(): + nets.append(net.ssid) + logging.debug("Returning networks: %s" % nets) + return nets + + def get_network_properties(self, ssid, net_type, async_cb, async_err_cb): + if not isinstance(ssid, unicode): + async_err_cb(ValueError("Invalid arguments; ssid must be unicode.")) + if net_type != NETWORK_TYPE_ALLOWED: + async_err_cb(ValueError("Bad network type")) + if not self._allowed_networks.has_key(ssid): + async_err_cb(NotFoundError("Network '%s' not found." % ssid)) + network = self._allowed_networks[ssid] + props = network.get_properties() + + # DBus workaround: the normal method return handler wraps + # the returned arguments in a tuple and then converts that to a + # struct, but NetworkManager expects a plain list of arguments. + # It turns out that the async callback method return code _doesn't_ + # wrap the returned arguments in a tuple, so as a workaround use + # the async callback stuff here even though we're not doing it + # asynchronously. + async_cb(*props) + + def update_network_info(self, ssid, auto, bssid, we_cipher, args): + if not isinstance(ssid, unicode): + raise ValueError("Invalid arguments; ssid must be unicode.") + if self._allowed_networks.has_key(ssid): + del self._allowed_networks[ssid] + net = Network(ssid) + try: + net.read_from_args(auto, bssid, we_cipher, args) + logging.debug("Updated network information for '%s'." % ssid) + self._allowed_networks[ssid] = net + self.save_config() + except NetworkInvalidError, e: + logging.debug("Error updating network information: %s" % e) + del net + + def get_key_for_network(self, dev_op, net_op, ssid, attempt, new_key, async_cb, async_err_cb): + if not isinstance(ssid, unicode): + raise ValueError("Invalid arguments; ssid must be unicode.") + if self._allowed_networks.has_key(ssid) and not new_key: + # We've got the info already + net = self._allowed_networks[ssid] + async_cb(tuple(net.get_security())) + return + + # Otherwise, ask the user for it + net = None + dev = self._nmclient.get_device(dev_op) + if not dev: + async_err_cb(NotFoundError("Device was unknown.")) + return + + if dev.get_type() == nmclient.DEVICE_TYPE_802_3_ETHERNET: + # We don't support wired 802.1x yet... + async_err_cb(UnsupportedError("Device type is unsupported by NMI.")) + return + + net = dev.get_network(net_op) + if not net: + async_err_cb(NotFoundError("Network was unknown.")) + return + + self._key_dialog = keydialog.new_key_dialog(net, async_cb, async_err_cb) + self._key_dialog.connect("response", self._key_dialog_response_cb) + self._key_dialog.connect("destroy", self._key_dialog_destroy_cb) + self._key_dialog.show_all() + + def _key_dialog_destroy_cb(self, widget, foo=None): + if widget != self._key_dialog: + return + self._key_dialog_response_cb(widget, gtk.RESPONSE_CANCEL) + + def _key_dialog_response_cb(self, widget, response_id): + if widget != self._key_dialog: + return + + (async_cb, async_err_cb) = self._key_dialog.get_callbacks() + net = self._key_dialog.get_network() + security = None + if response_id == gtk.RESPONSE_OK: + security = self._key_dialog.create_security() + self._key_dialog = None + widget.destroy() + + if response_id in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_NONE]: + # key dialog dialog was canceled; send the error back to NM + async_err_cb(CanceledKeyRequestError()) + elif response_id == gtk.RESPONSE_OK: + if not security: + raise RuntimeError("Invalid security arguments.") + props = security.get_properties() + a = tuple(props) + async_cb(*a) + else: + raise RuntimeError("Unhandled key dialog response %d" % response_id) + + def cancel_get_key_for_network(self): + # Close the wireless key dialog and just have it return + # with the 'canceled' argument set to true + if not self._key_dialog: + return + self._key_dialog_destroy_cb(self._key_dialog) + diff --git a/src/hardware/schoolserver.py b/src/hardware/schoolserver.py new file mode 100644 index 0000000..68d14f7 --- /dev/null +++ b/src/hardware/schoolserver.py @@ -0,0 +1,45 @@ +from sugar.profile import get_profile +from xmlrpclib import ServerProxy, Error +import sys +import os + +REGISTER_URL = 'http://schoolserver:8080/' + +def register_laptop(url=REGISTER_URL): + if not have_ofw_tree(): + return False + + sn = read_ofw('mfg-data/SN') + uuid = read_ofw('mfg-data/U#') + sn = sn or 'SHF00000000' + uuid = uuid or '00000000-0000-0000-0000-000000000000' + + profile = get_profile() + + try: + server = ServerProxy(url) + data = server.register(sn, profile.nick_name, uuid, profile.pubkey) + if data['success'] != 'OK': + print >> sys.stderr, "Error registering laptop: " + data['error'] + return False + + profile.jabber_server = data['jabberserver'] + profile.backup1 = data['backupurl'] + profile.save() + except Error, e: + print >> sys.stderr, "Error registering laptop: " + str(e) + return False + + return True + +def have_ofw_tree(): + return os.path.exists('/ofw') + +def read_ofw(path): + path = os.path.join('/ofw', path) + if not os.path.exists(path): + return None + fh = open(path, 'r') + data = fh.read().rstrip('\0\n') + fh.close() + return data diff --git a/src/intro/Makefile.am b/src/intro/Makefile.am new file mode 100644 index 0000000..3b92ea0 --- /dev/null +++ b/src/intro/Makefile.am @@ -0,0 +1,10 @@ +imagedir = $(pkgdatadir)/shell/intro +image_DATA = default-picture.png + +EXTRA_DIST = $(conf_DATA) $(image_DATA) +sugardir = $(pkgdatadir)/shell/intro +sugar_PYTHON = \ + __init__.py \ + colorpicker.py \ + intro.py \ + glive.py diff --git a/src/intro/__init__.py b/src/intro/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/intro/__init__.py diff --git a/src/intro/colorpicker.py b/src/intro/colorpicker.py new file mode 100644 index 0000000..90dbc26 --- /dev/null +++ b/src/intro/colorpicker.py @@ -0,0 +1,42 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 hippo + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style +from sugar.graphics.xocolor import XoColor + +class ColorPicker(hippo.CanvasBox, hippo.CanvasItem): + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + self.props.orientation = hippo.ORIENTATION_HORIZONTAL + + self._xo = CanvasIcon(size=style.XLARGE_ICON_SIZE, + icon_name='computer-xo') + self._set_random_colors() + self._xo.connect('activated', self._xo_activated_cb) + self.append(self._xo) + + def _xo_activated_cb(self, item): + self._set_random_colors() + + def get_color(self): + return self._xo_color + + def _set_random_colors(self): + self._xo_color = XoColor() + self._xo.props.xo_color = self._xo_color diff --git a/src/intro/default-picture.png b/src/intro/default-picture.png new file mode 100644 index 0000000..e26b9b0 --- /dev/null +++ b/src/intro/default-picture.png Binary files differ diff --git a/src/intro/glive.py b/src/intro/glive.py new file mode 100644 index 0000000..a875e48 --- /dev/null +++ b/src/intro/glive.py @@ -0,0 +1,196 @@ +# -*- Mode: Python -*- +# vi:si:et:sw=4:sts=4:ts=4 + +import gtk +import pygtk +pygtk.require('2.0') +import sys + +import pygst +pygst.require('0.10') +import gst +import gst.interfaces + +import gobject +gobject.threads_init() + +class Glive(gobject.GObject): + __gsignals__ = { + 'new-picture': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'sink': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self, parent, width, height): + gobject.GObject.__init__(self) + self._parent = parent + + #check out the halfpipe, d00d. + self.pipeline = gst.Pipeline() + + self.v4l2src = gst.element_factory_make("v4l2src", "v4l2src") + self.t = gst.element_factory_make("tee", "tee") + self.t_src_pad = self.t.get_request_pad( "src%d" ) + self.vscale = gst.element_factory_make("videoscale", "videoscale") + self.ximagesink = gst.element_factory_make("ximagesink", "ximagesink") + + self.pipeline.add(self.v4l2src) + self.pipeline.add(self.t) + self.pipeline.add(self.vscale) + self.pipeline.add(self.ximagesink) + + self.v4l2src.link(self.t) + + videoscale_structure = gst.Structure("video/x-raw-rgb") + videoscale_structure['width'] = width + videoscale_structure['height'] = height + videoscale_structure['bpp'] = 16 + videoscale_structure['depth'] = 16 + videoscale_caps = gst.Caps(videoscale_structure) + self.t_src_pad.link(self.vscale.get_pad("sink")) + self.vscale.link(self.ximagesink, videoscale_caps) + #self.vscale.link(self.ximagesink) + + self.queue = gst.element_factory_make("queue", "queue") + self.queue.set_property("leaky", True) + self.queue.set_property("max-size-buffers", 1) + self.qsrc = self.queue.get_pad( "src" ) + self.qsink = self.queue.get_pad("sink") + self.ffmpeg = gst.element_factory_make("ffmpegcolorspace", "ffmpegcolorspace") + self.jpgenc = gst.element_factory_make("jpegenc", "jpegenc") + self.filesink = gst.element_factory_make("fakesink", "fakesink") + self.filesink.connect( "handoff", self.copyframe ) + self.filesink.set_property("signal-handoffs", True) + self.pipeline.add(self.queue, self.ffmpeg, self.jpgenc, self.filesink) + + #only link at snapshot time + #self.t.link(self.queue) + self.queue.link(self.ffmpeg) + self.ffmpeg.link(self.jpgenc) + self.jpgenc.link(self.filesink) + self.exposureOpen = False + + self._bus = self.pipeline.get_bus() + self._CONNECT_SYNC = -1 + self._CONNECT_MSG = -1 + self.doPostBusStuff() + + def copyframe(self, fsink, buffer, pad, user_data=None): + #for some reason, we get two back to back buffers, even though we + #ask for only one. + if (self.exposureOpen): + self.exposureOpen = False + piccy = gtk.gdk.pixbuf_loader_new_with_mime_type("image/jpeg") + piccy.write( buffer ) + piccy.close() + pixbuf = piccy.get_pixbuf() + del piccy + + self.t.unlink(self.queue) + self.queue.set_property("leaky", True) + + gobject.idle_add(self.loadPic, pixbuf) + + def loadPic( self, pixbuf ): + self.emit('new-picture', pixbuf) + + def takeSnapshot( self ): + if (self.exposureOpen): + return + else: + self.exposureOpen = True + self.t.link(self.queue) + + def doPostBusStuff(self): + self._bus.enable_sync_message_emission() + self._bus.add_signal_watch() + self._CONNECT_SYNC = self._bus.connect('sync-message::element', self.on_sync_message) + self._CONNECT_MSG = self._bus.connect('message', self.on_message) + + def on_sync_message(self, bus, message): + if message.structure is None: + return + if message.structure.get_name() == 'prepare-xwindow-id': + self.emit('sink', message.src) + message.src.set_property('force-aspect-ratio', True) + + def on_message(self, bus, message): + t = message.type + if (t == gst.MESSAGE_ERROR): + err, debug = message.parse_error() + if (self.on_eos): + self.on_eos() + self._playing = False + elif (t == gst.MESSAGE_EOS): + if (self.on_eos): + self.on_eos() + self._playing = False + + def on_eos( self ): + pass + + def stop(self): + self.pipeline.set_state(gst.STATE_NULL) + + def play(self): + self.pipeline.set_state(gst.STATE_PLAYING) + + def pause(self): + self.pipeline.set_state(gst.STATE_PAUSED) + + +class LiveVideoSlot(gtk.EventBox): + __gsignals__ = { + 'pixbuf': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + } + + def __init__(self, width, height): + gtk.EventBox.__init__(self) + + self.imagesink = None + self.playa = None + self._width = width + self._height = height + + self.unset_flags(gtk.DOUBLE_BUFFERED) + self.connect('focus-in-event', self.focus_in) + self.connect('focus-out-event', self.focus_out) + self.connect("button-press-event", self._button_press_event_cb) + self.connect("expose-event", self._expose_event_cb) + + def _expose_event_cb(self, widget, event): + if not self.playa: + self.playa = Glive(self, self._width, self._height) + self.playa.connect('new-picture', self._new_picture_cb) + self.playa.connect('sink', self._new_sink_cb) + + def _new_picture_cb(self, playa, pixbuf): + self.emit('pixbuf', pixbuf) + + def _new_sink_cb(self, playa, sink): + if (self.imagesink != None): + assert self.window.xid + self.imagesink = None + del self.imagesink + self.imagesink = sink + self.imagesink.set_xwindow_id(self.window.xid) + + def _button_press_event_cb(self, widget, event): + self.takeSnapshot() + + def focus_in(self, widget, event, args=None): + self.play() + + def focus_out(self, widget, event, args=None): + self.stop() + + def play( self ): + self.playa.play() + + def pause( self ): + self.playa.pause() + + def stop( self ): + self.playa.stop() + + def takeSnapshot( self ): + self.playa.takeSnapshot() diff --git a/src/intro/intro.py b/src/intro/intro.py new file mode 100644 index 0000000..1bd46c7 --- /dev/null +++ b/src/intro/intro.py @@ -0,0 +1,267 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 +from ConfigParser import ConfigParser +from gettext import gettext as _ + +import gtk +import gobject +import dbus +import hippo +import logging + +from sugar import env +from sugar.graphics import style +from sugar.graphics.icon import Icon +from sugar.graphics.entry import CanvasEntry +from sugar.profile import get_profile + +import colorpicker + +_BACKGROUND_COLOR = style.COLOR_PANEL_GREY + +class _Page(hippo.CanvasBox): + __gproperties__ = { + 'valid' : (bool, None, None, False, + gobject.PARAM_READABLE) + } + + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + self.valid = False + + def set_valid(self, valid): + self.valid = valid + self.notify('valid') + + def do_get_property(self, pspec): + if pspec.name == 'valid': + return self.valid + + def activate(self): + pass + +class _NamePage(_Page): + def __init__(self, intro): + _Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER, + background_color=_BACKGROUND_COLOR.get_int(), + spacing=style.DEFAULT_SPACING, + orientation=hippo.ORIENTATION_HORIZONTAL,) + + self._intro = intro + + label = hippo.CanvasText(text=_("Name:")) + self.append(label) + + self._entry = CanvasEntry(box_width=style.zoom(300)) + self._entry.set_background(_BACKGROUND_COLOR.get_html()) + self._entry.connect('notify::text', self._text_changed_cb) + + widget = self._entry.props.widget + widget.set_max_length(45) + + self.append(self._entry) + + def _text_changed_cb(self, entry, pspec): + valid = len(entry.props.text.strip()) > 0 + self.set_valid(valid) + + def get_name(self): + return self._entry.props.text + + def activate(self): + self._entry.props.widget.grab_focus() + +class _ColorPage(_Page): + def __init__(self, **kwargs): + _Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER, + background_color=_BACKGROUND_COLOR.get_int(), + spacing=style.DEFAULT_SPACING, + yalign=hippo.ALIGNMENT_CENTER, **kwargs) + + self._label = hippo.CanvasText(text=_("Click to change color:"), + xalign=hippo.ALIGNMENT_CENTER) + self.append(self._label) + + self._cp = colorpicker.ColorPicker(xalign=hippo.ALIGNMENT_CENTER) + self.append(self._cp) + + self._color = self._cp.get_color() + self.set_valid(True) + + def get_color(self): + return self._cp.get_color() + +class _IntroBox(hippo.CanvasBox): + __gsignals__ = { + 'done': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT])) + } + + PAGE_NAME = 0 + PAGE_COLOR = 1 + + PAGE_FIRST = PAGE_NAME + PAGE_LAST = PAGE_COLOR + + def __init__(self): + hippo.CanvasBox.__init__(self, padding=style.zoom(30), + background_color=_BACKGROUND_COLOR.get_int()) + + self._page = self.PAGE_NAME + self._name_page = _NamePage(self) + self._color_page = _ColorPage() + self._current_page = None + + self._setup_page() + + def _setup_page(self): + self.remove_all() + + if self._page == self.PAGE_NAME: + self._current_page = self._name_page + elif self._page == self.PAGE_COLOR: + self._current_page = self._color_page + + self.append(self._current_page, hippo.PACK_EXPAND) + + button_box = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL) + + if self._page != self.PAGE_FIRST: + back_button = hippo.CanvasButton(text=_('Back')) + image = Icon(icon_name='go-left') + back_button.props.widget.set_image(image) + back_button.connect('activated', self._back_activated_cb) + button_box.append(back_button) + + spacer = hippo.CanvasBox() + button_box.append(spacer, hippo.PACK_EXPAND) + + self._next_button = hippo.CanvasButton() + image = Icon(icon_name='go-right') + self._next_button.props.widget.set_image(image) + + if self._page == self.PAGE_LAST: + self._next_button.props.text = _('Done') + self._next_button.connect('activated', self._done_activated_cb) + else: + self._next_button.props.text = _('Next') + self._next_button.connect('activated', self._next_activated_cb) + + self._current_page.activate() + + self._update_next_button() + button_box.append(self._next_button) + + self._current_page.connect('notify::valid', + self._page_valid_changed_cb) + self.append(button_box) + + def _update_next_button(self): + widget = self._next_button.props.widget + widget.props.sensitive = self._current_page.props.valid + + def _page_valid_changed_cb(self, page, pspec): + self._update_next_button() + + def _back_activated_cb(self, item): + self.back() + + def back(self): + if self._page != self.PAGE_FIRST: + self._page -= 1 + self._setup_page() + + def _next_activated_cb(self, item): + self.next() + + def next(self): + if self._page == self.PAGE_LAST: + self.done() + if self._current_page.props.valid: + self._page += 1 + self._setup_page() + + def _done_activated_cb(self, item): + self.done() + + def done(self): + path = os.path.join(os.path.dirname(__file__), 'default-picture.png') + pixbuf = gtk.gdk.pixbuf_new_from_file(path) + name = self._name_page.get_name() + color = self._color_page.get_color() + + self.emit('done', pixbuf, name, color) + + def _key_press_cb(self, widget, event): + if gtk.gdk.keyval_name(event.keyval) == "Return": + self.next() + return True + elif gtk.gdk.keyval_name(event.keyval) == "Escape": + self.back() + return True + return False + +class IntroWindow(gtk.Window): + def __init__(self): + gtk.Window.__init__(self) + + self._canvas = hippo.Canvas() + self._intro_box = _IntroBox() + self._intro_box.connect('done', self._done_cb) + self._canvas.set_root(self._intro_box) + + self.add(self._canvas) + self._canvas.show() + self.connect('key-press-event', self._intro_box._key_press_cb) + + def _done_cb(self, box, pixbuf, name, color): + self.hide() + gobject.idle_add(self._create_profile, pixbuf, name, color) + + def _create_profile(self, pixbuf, name, color): + # Save the buddy icon + icon_path = os.path.join(env.get_profile_path(), "buddy-icon.jpg") + scaled = pixbuf.scale_simple(200, 200, gtk.gdk.INTERP_BILINEAR) + pixbuf.save(icon_path, "jpeg", {"quality":"85"}) + + profile = get_profile() + profile.nick_name = name + profile.color = color + profile.save() + + # Generate keypair + import commands + keypath = os.path.join(env.get_profile_path(), "owner.key") + if not os.path.isfile(keypath): + cmd = "ssh-keygen -q -t dsa -f %s -C '' -N ''" % keypath + (s, o) = commands.getstatusoutput(cmd) + if s != 0: + logging.error("Could not generate key pair: %d" % s) + else: + logging.error("Keypair exists, skip generation.") + + gtk.main_quit() + return False + + +if __name__ == "__main__": + w = IntroWindow() + w.show() + w.connect('destroy', gtk.main_quit) + gtk.main() diff --git a/src/logsmanager.py b/src/logsmanager.py new file mode 100644 index 0000000..caa50d2 --- /dev/null +++ b/src/logsmanager.py @@ -0,0 +1,54 @@ +# Copyright (C) 2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import os +import time + +from sugar import env + +_MAX_BACKUP_DIRS = 3 + +def setup(): + logs_dir = env.get_logs_path() + if not os.path.isdir(logs_dir): + os.makedirs(logs_dir) + + backup_logs = [] + backup_dirs = [] + for f in os.listdir(logs_dir): + path = os.path.join(logs_dir, f) + if os.path.isfile(path): + backup_logs.append(f) + elif os.path.isdir(path): + backup_dirs.append(path) + + if len(backup_dirs) > _MAX_BACKUP_DIRS: + backup_dirs.sort() + root = backup_dirs[0] + for f in os.listdir(root): + os.remove(os.path.join(root, f)) + os.rmdir(root) + + if len(backup_logs) > 0: + name = str(int(time.time())) + backup_dir = os.path.join(logs_dir, name) + os.mkdir(backup_dir) + for log in backup_logs: + source_path = os.path.join(logs_dir, log) + dest_path = os.path.join(backup_dir, log) + os.rename(source_path, dest_path) + diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..f2f1a51 --- /dev/null +++ b/src/main.py @@ -0,0 +1,153 @@ +# Copyright (C) 2006, Red Hat, Inc. +# +# 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 sys +import os +from ConfigParser import ConfigParser +import gettext + +# HACK we need to import numpy before gtk otherwise we traceback in +# some locales. See http://dev.laptop.org/ticket/5559. +import numpy + +import pygtk +pygtk.require('2.0') +import gtk +import gobject + +from sugar import env +from sugar import logger +from sugar.profile import get_profile + +from view.Shell import Shell +from model.shellmodel import ShellModel +from shellservice import ShellService +from hardware import hardwaremanager +from intro import intro +import logsmanager +import config + +def _start_matchbox(): + cmd = ['matchbox-window-manager'] + + cmd.extend(['-use_titlebar', 'no']) + cmd.extend(['-theme', 'sugar']) + cmd.extend(['-kbdconfig', os.path.join(config.data_path, 'kbdconfig')]) + + gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH) + +def _save_session_info(): + # Save our DBus Session Bus address somewhere it can be found + # + # WARNING!!! this is going away at some near future point, do not rely on it + # + session_info_file = os.path.join(env.get_profile_path(), "session.info") + f = open(session_info_file, "w") + + cp = ConfigParser() + cp.add_section('Session') + cp.set('Session', 'dbus_address', os.environ['DBUS_SESSION_BUS_ADDRESS']) + cp.set('Session', 'display', gtk.gdk.display_get_default().get_name()) + cp.write(f) + + f.close() + +def _setup_translations(): + locale_path = os.path.join(config.prefix, 'share', 'locale') + domain = 'sugar' + + gettext.bindtextdomain(domain, locale_path) + gettext.textdomain(domain) + +def check_cm(bus_name): + try: + import dbus + bus = dbus.SessionBus() + bus_object = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') + name = bus_object.GetNameOwner(bus_name, dbus_interface='org.freedesktop.DBus') + if name: + return True + except dbus.DBusException: + pass + return False + +def _shell_started_cb(): + # Unfreeze the display + hw_manager = hardwaremanager.get_manager() + hw_manager.set_dcon_freeze(0) + +def main(): + gobject.idle_add(_shell_started_cb) + + logsmanager.setup() + logger.start('shell') + + _save_session_info() + _start_matchbox() + _setup_translations() + + hw_manager = hardwaremanager.get_manager() + hw_manager.startup() + + icons_path = env.get_data_path('icons') + gtk.icon_theme_get_default().append_search_path(icons_path) + + # Do initial setup if needed + if not get_profile().is_valid(): + win = intro.IntroWindow() + win.show_all() + gtk.main() + + if os.environ.has_key("SUGAR_TP_DEBUG"): + # Allow the user time to start up telepathy connection managers + # using the Sugar DBus bus address + import time + from telepathy.client import ManagerRegistry + + registry = ManagerRegistry() + registry.LoadManagers() + + debug_flags = os.environ["SUGAR_TP_DEBUG"].split(',') + for cm_name in debug_flags: + if cm_name not in ["gabble", "salut"]: + continue + + try: + cm = registry.services[cm_name] + except KeyError: + print RuntimeError("%s connection manager not found!" % cm_name) + + while not check_cm(cm['busname']): + print "Waiting for %s on: DBUS_SESSION_BUS_ADDRESS=%s" % \ + (cm_name, os.environ["DBUS_SESSION_BUS_ADDRESS"]) + try: + time.sleep(5) + except KeyboardInterrupt: + print "Got Ctrl+C, continuing..." + break + + model = ShellModel() + shell = Shell(model) + service = ShellService(shell) + + try: + gtk.main() + except KeyboardInterrupt: + print 'Ctrl+C pressed, exiting...' + + session_info_file = os.path.join(env.get_profile_path(), "session.info") + os.remove(session_info_file) + diff --git a/src/model/BuddyModel.py b/src/model/BuddyModel.py new file mode 100644 index 0000000..11c6567 --- /dev/null +++ b/src/model/BuddyModel.py @@ -0,0 +1,164 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 logging + +from sugar.presence import presenceservice +from sugar.graphics.xocolor import XoColor +import gobject + +_NOT_PRESENT_COLOR = "#888888,#BBBBBB" + +class BuddyModel(gobject.GObject): + __gsignals__ = { + 'appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'nick-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'color-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'icon-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([])), + 'current-activity-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self, key=None, buddy=None, nick=None): + if (key and buddy) or (not key and not buddy): + raise RuntimeError("Must specify only _one_ of key or buddy.") + + gobject.GObject.__init__(self) + + self._ba_handler = None + self._pc_handler = None + self._dis_handler = None + self._bic_handler = None + self._cac_handler = None + + self._pservice = presenceservice.get_instance() + + self._buddy = None + + if not buddy: + self._key = key + # connect to the PS's buddy-appeared signal and + # wait for the buddy to appear + self._ba_handler = self._pservice.connect('buddy-appeared', + self._buddy_appeared_cb) + # Set color to 'inactive'/'disconnected' + self._set_color_from_string(_NOT_PRESENT_COLOR) + self._nick = nick + + self._pservice.get_buddies_async(reply_handler=self._get_buddies_cb) + else: + self._update_buddy(buddy) + + def _get_buddies_cb(self, list): + buddy = None + for iter_buddy in list: + if iter_buddy.props.key == self._key: + buddy = iter_buddy + break + + if buddy: + if self._ba_handler: + # Once we have the buddy, we no longer need to + # monitor buddy-appeared events + self._pservice.disconnect(self._ba_handler) + self._ba_handler = None + + self._update_buddy(buddy) + + def _set_color_from_string(self, color_string): + self._color = XoColor(color_string) + + def get_key(self): + return self._key + + def get_nick(self): + return self._nick + + def get_color(self): + return self._color + + def get_buddy(self): + return self._buddy + + def is_owner(self): + if not self._buddy: + return False + return self._buddy.props.owner + + def is_present(self): + if self._buddy: + return True + return False + + def get_current_activity(self): + if self._buddy: + return self._buddy.props.current_activity + return None + + def _update_buddy(self, buddy): + if not buddy: + raise ValueError("Buddy cannot be None.") + + self._buddy = buddy + self._key = self._buddy.props.key + self._nick = self._buddy.props.nick + self._set_color_from_string(self._buddy.props.color) + + self._pc_handler = self._buddy.connect('property-changed', self._buddy_property_changed_cb) + self._bic_handler = self._buddy.connect('icon-changed', self._buddy_icon_changed_cb) + + def _buddy_appeared_cb(self, pservice, buddy): + if self._buddy or buddy.props.key != self._key: + return + + if self._ba_handler: + # Once we have the buddy, we no longer need to + # monitor buddy-appeared events + self._pservice.disconnect(self._ba_handler) + self._ba_handler = None + + self._update_buddy(buddy) + self.emit('appeared') + + def _buddy_property_changed_cb(self, buddy, keys): + if not self._buddy: + return + if 'color' in keys: + self._set_color_from_string(self._buddy.props.color) + self.emit('color-changed', self.get_color()) + if 'current-activity' in keys: + self.emit('current-activity-changed', buddy.props.current_activity) + if 'nick' in keys: + self._nick = self._buddy.props.nick + self.emit('nick-changed', self.get_nick()) + + def _buddy_disappeared_cb(self, buddy): + if buddy != self._buddy: + return + self._buddy.disconnect(self._pc_handler) + self._buddy.disconnect(self._dis_handler) + self._buddy.disconnect(self._bic_handler) + self._buddy.disconnect(self._cac_handler) + self._set_color_from_string(_NOT_PRESENT_COLOR) + self.emit('disappeared') + self._buddy = None + + def _buddy_icon_changed_cb(self, buddy): + self.emit('icon-changed') diff --git a/src/model/Friends.py b/src/model/Friends.py new file mode 100644 index 0000000..6fc3e97 --- /dev/null +++ b/src/model/Friends.py @@ -0,0 +1,114 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 dbus +import os +from ConfigParser import ConfigParser + +import gobject + +from model.BuddyModel import BuddyModel +from sugar import env +import logging + +class Friends(gobject.GObject): + __gsignals__ = { + 'friend-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object])), + 'friend-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([str])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._friends = {} + self._path = os.path.join(env.get_profile_path(), 'friends') + + self.load() + + def has_buddy(self, buddy): + return self._friends.has_key(buddy.get_key()) + + def add_friend(self, buddy_info): + self._friends[buddy_info.get_key()] = buddy_info + self.emit('friend-added', buddy_info) + + def make_friend(self, buddy): + if not self.has_buddy(buddy): + self.add_friend(buddy) + self.save() + + def remove(self, buddy_info): + del self._friends[buddy_info.get_key()] + self.save() + self.emit('friend-removed', buddy_info.get_key()) + + def __iter__(self): + return self._friends.values().__iter__() + + def load(self): + cp = ConfigParser() + + try: + success = cp.read([self._path]) + if success: + for key in cp.sections(): + # HACK: don't screw up on old friends files + if len(key) < 20: + continue + buddy = BuddyModel(key=key, nick=cp.get(key, 'nick')) + self.add_friend(buddy) + except Exception, exc: + logging.error("Error parsing friends file: %s" % exc) + + def save(self): + cp = ConfigParser() + + for friend in self: + section = friend.get_key() + cp.add_section(section) + cp.set(section, 'nick', friend.get_nick()) + cp.set(section, 'color', friend.get_color().to_string()) + + fileobject = open(self._path, 'w') + cp.write(fileobject) + fileobject.close() + + self._sync_friends() + + def _sync_friends(self): + # XXX: temporary hack + # remove this when the shell service has a D-Bus API for buddies + + def friends_synced(): + pass + + def friends_synced_error(e): + logging.error("Error asking presence service to sync friends: %s" + % e) + + keys = [] + for friend in self: + keys.append(friend.get_key()) + + bus = dbus.SessionBus() + ps = bus.get_object('org.laptop.Sugar.Presence', + '/org/laptop/Sugar/Presence') + psi = dbus.Interface(ps, 'org.laptop.Sugar.Presence') + psi.SyncFriends(keys, + reply_handler=friends_synced, + error_handler=friends_synced_error) diff --git a/src/model/Invites.py b/src/model/Invites.py new file mode 100644 index 0000000..9ffab44 --- /dev/null +++ b/src/model/Invites.py @@ -0,0 +1,72 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gobject +from sugar.presence import presenceservice + +class Invite: + def __init__(self, issuer, bundle_id, activity_id): + self._issuer = issuer + self._activity_id = activity_id + self._bundle_id = bundle_id + + def get_activity_id(self): + return self._activity_id + + def get_bundle_id(self): + return self._bundle_id + +class Invites(gobject.GObject): + __gsignals__ = { + 'invite-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object])), + 'invite-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._dict = {} + + ps = presenceservice.get_instance() + owner = ps.get_owner() + owner.connect('joined-activity', self._owner_joined_cb) + + def add_invite(self, issuer, bundle_id, activity_id): + if activity_id in self._dict: + # there is no point to add more than one time + # an invite for the same activity + return + + invite = Invite(issuer, bundle_id, activity_id) + self._dict[activity_id] = invite + self.emit('invite-added', invite) + + def remove_invite(self, invite): + self._dict.pop(invite.get_activity_id()) + self.emit('invite-removed', invite) + + def remove_activity(self, activity_id): + invite = self._dict.get(activity_id) + if invite is not None: + self.remove_invite(invite) + + def _owner_joined_cb(self, owner, activity): + self.remove_activity(activity.props.id) + + def __iter__(self): + return self._dict.values().__iter__() diff --git a/src/model/Makefile.am b/src/model/Makefile.am new file mode 100644 index 0000000..0b7d14c --- /dev/null +++ b/src/model/Makefile.am @@ -0,0 +1,14 @@ +SUBDIRS = devices + +sugardir = $(pkgdatadir)/shell/model +sugar_PYTHON = \ + __init__.py \ + accesspointmodel.py \ + BuddyModel.py \ + Friends.py \ + Invites.py \ + Owner.py \ + MeshModel.py \ + shellmodel.py \ + homeactivity.py \ + homemodel.py diff --git a/src/model/MeshModel.py b/src/model/MeshModel.py new file mode 100644 index 0000000..da5b3c2 --- /dev/null +++ b/src/model/MeshModel.py @@ -0,0 +1,235 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gobject + +from sugar.graphics.xocolor import XoColor +from sugar.presence import presenceservice +from sugar import activity + +from model.BuddyModel import BuddyModel +from model.accesspointmodel import AccessPointModel +from hardware import hardwaremanager +from hardware import nmclient + +class ActivityModel: + def __init__(self, activity, bundle): + self.activity = activity + self.bundle = bundle + + def get_id(self): + return self.activity.props.id + + def get_icon_name(self): + return self.bundle.icon + + def get_color(self): + return XoColor(self.activity.props.color) + + def get_bundle_id(self): + return self.bundle.bundle_id + +class MeshModel(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'buddy-moved': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'access-point-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'access-point-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'mesh-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'mesh-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._activities = {} + self._buddies = {} + self._access_points = {} + self._mesh = None + + self._pservice = presenceservice.get_instance() + self._pservice.connect("activity-appeared", + self._activity_appeared_cb) + self._pservice.connect('activity-disappeared', + self._activity_disappeared_cb) + self._pservice.connect("buddy-appeared", + self._buddy_appeared_cb) + self._pservice.connect("buddy-disappeared", + self._buddy_disappeared_cb) + + # Add any buddies the PS knows about already + self._pservice.get_buddies_async(reply_handler=self._get_buddies_cb) + + self._pservice.get_activities_async(reply_handler=self._get_activities_cb) + + network_manager = hardwaremanager.get_network_manager() + if network_manager: + for nm_device in network_manager.get_devices(): + self._add_network_device(nm_device) + network_manager.connect('device-added', + self._nm_device_added_cb) + network_manager.connect('device-removed', + self._nm_device_removed_cb) + + def _get_buddies_cb(self, list): + for buddy in list: + self._buddy_appeared_cb(self._pservice, buddy) + + def _get_activities_cb(self, list): + for activity in list: + self._check_activity(activity) + + def _nm_device_added_cb(self, manager, nm_device): + self._add_network_device(nm_device) + + def _nm_device_removed_cb(self, manager, nm_device): + self._remove_network_device(nm_device) + + def _nm_network_appeared_cb(self, nm_device, nm_network): + self._add_access_point(nm_device, nm_network) + + def _nm_network_disappeared_cb(self, nm_device, nm_network): + if self._access_points.has_key(nm_network.get_op()): + ap = self._access_points[nm_network.get_op()] + self._remove_access_point(ap) + + def _add_network_device(self, nm_device): + dtype = nm_device.get_type() + if dtype == nmclient.DEVICE_TYPE_802_11_WIRELESS: + for nm_network in nm_device.get_networks(): + self._add_access_point(nm_device, nm_network) + + nm_device.connect('network-appeared', + self._nm_network_appeared_cb) + nm_device.connect('network-disappeared', + self._nm_network_disappeared_cb) + elif dtype == nmclient.DEVICE_TYPE_802_11_MESH_OLPC: + self._mesh = nm_device + self.emit('mesh-added', self._mesh) + + def _remove_network_device(self, nm_device): + if nm_device == self._mesh: + self._mesh = None + self.emit('mesh-removed') + elif nm_device.get_type() == nmclient.DEVICE_TYPE_802_11_WIRELESS: + aplist = self._access_points.values() + for ap in aplist: + if ap.get_nm_device() == nm_device: + self._remove_access_point(ap) + + def _add_access_point(self, nm_device, nm_network): + model = AccessPointModel(nm_device, nm_network) + self._access_points[model.get_id()] = model + self.emit('access-point-added', model) + + def _remove_access_point(self, ap): + if not self._access_points.has_key(ap.get_id()): + return + self.emit('access-point-removed', ap) + del self._access_points[ap.get_id()] + + def get_mesh(self): + return self._mesh + + def get_access_points(self): + return self._access_points.values() + + def get_activities(self): + return self._activities.values() + + def get_buddies(self): + return self._buddies.values() + + def _buddy_activity_changed_cb(self, model, cur_activity): + if not self._buddies.has_key(model.get_key()): + return + if cur_activity and self._activities.has_key(cur_activity.props.id): + activity_model = self._activities[cur_activity.props.id] + self.emit('buddy-moved', model, activity_model) + else: + self.emit('buddy-moved', model, None) + + def _buddy_appeared_cb(self, pservice, buddy): + if self._buddies.has_key(buddy.props.key): + return + + model = BuddyModel(buddy=buddy) + model.connect('current-activity-changed', + self._buddy_activity_changed_cb) + self._buddies[buddy.props.key] = model + self.emit('buddy-added', model) + + cur_activity = buddy.props.current_activity + if cur_activity: + self._buddy_activity_changed_cb(model, cur_activity) + + def _buddy_disappeared_cb(self, pservice, buddy): + if not self._buddies.has_key(buddy.props.key): + return + self.emit('buddy-removed', self._buddies[buddy.props.key]) + del self._buddies[buddy.props.key] + + def _activity_appeared_cb(self, pservice, activity): + self._check_activity(activity) + + def _check_activity(self, presence_activity): + registry = activity.get_registry() + bundle = registry.get_activity(presence_activity.props.type) + if not bundle: + return + if self.has_activity(presence_activity.props.id): + return + self.add_activity(bundle, presence_activity) + + def has_activity(self, activity_id): + return self._activities.has_key(activity_id) + + def get_activity(self, activity_id): + if self.has_activity(activity_id): + return self._activities[activity_id] + else: + return None + + def add_activity(self, bundle, activity): + model = ActivityModel(activity, bundle) + self._activities[model.get_id()] = model + self.emit('activity-added', model) + + for buddy in self._pservice.get_buddies(): + cur_activity = buddy.props.current_activity + key = buddy.props.key + if cur_activity == activity and self._buddies.has_key(key): + buddy_model = self._buddies[key] + self.emit('buddy-moved', buddy_model, model) + + def _activity_disappeared_cb(self, pservice, activity): + if self._activities.has_key(activity.props.id): + activity_model = self._activities[activity.props.id] + self.emit('activity-removed', activity_model) + del self._activities[activity.props.id] diff --git a/src/model/Owner.py b/src/model/Owner.py new file mode 100644 index 0000000..b06b391 --- /dev/null +++ b/src/model/Owner.py @@ -0,0 +1,87 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gobject +import os +import random +import base64 +import time +import logging +import dbus + +from sugar import env +from sugar import profile +from sugar.presence import presenceservice +from sugar import util +from model.Invites import Invites + +class ShellOwner(gobject.GObject): + __gtype_name__ = "ShellOwner" + + __gsignals__ = { + 'nick-changed' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_STRING])), + 'color-changed' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'icon-changed' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + """Class representing the owner of this machine/instance. This class + runs in the shell and serves up the buddy icon and other stuff. It's the + server portion of the Owner, paired with the client portion in Buddy.py.""" + def __init__(self): + gobject.GObject.__init__(self) + + self._nick = profile.get_nick_name() + + self._icon = None + self._icon_hash = "" + icon = os.path.join(env.get_profile_path(), "buddy-icon.jpg") + if not os.path.exists(icon): + raise RuntimeError("missing buddy icon") + + fd = open(icon, "r") + self._icon = fd.read() + fd.close() + if not self._icon: + raise RuntimeError("invalid buddy icon") + + # Get the icon's hash + import md5 + digest = md5.new(self._icon).digest() + self._icon_hash = util.printable_hash(digest) + + self._pservice = presenceservice.get_instance() + self._pservice.connect('activity-invitation', + self._activity_invitation_cb) + self._pservice.connect('activity-disappeared', + self._activity_disappeared_cb) + + self._invites = Invites() + + def get_invites(self): + return self._invites + + def get_nick(self): + return self._nick + + def _activity_invitation_cb(self, pservice, activity, buddy, message): + self._invites.add_invite(buddy, activity.props.type, + activity.props.id) + + def _activity_disappeared_cb(self, pservice, activity): + self._invites.remove_activity(activity.props.id) diff --git a/src/model/__init__.py b/src/model/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/model/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 + diff --git a/src/model/accesspointmodel.py b/src/model/accesspointmodel.py new file mode 100644 index 0000000..1d4d6cb --- /dev/null +++ b/src/model/accesspointmodel.py @@ -0,0 +1,81 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gobject +import sys + +from hardware import nmclient + +STATE_CONNECTING = 0 +STATE_CONNECTED = 1 +STATE_NOTCONNECTED = 2 + +_nm_state_to_state = { + nmclient.NETWORK_STATE_CONNECTED : STATE_CONNECTED, + nmclient.NETWORK_STATE_CONNECTING : STATE_CONNECTING, + nmclient.NETWORK_STATE_NOTCONNECTED : STATE_NOTCONNECTED +} + +class AccessPointModel(gobject.GObject): + __gproperties__ = { + 'name' : (str, None, None, None, + gobject.PARAM_READABLE), + 'strength' : (int, None, None, 0, 100, 0, + gobject.PARAM_READABLE), + 'state' : (int, None, None, STATE_CONNECTING, + STATE_NOTCONNECTED, 0, gobject.PARAM_READABLE), + 'capabilities' : (int, None, None, 0, 0x7FFFFFFF, 0, + gobject.PARAM_READABLE), + 'mode' : (int, None, None, 0, 6, 0, gobject.PARAM_READABLE) + } + + def __init__(self, nm_device, nm_network): + gobject.GObject.__init__(self) + self._nm_network = nm_network + self._nm_device = nm_device + + self._nm_network.connect('strength-changed', + self._strength_changed_cb) + self._nm_network.connect('state-changed', + self._state_changed_cb) + + def _strength_changed_cb(self, nm_network): + self.notify('strength') + + def _state_changed_cb(self, nm_network): + self.notify('state') + + def get_id(self): + return self._nm_network.get_op() + + def get_nm_device(self): + return self._nm_device + + def get_nm_network(self): + return self._nm_network + + def do_get_property(self, pspec): + if pspec.name == 'strength': + return self._nm_network.get_strength() + elif pspec.name == 'name': + return self._nm_network.get_ssid() + elif pspec.name == 'state': + nm_state = self._nm_network.get_state() + return _nm_state_to_state[nm_state] + elif pspec.name == 'capabilities': + return self._nm_network.get_caps() + elif pspec.name == 'mode': + return self._nm_network.get_mode() diff --git a/src/model/devices/Makefile.am b/src/model/devices/Makefile.am new file mode 100644 index 0000000..5440eeb --- /dev/null +++ b/src/model/devices/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = network + +sugardir = $(pkgdatadir)/shell/model/devices +sugar_PYTHON = \ + __init__.py \ + device.py \ + devicesmodel.py \ + battery.py diff --git a/src/model/devices/__init__.py b/src/model/devices/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/model/devices/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 + diff --git a/src/model/devices/battery.py b/src/model/devices/battery.py new file mode 100644 index 0000000..853d00e --- /dev/null +++ b/src/model/devices/battery.py @@ -0,0 +1,96 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 logging + +import gobject +import dbus + +from model.devices import device + +_LEVEL_PROP = 'battery.charge_level.percentage' +_CHARGING_PROP = 'battery.rechargeable.is_charging' +_DISCHARGING_PROP = 'battery.rechargeable.is_discharging' + +class Device(device.Device): + __gproperties__ = { + 'level' : (int, None, None, 0, 100, 0, + gobject.PARAM_READABLE), + 'charging' : (bool, None, None, False, + gobject.PARAM_READABLE), + 'discharging' : (bool, None, None, False, + gobject.PARAM_READABLE) + } + + def __init__(self, udi): + device.Device.__init__(self, udi) + + bus = dbus.Bus(dbus.Bus.TYPE_SYSTEM) + proxy = bus.get_object('org.freedesktop.Hal', udi) + self._battery = dbus.Interface(proxy, 'org.freedesktop.Hal.Device') + bus.add_signal_receiver(self._battery_changed, + 'PropertyModified', + 'org.freedesktop.Hal.Device', + 'org.freedesktop.Hal', + udi) + + self._level = self._get_level() + self._charging = self._get_charging() + self._discharging = self._get_discharging() + + def _get_level(self): + try: + return self._battery.GetProperty(_LEVEL_PROP) + except dbus.DBusException: + logging.error('Cannot access %s' % _LEVEL_PROP) + return 0 + + def _get_charging(self): + try: + return self._battery.GetProperty(_CHARGING_PROP) + except dbus.DBusException: + logging.error('Cannot access %s' % _CHARGING_PROP) + return False + + def _get_discharging(self): + try: + return self._battery.GetProperty(_DISCHARGING_PROP) + except dbus.DBusException: + logging.error('Cannot access %s' % _DISCHARGING_PROP) + return False + + def do_get_property(self, pspec): + if pspec.name == 'level': + return self._level + if pspec.name == 'charging': + return self._charging + if pspec.name == 'discharging': + return self._discharging + + def get_type(self): + return 'battery' + + def _battery_changed(self, num_changes, changes_list): + for change in changes_list: + if change[0] == _LEVEL_PROP: + self._level = self._get_level() + self.notify('level') + elif change[0] == _CHARGING_PROP: + self._charging = self._get_charging() + self.notify('charging') + elif change[0] == _DISCHARGING_PROP: + self._discharging = self._get_discharging() + self.notify('discharging') diff --git a/src/model/devices/device.py b/src/model/devices/device.py new file mode 100644 index 0000000..d7105b5 --- /dev/null +++ b/src/model/devices/device.py @@ -0,0 +1,45 @@ +# +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 gobject +from hardware import nmclient + +from sugar import util + +STATE_ACTIVATING = 0 +STATE_ACTIVATED = 1 +STATE_INACTIVE = 2 + +_nm_state_to_state = { + nmclient.DEVICE_STATE_ACTIVATING : STATE_ACTIVATING, + nmclient.DEVICE_STATE_ACTIVATED : STATE_ACTIVATED, + nmclient.DEVICE_STATE_INACTIVE : STATE_INACTIVE +} + +class Device(gobject.GObject): + def __init__(self, device_id=None): + gobject.GObject.__init__(self) + if device_id: + self._id = device_id + else: + self._id = util.unique_id() + + def get_type(self): + return 'unknown' + + def get_id(self): + return self._id diff --git a/src/model/devices/devicesmodel.py b/src/model/devices/devicesmodel.py new file mode 100644 index 0000000..fab9fa4 --- /dev/null +++ b/src/model/devices/devicesmodel.py @@ -0,0 +1,136 @@ +# +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 logging +import gobject +import dbus + +from model.devices import device +from model.devices.network import wired +from model.devices.network import wireless +from model.devices.network import mesh +from model.devices import battery +from hardware import hardwaremanager +from hardware import nmclient + +class DevicesModel(gobject.GObject): + __gsignals__ = { + 'device-appeared' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'device-disappeared': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._devices = {} + self._sigids = {} + + self._observe_hal_manager() + self._observe_network_manager() + + def _observe_hal_manager(self): + bus = dbus.Bus(dbus.Bus.TYPE_SYSTEM) + proxy = bus.get_object('org.freedesktop.Hal', + '/org/freedesktop/Hal/Manager') + hal_manager = dbus.Interface(proxy, 'org.freedesktop.Hal.Manager') + + for udi in hal_manager.FindDeviceByCapability('battery'): + self.add_device(battery.Device(udi)) + + def _observe_network_manager(self): + network_manager = hardwaremanager.get_network_manager() + if not network_manager: + return + + for device in network_manager.get_devices(): + self._check_network_device(device) + + network_manager.connect('device-added', + self._network_device_added_cb) + network_manager.connect('device-activating', + self._network_device_activating_cb) + network_manager.connect('device-activated', + self._network_device_activated_cb) + network_manager.connect('device-removed', + self._network_device_removed_cb) + + def _network_device_added_cb(self, network_manager, nm_device): + state = nm_device.get_state() + if state == nmclient.DEVICE_STATE_ACTIVATING \ + or state == nmclient.DEVICE_STATE_ACTIVATED: + self._check_network_device(nm_device) + + def _network_device_activating_cb(self, network_manager, nm_device): + self._check_network_device(nm_device) + + def _network_device_activated_cb(self, network_manager, nm_device): + pass + + def _network_device_removed_cb(self, network_manager, nm_device): + if self._devices.has_key(str(nm_device.get_op())): + self.remove_device(self._get_network_device(nm_device)) + + def _check_network_device(self, nm_device): + if not nm_device.is_valid(): + logging.debug("Device %s not valid" % nm_device.get_op()) + return + + dtype = nm_device.get_type() + if dtype == nmclient.DEVICE_TYPE_802_11_WIRELESS \ + or dtype == nmclient.DEVICE_TYPE_802_11_MESH_OLPC: + self._add_network_device(nm_device) + + def _get_network_device(self, nm_device): + return self._devices[str(nm_device.get_op())] + + def _network_device_state_changed_cb(self, dev, param): + if dev.props.state == device.STATE_INACTIVE: + self.remove_device(dev) + + def _add_network_device(self, nm_device): + if self._devices.has_key(str(nm_device.get_op())): + logging.debug("Tried to add device %s twice" % nm_device.get_op()) + return + + dtype = nm_device.get_type() + if dtype == nmclient.DEVICE_TYPE_802_11_WIRELESS: + dev = wireless.Device(nm_device) + self.add_device(dev) + sigid = dev.connect('notify::state', self._network_device_state_changed_cb) + self._sigids[dev] = sigid + if dtype == nmclient.DEVICE_TYPE_802_11_MESH_OLPC: + dev = mesh.Device(nm_device) + self.add_device(dev) + sigid = dev.connect('notify::state', self._network_device_state_changed_cb) + self._sigids[dev] = sigid + + def __iter__(self): + return iter(self._devices.values()) + + def add_device(self, device): + self._devices[device.get_id()] = device + self.emit('device-appeared', device) + + def remove_device(self, device): + self.emit('device-disappeared', self._devices[device.get_id()]) + device.disconnect(self._sigids[device]) + del self._sigids[device] + del self._devices[device.get_id()] diff --git a/src/model/devices/network/Makefile.am b/src/model/devices/network/Makefile.am new file mode 100644 index 0000000..04074e5 --- /dev/null +++ b/src/model/devices/network/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pkgdatadir)/shell/model/devices/network +sugar_PYTHON = \ + __init__.py \ + mesh.py \ + wired.py \ + wireless.py diff --git a/src/model/devices/network/__init__.py b/src/model/devices/network/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/model/devices/network/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 + diff --git a/src/model/devices/network/mesh.py b/src/model/devices/network/mesh.py new file mode 100644 index 0000000..0152d8a --- /dev/null +++ b/src/model/devices/network/mesh.py @@ -0,0 +1,75 @@ +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gobject + +from model.devices import device +from hardware import nmclient + +class Device(device.Device): + __gproperties__ = { + 'strength' : (int, None, None, 0, 100, 0, + gobject.PARAM_READABLE), + 'state' : (int, None, None, device.STATE_ACTIVATING, + device.STATE_INACTIVE, 0, gobject.PARAM_READABLE), + 'activation-stage': (int, None, None, 0, 7, 0, gobject.PARAM_READABLE), + 'frequency': (float, None, None, 0, 2.72, 0, gobject.PARAM_READABLE), + 'mesh-step': (int, None, None, 0, 4, 0, gobject.PARAM_READABLE), + } + + def __init__(self, nm_device): + device.Device.__init__(self) + self._nm_device = nm_device + + self._nm_device.connect('strength-changed', + self._strength_changed_cb) + self._nm_device.connect('state-changed', + self._state_changed_cb) + self._nm_device.connect('activation-stage-changed', + self._activation_stage_changed_cb) + + def _strength_changed_cb(self, nm_device): + self.notify('strength') + + def _state_changed_cb(self, nm_device): + self.notify('state') + + def _activation_stage_changed_cb(self, nm_device): + self.notify('activation-stage') + + def do_get_property(self, pspec): + if pspec.name == 'strength': + return self._nm_device.get_strength() + elif pspec.name == 'state': + nm_state = self._nm_device.get_state() + return device._nm_state_to_state[nm_state] + elif pspec.name == 'activation-stage': + return self._nm_device.get_activation_stage() + elif pspec.name == 'frequency': + return self._nm_device.get_frequency() + elif pspec.name == 'mesh-step': + return self._nm_device.get_mesh_step() + + def get_type(self): + return 'network.mesh' + + def get_id(self): + return str(self._nm_device.get_op()) + + def get_nm_device(self): + return self._nm_device + diff --git a/src/model/devices/network/wired.py b/src/model/devices/network/wired.py new file mode 100644 index 0000000..66c5206 --- /dev/null +++ b/src/model/devices/network/wired.py @@ -0,0 +1,28 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 model.devices import device + +class Device(device.Device): + def __init__(self, nm_device): + device.Device.__init__(self) + self._nm_device = device + + def get_id(self): + return str(self._nm_device.get_op()) + + def get_type(self): + return 'network.wired' diff --git a/src/model/devices/network/wireless.py b/src/model/devices/network/wireless.py new file mode 100644 index 0000000..c45a08e --- /dev/null +++ b/src/model/devices/network/wireless.py @@ -0,0 +1,96 @@ +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gobject + +from model.devices import device +from hardware import nmclient + +def freq_to_channel(freq): + ftoc = { 2.412: 1, 2.417: 2, 2.422: 3, 2.427: 4, + 2.432: 5, 2.437: 6, 2.442: 7, 2.447: 8, + 2.452: 9, 2.457: 10, 2.462: 11, 2.467: 12, + 2.472: 13 + } + return ftoc[freq] + +def channel_to_freq(channel): + ctof = { 1: 2.412, 2: 2.417, 3: 2.422, 4: 2.427, + 5: 2.432, 6: 2.437, 7: 2.442, 8: 2.447, + 9: 2.452, 10: 2.457, 11: 2.462, 12: 2.467, + 13: 2.472 + } + return ctof[channel] + + +class Device(device.Device): + __gproperties__ = { + 'name' : (str, None, None, None, + gobject.PARAM_READABLE), + 'strength' : (int, None, None, 0, 100, 0, + gobject.PARAM_READABLE), + 'state' : (int, None, None, device.STATE_ACTIVATING, + device.STATE_INACTIVE, 0, gobject.PARAM_READABLE), + 'frequency': (float, None, None, 0.0, 9999.99, 0.0, + gobject.PARAM_READABLE) + } + + def __init__(self, nm_device): + device.Device.__init__(self) + self._nm_device = nm_device + + self._nm_device.connect('strength-changed', + self._strength_changed_cb) + self._nm_device.connect('ssid-changed', + self._ssid_changed_cb) + self._nm_device.connect('state-changed', + self._state_changed_cb) + + def _strength_changed_cb(self, nm_device): + self.notify('strength') + + def _ssid_changed_cb(self, nm_device): + self.notify('name') + + def _state_changed_cb(self, nm_device): + self.notify('state') + + def do_get_property(self, pspec): + if pspec.name == 'strength': + return self._nm_device.get_strength() + elif pspec.name == 'name': + import logging + logging.debug('wireless.Device.props.name: %s' % self._nm_device.get_ssid()) + return self._nm_device.get_ssid() + elif pspec.name == 'state': + nm_state = self._nm_device.get_state() + return device._nm_state_to_state[nm_state] + elif pspec.name == 'frequency': + return self._nm_device.get_frequency() + + def get_type(self): + return 'network.wireless' + + def get_id(self): + return str(self._nm_device.get_op()) + + def get_active_network_colors(self): + net = self._nm_device.get_active_network() + if not net: + return (None, None) + return net.get_colors() + diff --git a/src/model/homeactivity.py b/src/model/homeactivity.py new file mode 100644 index 0000000..7365271 --- /dev/null +++ b/src/model/homeactivity.py @@ -0,0 +1,215 @@ +# Copyright (C) 2006-2007 Owen Williams. +# +# 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 time +import logging + +import gobject +import dbus + +from sugar.graphics.xocolor import XoColor +from sugar.presence import presenceservice +from sugar import profile + +_SERVICE_NAME = "org.laptop.Activity" +_SERVICE_PATH = "/org/laptop/Activity" +_SERVICE_INTERFACE = "org.laptop.Activity" + +class HomeActivity(gobject.GObject): + """Activity which appears in the "Home View" of the Sugar shell + + This class stores the Sugar Shell's metadata regarding a + given activity/application in the system. It interacts with + the sugar.activity.* modules extensively in order to + accomplish its tasks. + """ + + __gtype_name__ = 'SugarHomeActivity' + + __gproperties__ = { + 'launching' : (bool, None, None, False, + gobject.PARAM_READWRITE), + } + + def __init__(self, activity_info, activity_id): + """Initialise the HomeActivity + + activity_info -- sugar.activity.registry.ActivityInfo instance, + provides the information required to actually + create the new instance. This is, in effect, + the "type" of activity being created. + activity_id -- unique identifier for this instance + of the activity type + """ + gobject.GObject.__init__(self) + + self._window = None + self._xid = None + self._pid = None + self._service = None + self._activity_id = activity_id + self._activity_info = activity_info + self._launch_time = time.time() + self._launching = False + + self._retrieve_service() + + if not self._service: + bus = dbus.SessionBus() + bus.add_signal_receiver(self._name_owner_changed_cb, + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus") + + def set_window(self, window): + """An activity is 'launched' once we get its window.""" + if self._window or self._xid: + raise RuntimeError("Activity is already launched!") + if not window: + raise ValueError("window must be valid") + + self._window = window + self._xid = window.get_xid() + self._pid = window.get_pid() + + def get_service(self): + """Get the activity service + + Note that non-native Sugar applications will not have + such a service, so the return value will be None in + those cases. + """ + + return self._service + + def get_title(self): + """Retrieve the application's root window's suggested title""" + if self._window: + return self._window.get_name() + else: + return '' + + def get_icon_path(self): + """Retrieve the activity's icon (file) name""" + if self._activity_info: + return self._activity_info.icon + else: + return None + + def get_icon_color(self): + """Retrieve the appropriate icon colour for this activity + + Uses activity_id to index into the PresenceService's + set of activity colours, if the PresenceService does not + have an entry (implying that this is not a Sugar-shared application) + uses the local user's profile.get_color() to determine the + colour for the icon. + """ + pservice = presenceservice.get_instance() + + # HACK to suppress warning in logs when activity isn't found + # (if it's locally launched and not shared yet) + activity = None + for act in pservice.get_activities(): + if self._activity_id == act.props.id: + activity = act + break + + if activity != None: + return XoColor(activity.props.color) + else: + return profile.get_color() + + def get_activity_id(self): + """Retrieve the "activity_id" passed in to our constructor + + This is a "globally likely unique" identifier generated by + sugar.util.unique_id + """ + return self._activity_id + + def get_xid(self): + """Retrieve the X-windows ID of our root window""" + return self._xid + + def get_window(self): + """Retrieve the X-windows root window of this application + + This was stored by the set_window method, which was + called by HomeModel._add_activity, which was called + via a callback that looks for all 'window-opened' + events. + + HomeModel currently uses a dbus service query on the + activity to determine to which HomeActivity the newly + launched window belongs. + """ + return self._window + + def get_type(self): + """Retrieve the activity bundle id for future reference""" + if self._activity_info: + return self._activity_info.bundle_id + else: + return None + + def get_launch_time(self): + """Return the time at which the activity was first launched + + Format is floating-point time.time() value + (seconds since the epoch) + """ + return self._launch_time + + def get_pid(self): + """Returns the activity's PID""" + return self._pid + + def equals(self, activity): + if self._activity_id and activity.get_activity_id(): + return self._activity_id == activity.get_activity_id() + if self._xid and activity.get_xid(): + return self._xid == activity.get_xid() + return False + + def do_set_property(self, pspec, value): + if pspec.name == 'launching': + self._launching = value + + def do_get_property(self, pspec): + if pspec.name == 'launching': + return self._launching + + def _get_service_name(self): + if self._activity_id: + return _SERVICE_NAME + self._activity_id + else: + return None + + def _retrieve_service(self): + if not self._activity_id: + return + + try: + bus = dbus.SessionBus() + proxy = bus.get_object(self._get_service_name(), + _SERVICE_PATH + "/" + self._activity_id) + self._service = dbus.Interface(proxy, _SERVICE_INTERFACE) + except dbus.DBusException: + self._service = None + + def _name_owner_changed_cb(self, name, old, new): + if name == self._get_service_name(): + self._retrieve_service() diff --git a/src/model/homemodel.py b/src/model/homemodel.py new file mode 100644 index 0000000..44d5417 --- /dev/null +++ b/src/model/homemodel.py @@ -0,0 +1,283 @@ +# Copyright (C) 2006-2007 Owen Williams. +# +# 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 logging + +import gobject +import wnck +import dbus + +from sugar import wm +from sugar import activity + +from model.homeactivity import HomeActivity + +class HomeModel(gobject.GObject): + """Model of the "Home" view (activity management) + + The HomeModel is basically the point of registration + for all running activities within Sugar. It traps + events that tell the system there is a new activity + being created (generated by the activity factories), + or removed, as well as those which tell us that the + currently focussed activity has changed. + + The HomeModel tracks a set of HomeActivity instances, + which are tracking the window to activity mappings + the activity factories have set up. + """ + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-started': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'active-activity-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'pending-activity-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._activities = [] + self._active_activity = None + self._pending_activity = None + + screen = wnck.screen_get_default() + screen.connect('window-opened', self._window_opened_cb) + screen.connect('window-closed', self._window_closed_cb) + screen.connect('active-window-changed', + self._active_window_changed_cb) + + def _get_activities_with_window(self): + ret = [] + for i in self._activities: + if i.get_window() is not None: + ret.append(i) + return ret + + def get_previous_activity(self): + activities = self._get_activities_with_window() + i = activities.index(self._pending_activity) + if len(activities) == 0: + return None + elif i - 1 >= 0: + return activities[i - 1] + else: + return activities[len(activities) - 1] + + def get_next_activity(self): + activities = self._get_activities_with_window() + i = activities.index(self._pending_activity) + if len(activities) == 0: + return None + elif i + 1 < len(activities): + return activities[i + 1] + else: + return activities[0] + + def get_pending_activity(self): + """Returns the activity that would be seen in the Activity zoom level + + In the Home (or Neighborhood or Groups) zoom level, this + indicates the activity that would become active if the user + switched to the Activity zoom level. (In the Activity zoom + level, this just returns the currently-active activity.) + Unlike get_active_activity(), this never returns None as long + as there is any activity running. + """ + return self._pending_activity + + def _set_pending_activity(self, home_activity): + if self._pending_activity == home_activity: + return + + self._pending_activity = home_activity + self.emit('pending-activity-changed', self._pending_activity) + + def get_active_activity(self): + """Returns the activity that the user is currently working in + + In the Activity zoom level, this returns the currently-active + activity. In the other zoom levels, it returns the activity + that was most-recently active in the Activity zoom level, or + None if the most-recently-active activity is no longer + running. + """ + return self._active_activity + + def _set_active_activity(self, home_activity): + if self._active_activity == home_activity: + return + + if self._active_activity: + service = self._active_activity.get_service() + if service: + service.SetActive(False, + reply_handler=self._set_active_success, + error_handler=self._set_active_error) + if home_activity: + service = home_activity.get_service() + if service: + service.SetActive(True, + reply_handler=self._set_active_success, + error_handler=self._set_active_error) + + self._active_activity = home_activity + self.emit('active-activity-changed', self._active_activity) + + def __iter__(self): + return iter(self._activities) + + def __len__(self): + return len(self._activities) + + def __getitem__(self, i): + return self._activities[i] + + def index(self, obj): + return self._activities.index(obj) + + def _window_opened_cb(self, screen, window): + if window.get_window_type() == wnck.WINDOW_NORMAL: + home_activity = None + + activity_id = wm.get_activity_id(window) + + service_name = wm.get_bundle_id(window) + if service_name: + registry = activity.get_registry() + activity_info = registry.get_activity(service_name) + else: + activity_info = None + + if activity_id: + home_activity = self._get_activity_by_id(activity_id) + + if not home_activity: + home_activity = HomeActivity(activity_info, activity_id) + self._add_activity(home_activity) + + home_activity.set_window(window) + + home_activity.props.launching = False + self.emit('activity-started', home_activity) + + if self._pending_activity is None: + self._set_pending_activity(home_activity) + + def _window_closed_cb(self, screen, window): + if window.get_window_type() == wnck.WINDOW_NORMAL: + self._remove_activity_by_xid(window.get_xid()) + + def _get_activity_by_xid(self, xid): + for home_activity in self._activities: + if home_activity.get_xid() == xid: + return home_activity + return None + + def _get_activity_by_id(self, activity_id): + for home_activity in self._activities: + if home_activity.get_activity_id() == activity_id: + return home_activity + return None + + def _set_active_success(self): + pass + + def _set_active_error(self, err): + logging.error("set_active() failed: %s" % err) + + def _active_window_changed_cb(self, screen, previous_window=None): + window = screen.get_active_window() + if window is None: + return + + if window.get_window_type() != wnck.WINDOW_DIALOG: + while window.get_transient() is not None: + window = window.get_transient() + + activity = self._get_activity_by_xid(window.get_xid()) + if activity is not None: + self._set_pending_activity(activity) + self._set_active_activity(activity) + + def _add_activity(self, home_activity): + self._activities.append(home_activity) + self.emit('activity-added', home_activity) + + def _remove_activity(self, home_activity): + if home_activity == self._active_activity: + self._set_active_activity(None) + + if home_activity == self._pending_activity: + # Figure out the new _pending_activity + windows = wnck.screen_get_default().get_windows_stacked() + windows.reverse() + for window in windows: + new_activity = self._get_activity_by_xid(window.get_xid()) + if new_activity is not None: + self._set_pending_activity(new_activity) + break + else: + logging.error('No activities are running') + self._set_pending_activity(None) + + self.emit('activity-removed', home_activity) + self._activities.remove(home_activity) + + def _remove_activity_by_xid(self, xid): + home_activity = self._get_activity_by_xid(xid) + if home_activity: + self._remove_activity(home_activity) + else: + logging.error('Model for window %d does not exist.' % xid) + + def notify_activity_launch(self, activity_id, service_name): + registry = activity.get_registry() + activity_info = registry.get_activity(service_name) + if not activity_info: + raise ValueError("Activity service name '%s' was not found in the bundle registry." % service_name) + home_activity = HomeActivity(activity_info, activity_id) + home_activity.props.launching = True + self._add_activity(home_activity) + + # FIXME: better learn about finishing processes by receiving a signal. + # Now just check whether an activity has a window after ~90sec + gobject.timeout_add(90000, self._check_activity_launched, activity_id) + + def notify_activity_launch_failed(self, activity_id): + home_activity = self._get_activity_by_id(activity_id) + if home_activity: + logging.debug("Activity %s (%s) launch failed" % (activity_id, home_activity.get_type())) + self._remove_activity(home_activity) + else: + logging.error('Model for activity id %s does not exist.' % activity_id) + + def _check_activity_launched(self, activity_id): + home_activity = self._get_activity_by_id(activity_id) + if home_activity and home_activity.props.launching: + logging.debug('Activity %s still launching, assuming it failed...', activity_id) + self.notify_activity_launch_failed(activity_id) + return False diff --git a/src/model/shellmodel.py b/src/model/shellmodel.py new file mode 100644 index 0000000..5462e27 --- /dev/null +++ b/src/model/shellmodel.py @@ -0,0 +1,112 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 wnck +import gobject + +from sugar.presence import presenceservice +from model.Friends import Friends +from model.MeshModel import MeshModel +from model.homemodel import HomeModel +from model.Owner import ShellOwner +from model.devices.devicesmodel import DevicesModel +from sugar import env + +class ShellModel(gobject.GObject): + STATE_STARTUP = 0 + STATE_RUNNING = 1 + STATE_SHUTDOWN = 2 + + ZOOM_MESH = 0 + ZOOM_FRIENDS = 1 + ZOOM_HOME = 2 + ZOOM_ACTIVITY = 3 + + __gproperties__ = { + 'state' : (int, None, None, + 0, 2, STATE_RUNNING, + gobject.PARAM_READWRITE), + 'zoom-level' : (int, None, None, + 0, 3, ZOOM_HOME, + gobject.PARAM_READABLE) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._current_activity = None + self._state = self.STATE_RUNNING + self._zoom_level = self.ZOOM_HOME + self._showing_desktop = True + + self._pservice = presenceservice.get_instance() + + self._owner = ShellOwner() + + self._friends = Friends() + self._mesh = MeshModel() + self._home = HomeModel() + self._devices = DevicesModel() + + self._screen = wnck.screen_get_default() + self._screen.connect('showing-desktop-changed', + self._showing_desktop_changed_cb) + + def set_zoom_level(self, level): + self._zoom_level = level + self.notify('zoom-level') + + def get_zoom_level(self): + if self._screen.get_showing_desktop(): + return self._zoom_level + else: + return self.ZOOM_ACTIVITY + + def do_set_property(self, pspec, value): + if pspec.name == 'state': + self._state = value + + def do_get_property(self, pspec): + if pspec.name == 'state': + return self._state + elif pspec.name == 'zoom-level': + return self.get_zoom_level() + + def get_mesh(self): + return self._mesh + + def get_friends(self): + return self._friends + + def get_invites(self): + return self._owner.get_invites() + + def get_home(self): + return self._home + + def get_owner(self): + return self._owner + + def get_devices(self): + return self._devices + + def _showing_desktop_changed_cb(self, screen): + showing_desktop = self._screen.get_showing_desktop() + if self._showing_desktop != showing_desktop: + self._showing_desktop = showing_desktop + self.notify('zoom-level') diff --git a/src/shellservice.py b/src/shellservice.py new file mode 100644 index 0000000..458f941 --- /dev/null +++ b/src/shellservice.py @@ -0,0 +1,130 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 + +"""D-bus service providing access to the shell's functionality""" +import dbus +import os + +_DBUS_SERVICE = "org.laptop.Shell" +_DBUS_SHELL_IFACE = "org.laptop.Shell" +_DBUS_OWNER_IFACE = "org.laptop.Shell.Owner" +_DBUS_PATH = "/org/laptop/Shell" + +_DBUS_RAINBOW_IFACE = "org.laptop.security.Rainbow" + +class ShellService(dbus.service.Object): + """Provides d-bus service to script the shell's operations + + Uses a shell_model object to observe events such as changes to: + + * nickname + * colour + * icon + * currently active activity + + and pass the event off to the methods in the dbus signature. + + Key method here at the moment is add_bundle, which is used to + do a run-time registration of a bundle using it's application path. + + XXX At the moment the d-bus service methods do not appear to do + anything other than add_bundle + """ + + _rainbow = None + + def __init__(self, shell): + self._shell = shell + self._shell_model = shell.get_model() + + self._owner = self._shell_model.get_owner() + self._owner.connect('nick-changed', self._owner_nick_changed_cb) + self._owner.connect('icon-changed', self._owner_icon_changed_cb) + self._owner.connect('color-changed', self._owner_color_changed_cb) + + self._home_model = self._shell_model.get_home() + self._home_model.connect('active-activity-changed', + self._cur_activity_changed_cb) + + bus = dbus.SessionBus() + bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus) + dbus.service.Object.__init__(self, bus_name, _DBUS_PATH) + + @dbus.service.method(_DBUS_SHELL_IFACE, + in_signature="s", out_signature="b") + def ActivateActivity(self, activity_id): + host = self._shell.get_activity(activity_id) + if host: + host.present() + return True + + return False + + @dbus.service.method(_DBUS_SHELL_IFACE, + in_signature="ss", out_signature="") + def NotifyLaunch(self, bundle_id, activity_id): + self._shell.notify_launch(bundle_id, activity_id) + + @dbus.service.method(_DBUS_SHELL_IFACE, + in_signature="s", out_signature="") + def NotifyLaunchFailure(self, activity_id): + self._shell.notify_launch_failure(activity_id) + + @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s") + def ColorChanged(self, color): + pass + + def _owner_color_changed_cb(self, new_color): + self.ColorChanged(new_color.to_string()) + + @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s") + def NickChanged(self, nick): + pass + + def _owner_nick_changed_cb(self, new_nick): + self.NickChanged(new_nick) + + @dbus.service.signal(_DBUS_OWNER_IFACE, signature="ay") + def IconChanged(self, icon_data): + pass + + def _owner_icon_changed_cb(self, new_icon): + self.IconChanged(dbus.ByteArray(new_icon)) + + def _get_rainbow_service(self): + """Lazily initializes an interface to the Rainbow security daemon.""" + if self._rainbow is None: + system_bus = dbus.SystemBus() + object = system_bus.get_object(_DBUS_RAINBOW_IFACE, '/', + follow_name_owner_changes=True) + self._rainbow = dbus.Interface(object, + dbus_interface=_DBUS_RAINBOW_IFACE) + return self._rainbow + + @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s") + def CurrentActivityChanged(self, activity_id): + if os.path.exists('/etc/olpc-security'): + self._get_rainbow_service().ChangeActivity( + activity_id, + dbus_interface=_DBUS_RAINBOW_IFACE) + + def _cur_activity_changed_cb(self, owner, new_activity): + new_id = "" + if new_activity: + new_id = new_activity.get_activity_id() + if new_id: + self.CurrentActivityChanged(new_id) + diff --git a/src/view/ActivityHost.py b/src/view/ActivityHost.py new file mode 100644 index 0000000..4332372 --- /dev/null +++ b/src/view/ActivityHost.py @@ -0,0 +1,118 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gtk +import dbus +import logging + +from sugar.presence import presenceservice + +import OverlayWindow + +class ActivityChatWindow(gtk.Window): + def __init__(self, gdk_window, chat_widget): + gtk.Window.__init__(self) + + self.realize() + self.set_decorated(False) + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(True) + self.window.set_transient_for(gdk_window) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_default_size(600, 450) + + self.add(chat_widget) + +class ActivityHost: + def __init__(self, model): + self._model = model + self._window = model.get_window() + self._gdk_window = gtk.gdk.window_foreign_new(self.get_xid()) + + try: + self._overlay_window = OverlayWindow.OverlayWindow(self._gdk_window) + win = self._overlay_window.window + except RuntimeError: + self._overlay_window = None + win = self._gdk_window + + #self._chat_widget = ActivityChat.ActivityChat(self) + self._chat_widget = gtk.HBox() + self._chat_window = ActivityChatWindow(win, self._chat_widget) + + self._frame_was_visible = False + + def get_id(self): + return self._model.get_activity_id() + + def get_xid(self): + return self._window.get_xid() + + def get_model(self): + return self._model + + def invite(self, buddy_model): + service = self._model.get_service() + if service: + buddy = buddy_model.get_buddy() + service.Invite(buddy.props.key) + else: + logging.error('Invite failed, activity service not ') + + def toggle_fullscreen(self): + fullscreen = not self._window.is_fullscreen() + self._window.set_fullscreen(fullscreen) + + def present(self): + # wnck.Window.activate() expects a timestamp, but we don't + # always have one, and libwnck will complain if we pass "0", + # and matchbox doesn't look at the timestamp anyway. So we + # just always pass "1". + self._window.activate(1) + + def close(self): + # The "1" is a fake timestamp as with present() + self._window.close(1) + + def show_dialog(self, dialog): + dialog.show() + dialog.window.set_transient_for(self._gdk_window) + + def chat_show(self, frame_was_visible): + if self._overlay_window: + self._overlay_window.appear() + self._chat_window.show_all() + self._frame_was_visible = frame_was_visible + + def chat_hide(self): + self._chat_window.hide() + if self._overlay_window: + self._overlay_window.disappear() + wasvis = self._frame_was_visible + self._frame_was_visible = False + return wasvis + + def is_chat_visible(self): + return self._chat_window.get_property('visible') + + def set_active(self, active): + if not active: + self.chat_hide() + self._frame_was_visible = False + + def destroy(self): + self._chat_window.destroy() + self._frame_was_visible = False diff --git a/src/view/BuddyIcon.py b/src/view/BuddyIcon.py new file mode 100644 index 0000000..3734001 --- /dev/null +++ b/src/view/BuddyIcon.py @@ -0,0 +1,54 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 sugar.graphics.icon import CanvasIcon +from sugar.graphics.palette import Palette +from sugar.graphics import style + +from view.BuddyMenu import BuddyMenu + +class BuddyIcon(CanvasIcon): + def __init__(self, shell, buddy, size=style.STANDARD_ICON_SIZE): + CanvasIcon.__init__(self, icon_name='computer-xo', size=size) + + self._greyed_out = False + self._shell = shell + self._buddy = buddy + self._buddy.connect('appeared', self._buddy_presence_change_cb) + self._buddy.connect('disappeared', self._buddy_presence_change_cb) + self._buddy.connect('color-changed', self._buddy_presence_change_cb) + + palette = BuddyMenu(shell, buddy) + self.set_palette(palette) + + self._update_color() + + def _buddy_presence_change_cb(self, buddy, color=None): + # Update the icon's color when the buddy comes and goes + self._update_color() + + def _update_color(self): + if self._greyed_out: + self.props.stroke_color = '#D5D5D5' + self.props.fill_color = '#E5E5E5' + else: + self.props.xo_color = self._buddy.get_color() + + def set_filter(self, query): + self._greyed_out = (self._buddy.get_nick().lower().find(query) == -1) \ + and not self._buddy.is_owner() + self._update_color() + diff --git a/src/view/BuddyMenu.py b/src/view/BuddyMenu.py new file mode 100644 index 0000000..e7e12ca --- /dev/null +++ b/src/view/BuddyMenu.py @@ -0,0 +1,113 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 logging + +import gobject +import hippo + +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon +from sugar.presence import presenceservice + +class BuddyMenu(Palette): + def __init__(self, shell, buddy): + self._buddy = buddy + self._shell = shell + + Palette.__init__(self, buddy.get_nick()) + self._active_activity_changed_hid = None + self.connect('destroy', self.__destroy_cb) + + self._buddy.connect('icon-changed', self._buddy_icon_changed_cb) + self._buddy.connect('nick-changed', self._buddy_nick_changed_cb) + + owner = self._get_shell_model().get_owner() + if not buddy.is_owner(): + self._add_items() + + def _get_shell_model(self): + return self._shell.get_model() + + def _get_home_model(self): + return self._get_shell_model().get_home() + + def __destroy_cb(self, menu): + if self._active_activity_changed_hid is not None: + home_model = self._get_home_model() + home_model.disconnect(self._active_activity_changed_hid) + + def _add_items(self): + pservice = presenceservice.get_instance() + + friends = self._get_shell_model().get_friends() + if friends.has_buddy(self._buddy): + menu_item = MenuItem(_('Remove friend'), 'list-remove') + menu_item.connect('activate', self._remove_friend_cb) + else: + menu_item = MenuItem(_('Make friend'), 'list-add') + menu_item.connect('activate', self._make_friend_cb) + + self.menu.append(menu_item) + menu_item.show() + + self._invite_menu = MenuItem('') + self._invite_menu.connect('activate', self._invite_friend_cb) + self.menu.append(self._invite_menu) + + home_model = self._get_home_model() + self._active_activity_changed_hid = home_model.connect( + 'active-activity-changed', self._cur_activity_changed_cb) + activity = home_model.get_active_activity() + self._update_invite_menu(activity) + + def _update_invite_menu(self, activity): + if activity is None: + self._invite_menu.hide() + else: + title = activity.get_title() + label = self._invite_menu.get_children()[0] + label.set_text(_('Invite to %s') % title) + + icon = Icon(file=activity.get_icon_path()) + icon.props.xo_color = activity.get_icon_color() + self._invite_menu.set_image(icon) + icon.show() + + self._invite_menu.show() + + def _cur_activity_changed_cb(self, home_model, activity_model): + self._update_invite_menu(activity_model) + + def _buddy_icon_changed_cb(self, buddy): + pass + + def _buddy_nick_changed_cb(self, buddy, nick): + self.set_primary_text(nick) + + def _make_friend_cb(self, menuitem): + friends = self._get_shell_model().get_friends() + friends.make_friend(self._buddy) + + def _remove_friend_cb(self, menuitem): + friends = self._get_shell_model().get_friends() + friends.remove(self._buddy) + + def _invite_friend_cb(self, menuitem): + activity = self._shell.get_current_activity() + activity.invite(self._buddy) + diff --git a/src/view/Makefile.am b/src/view/Makefile.am new file mode 100644 index 0000000..abbb230 --- /dev/null +++ b/src/view/Makefile.am @@ -0,0 +1,14 @@ +SUBDIRS = devices frame home + +sugardir = $(pkgdatadir)/shell/view +sugar_PYTHON = \ + __init__.py \ + ActivityHost.py \ + BuddyIcon.py \ + BuddyMenu.py \ + clipboardicon.py \ + clipboardmenu.py \ + keyhandler.py \ + pulsingicon.py \ + OverlayWindow.py \ + Shell.py diff --git a/src/view/OverlayWindow.py b/src/view/OverlayWindow.py new file mode 100644 index 0000000..376ca2f --- /dev/null +++ b/src/view/OverlayWindow.py @@ -0,0 +1,68 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gtk +import cairo + +def _grab_pixbuf(window=None): + if not window: + screen = gtk.gdk.screen_get_default() + window = screen.get_root_window() + color_map = gtk.gdk.colormap_get_system() + (x, y, w, h, bpp) = window.get_geometry() + return gtk.gdk.pixbuf_get_from_drawable(None, window, color_map, x, y, 0, 0, w, h) + +class OverlayWindow(gtk.Window): + def __init__(self, lower_window): + gtk.Window.__init__(self) + self._lower_window = lower_window + + self._img = gtk.Image() + self.add(self._img) + + self.realize() + + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(False) + self.window.set_transient_for(lower_window) + + self.set_decorated(False) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_default_size(gtk.gdk.screen_width(), gtk.gdk.screen_height()) + self.set_app_paintable(True) + +# self.connect('expose-event', self._expose_cb) + + def appear(self): + pbuf = _grab_pixbuf(self._lower_window) + #pbuf.saturate_and_pixelate(pbuf, 0.5, False) + w = pbuf.get_width() + h = pbuf.get_height() + pbuf2 = pbuf.composite_color_simple(w, h, gtk.gdk.INTERP_NEAREST, 100, 1024, 0, 0) + self._img.set_from_pixbuf(pbuf2) + self.show_all() + + def disappear(self): + self._img.set_from_pixbuf(None) + self.hide() + + def _expose_cb(self, widget, event): + cr = widget.window.cairo_create() + cr.set_source_rgba(0.0, 0.0, 0.0, 0.4) # Transparent + cr.set_operator(cairo.OPERATOR_SOURCE) + cr.paint() + return False + diff --git a/src/view/Shell.py b/src/view/Shell.py new file mode 100644 index 0000000..72aa3b1 --- /dev/null +++ b/src/view/Shell.py @@ -0,0 +1,298 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 _ +from sets import Set +import logging +import tempfile +import os +import time +import shutil + +import gobject +import gtk +import wnck +import dbus + +from sugar.activity.activityhandle import ActivityHandle +from sugar import activity +from sugar.activity import activityfactory +from sugar.datastore import datastore +from sugar import profile +from sugar import env + +from view.ActivityHost import ActivityHost +from view.frame.frame import Frame +from view.keyhandler import KeyHandler +from view.home.HomeWindow import HomeWindow +from model.shellmodel import ShellModel + +# #3903 - this constant can be removed and assumed to be 1 when dbus-python +# 0.82.3 is the only version used +if dbus.version >= (0, 82, 3): + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1 +else: + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1000 + +class Shell(gobject.GObject): + def __init__(self, model): + gobject.GObject.__init__(self) + + self._activities_starting = Set() + self._model = model + self._hosts = {} + self._screen = wnck.screen_get_default() + self._current_host = None + self._pending_host = None + self._screen_rotation = 0 + self._zoom_level = ShellModel.ZOOM_HOME + + self._key_handler = KeyHandler(self) + + self._frame = Frame(self) + + self._home_window = HomeWindow(self) + self._home_window.show() + + home_model = self._model.get_home() + home_model.connect('activity-started', self._activity_started_cb) + home_model.connect('activity-removed', self._activity_removed_cb) + home_model.connect('active-activity-changed', + self._active_activity_changed_cb) + home_model.connect('pending-activity-changed', + self._pending_activity_changed_cb) + + self._model.connect('notify::zoom-level', self._zoom_level_changed_cb) + + gobject.idle_add(self._start_journal_idle) + + def _start_journal_idle(self): + # Mount the datastore in internal flash + ds_path = env.get_profile_path('datastore') + try: + datastore.mount(ds_path, [], timeout=120 * \ + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND) + except: + # Don't explode if there's corruption; move the data out of the way + # and attempt to create a store from scratch. + shutil.move(ds_path, os.path.abspath(ds_path) + str(time.time())) + datastore.mount(ds_path, [], timeout=120 * \ + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND) + + # Checking for the bundle existence will also ensure + # that the shell service is started up. + registry = activity.get_registry() + if registry.get_activity('org.laptop.JournalActivity'): + self.start_activity('org.laptop.JournalActivity') + + def _activity_started_cb(self, home_model, home_activity): + activity_host = ActivityHost(home_activity) + self._hosts[activity_host.get_xid()] = activity_host + if home_activity.get_type() in self._activities_starting: + self._activities_starting.remove(home_activity.get_type()) + + def _activity_removed_cb(self, home_model, home_activity): + if home_activity.get_type() in self._activities_starting: + self._activities_starting.remove(home_activity.get_type()) + xid = home_activity.get_xid() + if self._hosts.has_key(xid): + self._hosts[xid].destroy() + del self._hosts[xid] + + def _active_activity_changed_cb(self, home_model, home_activity): + if home_activity: + host = self._hosts[home_activity.get_xid()] + else: + host = None + + if self._current_host: + self._current_host.set_active(False) + + self._current_host = host + + def _pending_activity_changed_cb(self, home_model, home_activity): + if home_activity: + self._pending_host = self._hosts[home_activity.get_xid()] + else: + self._pending_host = None + + def get_model(self): + return self._model + + def get_frame(self): + return self._frame + + def join_activity(self, bundle_id, activity_id): + activity_host = self.get_activity(activity_id) + if activity_host: + activity_host.present() + return + + # Get the service name for this activity, if + # we have a bundle on the system capable of handling + # this activity type + registry = activity.get_registry() + bundle = registry.get_activity(bundle_id) + if not bundle: + logging.error("Couldn't find activity for type %s" % bundle_id) + return + + handle = ActivityHandle(activity_id) + activityfactory.create(bundle_id, handle) + + def notify_launch(self, bundle_id, activity_id): + # Zoom to Home for launch feedback + self.set_zoom_level(ShellModel.ZOOM_HOME) + + home_model = self._model.get_home() + home_model.notify_activity_launch(activity_id, bundle_id) + + def notify_launch_failure(self, activity_id): + home_model = self._model.get_home() + home_model.notify_activity_launch_failed(activity_id) + + def start_activity(self, activity_type): + if activity_type in self._activities_starting: + logging.debug("This activity is still launching.") + return + + self._activities_starting.add(activity_type) + activityfactory.create(activity_type) + + def take_activity_screenshot(self): + if self._model.get_zoom_level() != ShellModel.ZOOM_ACTIVITY: + return + if self.get_frame().visible: + return + + home_model = self._model.get_home() + activity = home_model.get_active_activity() + if activity is not None: + service = activity.get_service() + if service is not None: + try: + service.TakeScreenshot(timeout=2.0) + except dbus.DBusException, e: + logging.debug('Error raised by TakeScreenshot(): %s', e) + + def set_zoom_level(self, level): + if level == self._zoom_level: + return + + self.take_activity_screenshot() + + if level == ShellModel.ZOOM_ACTIVITY: + if self._pending_host is not None: + self._pending_host.present() + self._screen.toggle_showing_desktop(False) + else: + self._model.set_zoom_level(level) + self._screen.toggle_showing_desktop(True) + self._home_window.set_zoom_level(level) + + def _zoom_level_changed_cb(self, model, pspec): + new_level = model.props.zoom_level + + if new_level == ShellModel.ZOOM_HOME: + self._frame.show(Frame.MODE_NON_INTERACTIVE) + + if self._zoom_level == ShellModel.ZOOM_HOME: + self._frame.hide() + + self._zoom_level = new_level + + def toggle_activity_fullscreen(self): + if self._model.get_zoom_level() == ShellModel.ZOOM_ACTIVITY: + self.get_current_activity().toggle_fullscreen() + + def activate_previous_activity(self): + home_model = self._model.get_home() + activity = home_model.get_previous_activity() + if activity: + self.take_activity_screenshot() + activity.get_window().activate(1) + + def activate_next_activity(self): + home_model = self._model.get_home() + activity = home_model.get_next_activity() + if activity: + self.take_activity_screenshot() + activity.get_window().activate(1) + + def close_current_activity(self): + if self._model.get_zoom_level() != ShellModel.ZOOM_ACTIVITY: + return + + home_model = self._model.get_home() + activity = home_model.get_active_activity() + if activity.get_type() == 'org.laptop.JournalActivity': + return + + self.take_activity_screenshot() + self.get_current_activity().close() + + def get_current_activity(self): + return self._current_host + + def get_activity(self, activity_id): + for host in self._hosts.values(): + if host.get_id() == activity_id: + return host + return None + + def toggle_chat_visibility(self): + act = self.get_current_activity() + if not act: + return + is_visible = self._frame.is_visible() + if act.is_chat_visible(): + frame_was_visible = act.chat_hide() + if not frame_was_visible: + self._frame.do_slide_out() + else: + if not is_visible: + self._frame.do_slide_in() + act.chat_show(is_visible) + + def take_screenshot(self): + file_path = os.path.join(tempfile.gettempdir(), '%i' % time.time()) + + window = gtk.gdk.get_default_root_window() + width, height = window.get_size() + x_orig, y_orig = window.get_origin() + + screenshot = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha=False, + bits_per_sample=8, width=width, height=height) + screenshot.get_from_drawable(window, window.get_colormap(), x_orig, y_orig, 0, 0, + width, height) + screenshot.save(file_path, "png") + try: + jobject = datastore.create() + try: + jobject.metadata['title'] = _('Screenshot') + jobject.metadata['keep'] = '0' + jobject.metadata['buddies'] = '' + jobject.metadata['preview'] = '' + jobject.metadata['icon-color'] = profile.get_color().to_string() + jobject.metadata['mime_type'] = 'image/png' + jobject.file_path = file_path + datastore.write(jobject) + finally: + jobject.destroy() + del jobject + finally: + os.remove(file_path) + diff --git a/src/view/__init__.py b/src/view/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/view/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 + diff --git a/src/view/clipboardicon.py b/src/view/clipboardicon.py new file mode 100644 index 0000000..4b36395 --- /dev/null +++ b/src/view/clipboardicon.py @@ -0,0 +1,165 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007, 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 + +import logging +from gettext import gettext as _ + +import gobject +import gtk + +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.xocolor import XoColor +from sugar.graphics.icon import Icon +from sugar.graphics import style +from sugar.clipboard import clipboardservice +from sugar.bundle.activitybundle import ActivityBundle +from sugar import util +from sugar import profile + +from view.clipboardmenu import ClipboardMenu +from view.frame.frameinvoker import FrameWidgetInvoker + +class ClipboardIcon(RadioToolButton): + __gtype_name__ = 'SugarClipboardIcon' + + def __init__(self, object_id, name, group): + RadioToolButton.__init__(self, group=group) + self._object_id = object_id + self._name = name + self._percent = 0 + self._preview = None + self._activity = None + self.owns_clipboard = False + self.props.sensitive = False + self.props.active = False + + self._icon = Icon() + self._icon.props.xo_color = profile.get_color() + self.set_icon_widget(self._icon) + self._icon.show() + + cb_service = clipboardservice.get_instance() + cb_service.connect('object-state-changed', self._object_state_changed_cb) + obj = cb_service.get_object(self._object_id) + + self.palette = ClipboardMenu(self._object_id, self._name, self._percent, + self._preview, self._activity, + self._is_bundle(obj['FORMATS'])) + self.palette.props.invoker = FrameWidgetInvoker(self) + + self.child.connect('drag_data_get', self._drag_data_get_cb) + self.connect('notify::active', self._notify_active_cb) + + def _is_bundle(self, formats): + # A bundle will have only one format. + return formats and formats[0] in [ActivityBundle.MIME_TYPE, + ActivityBundle.DEPRECATED_MIME_TYPE] + + def get_object_id(self): + return self._object_id + + def _drag_data_get_cb(self, widget, context, selection, targetType, eventTime): + logging.debug('_drag_data_get_cb: requested target ' + selection.target) + + cb_service = clipboardservice.get_instance() + data = cb_service.get_object_data(self._object_id, selection.target)['DATA'] + + selection.set(selection.target, 8, data) + + def _put_in_clipboard(self): + logging.debug('ClipboardIcon._put_in_clipboard') + + if self._percent < 100: + raise ValueError('Object is not complete, cannot be put into the clipboard.') + + targets = self._get_targets() + if targets: + clipboard = gtk.Clipboard() + if not clipboard.set_with_data(targets, + self._clipboard_data_get_cb, + self._clipboard_clear_cb, + targets): + logging.error('GtkClipboard.set_with_data failed!') + else: + self.owns_clipboard = True + + def _clipboard_data_get_cb(self, clipboard, selection, info, targets): + if not selection.target in [target[0] for target in targets]: + logging.warning('ClipboardIcon._clipboard_data_get_cb: asked %s but' \ + ' only have %r.' % (selection.target, targets)) + return + cb_service = clipboardservice.get_instance() + data = cb_service.get_object_data(self._object_id, selection.target)['DATA'] + + selection.set(selection.target, 8, data) + + def _clipboard_clear_cb(self, clipboard, targets): + logging.debug('ClipboardIcon._clipboard_clear_cb') + self.owns_clipboard = False + + def _object_state_changed_cb(self, cb_service, object_id, name, percent, + icon_name, preview, activity): + + if object_id != self._object_id: + return + + cb_service = clipboardservice.get_instance() + obj = cb_service.get_object(self._object_id) + + if icon_name: + self._icon.props.icon_name = icon_name + else: + self._icon.props.icon_name = 'application-octet-stream' + + self.child.drag_source_set(gtk.gdk.BUTTON1_MASK, + self._get_targets(), + gtk.gdk.ACTION_COPY) + self.child.drag_source_set_icon_name(self._icon.props.icon_name) + + self._name = name + self._preview = preview + self._activity = activity + self.palette.set_state(name, percent, preview, activity, + self._is_bundle(obj['FORMATS'])) + + old_percent = self._percent + self._percent = percent + if self._percent == 100: + self.props.sensitive = True + + # Clipboard object became complete. Make it the active one. + if old_percent < 100 and self._percent == 100: + self.props.active = True + + def _notify_active_cb(self, widget, pspec): + if self.props.active: + self._put_in_clipboard() + else: + self.owns_clipboard = False + + def _get_targets(self): + cb_service = clipboardservice.get_instance() + + attrs = cb_service.get_object(self._object_id) + format_types = attrs[clipboardservice.FORMATS_KEY] + + targets = [] + for format_type in format_types: + targets.append((format_type, 0, 0)) + + return targets + diff --git a/src/view/clipboardmenu.py b/src/view/clipboardmenu.py new file mode 100644 index 0000000..b847828 --- /dev/null +++ b/src/view/clipboardmenu.py @@ -0,0 +1,223 @@ +# Copyright (C) 2007, 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 gettext import gettext as _ +import tempfile +import urlparse +import os +import logging + +import gtk +import hippo + +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics import style +from sugar.clipboard import clipboardservice +from sugar.datastore import datastore +from sugar import mime +from sugar import profile +from sugar import activity + +class ClipboardMenu(Palette): + + def __init__(self, object_id, name, percent, preview, activities, installable): + Palette.__init__(self, name) + + self._object_id = object_id + self._percent = percent + self._activities = activities + + self.set_group_id('frame') + + self._progress_bar = None + + """ + if preview: + self._preview_text = hippo.CanvasText(text=preview, + size_mode=hippo.CANVAS_SIZE_WRAP_WORD) + self._preview_text.props.color = color.LABEL_TEXT.get_int() + self._preview_text.props.font_desc = \ + style.FONT_NORMAL.get_pango_desc() + self.append(self._preview_text) + """ + + self._remove_item = MenuItem(_('Remove'), 'list-remove') + self._remove_item.connect('activate', self._remove_item_activate_cb) + self.menu.append(self._remove_item) + self._remove_item.show() + + self._open_item = MenuItem(_('Open')) + self._open_item.connect('activate', self._open_item_activate_cb) + self.menu.append(self._open_item) + self._open_item.show() + + #self._stop_item = MenuItem(_('Stop download'), 'stock-close') + # TODO: Implement stopping downloads + #self._stop_item.connect('activate', self._stop_item_activate_cb) + #self.append_menu_item(self._stop_item) + + self._journal_item = MenuItem(_('Add to journal'), 'document-save') + self._journal_item.connect('activate', self._journal_item_activate_cb) + self.menu.append(self._journal_item) + self._journal_item.show() + + self._update_items_visibility(installable) + self._update_open_submenu() + + def _update_open_submenu(self): + logging.debug('_update_open_submenu: %r' % self._activities) + if self._activities is None or len(self._activities) <= 1: + if self._open_item.get_submenu() is not None: + self._open_item.remove_submenu() + return + + submenu = self._open_item.get_submenu() + if submenu is None: + submenu = gtk.Menu() + self._open_item.set_submenu(submenu) + submenu.show() + else: + for item in submenu.get_children(): + submenu.remove(item) + + for service_name in self._activities: + registry = activity.get_registry() + activity_info = registry.get_activity(service_name) + + if not activity_info: + logging.warning('Activity %s is unknown.' % service_name) + + item = gtk.MenuItem(activity_info.name) + item.connect('activate', self._open_submenu_item_activate_cb, service_name) + submenu.append(item) + item.show() + + def _update_items_visibility(self, installable): + if self._percent == 100 and (self._activities or installable): + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = True + #self._stop_item.props.sensitive = False + self._journal_item.props.sensitive = True + elif self._percent == 100 and (not self._activities and not installable): + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = False + #self._stop_item.props.sensitive = False + self._journal_item.props.sensitive = True + else: + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = False + # TODO: reenable the stop item when we implement stoping downloads. + #self._stop_item.props.sensitive = True + self._journal_item.props.sensitive = False + + self._update_progress_bar() + + def _update_progress_bar(self): + if self._percent == 100.0: + if self._progress_bar: + self._progress_bar = None + self.set_content(None) + else: + if self._progress_bar is None: + self._progress_bar = gtk.ProgressBar() + self._progress_bar.show() + self.set_content(self._progress_bar) + + self._progress_bar.props.fraction = self._percent / 100.0 + self._progress_bar.props.text = '%.2f %%' % self._percent + + def set_state(self, name, percent, preview, activities, installable): + self.set_primary_text(name) + self._percent = percent + self._activities = activities + self._update_progress_bar() + self._update_items_visibility(installable) + self._update_open_submenu() + + def _open_item_activate_cb(self, menu_item): + logging.debug('_open_item_activate_cb') + if self._percent < 100 or menu_item.get_submenu() is not None: + return + jobject = self._copy_to_journal() + jobject.resume(self._activities[0]) + jobject.destroy() + + def _open_submenu_item_activate_cb(self, menu_item, service_name): + logging.debug('_open_submenu_item_activate_cb') + if self._percent < 100: + return + jobject = self._copy_to_journal() + jobject.resume(service_name) + jobject.destroy() + + def _remove_item_activate_cb(self, menu_item): + cb_service = clipboardservice.get_instance() + cb_service.delete_object(self._object_id) + + def _journal_item_activate_cb(self, menu_item): + logging.debug('_journal_item_activate_cb') + jobject = self._copy_to_journal() + jobject.destroy() + + def _write_to_temp_file(self, data): + f, file_path = tempfile.mkstemp() + try: + os.write(f, data) + finally: + os.close(f) + return file_path + + def _copy_to_journal(self): + cb_service = clipboardservice.get_instance() + obj = cb_service.get_object(self._object_id) + + format = mime.choose_most_significant(obj['FORMATS']) + data = cb_service.get_object_data(self._object_id, format) + + transfer_ownership = False + if format == 'text/uri-list': + uris = mime.split_uri_list(data['DATA']) + if len(uris) == 1 and uris[0].startswith('file://'): + file_path = urlparse.urlparse(uris[0]).path + transfer_ownership = False + mime_type = mime.get_for_file(file_path) + else: + file_path = self._write_to_temp_file(data['DATA']) + transfer_ownership = True + mime_type = 'text/uri-list' + else: + if data['ON_DISK']: + file_path = urlparse.urlparse(data['DATA']).path + transfer_ownership = False + mime_type = mime.get_for_file(file_path) + else: + file_path = self._write_to_temp_file(data['DATA']) + transfer_ownership = True + mime_type = mime.get_for_file(file_path) + + jobject = datastore.create() + jobject.metadata['title'] = _('Clipboard object: %s.') % obj['NAME'] + jobject.metadata['keep'] = '0' + jobject.metadata['buddies'] = '' + jobject.metadata['preview'] = '' + jobject.metadata['icon-color'] = profile.get_color().to_string() + jobject.metadata['mime_type'] = mime_type + jobject.file_path = file_path + datastore.write(jobject, transfer_ownership=transfer_ownership) + + return jobject + diff --git a/src/view/devices/Makefile.am b/src/view/devices/Makefile.am new file mode 100644 index 0000000..c040beb --- /dev/null +++ b/src/view/devices/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = network + +sugardir = $(pkgdatadir)/shell/view/devices +sugar_PYTHON = \ + __init__.py \ + battery.py \ + deviceview.py diff --git a/src/view/devices/__init__.py b/src/view/devices/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/view/devices/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 + diff --git a/src/view/devices/battery.py b/src/view/devices/battery.py new file mode 100644 index 0000000..09c69df --- /dev/null +++ b/src/view/devices/battery.py @@ -0,0 +1,100 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 gtk + +from sugar import profile +from sugar.graphics.icon import CanvasIcon +from sugar.graphics.icon import get_icon_state +from sugar.graphics import style +from sugar.graphics.palette import Palette + +_ICON_NAME = 'battery' + +_STATUS_CHARGING = 0 +_STATUS_DISCHARGING = 1 +_STATUS_FULLY_CHARGED = 2 + +class DeviceView(CanvasIcon): + def __init__(self, model): + CanvasIcon.__init__(self, size=style.MEDIUM_ICON_SIZE, + xo_color=profile.get_color()) + self._model = model + self._palette = BatteryPalette(_('My Battery life')) + self.set_palette(self._palette) + + model.connect('notify::level', self._battery_status_changed_cb) + model.connect('notify::charging', self._battery_status_changed_cb) + model.connect('notify::discharging', self._battery_status_changed_cb) + self._update_info() + + def _update_info(self): + name = get_icon_state(_ICON_NAME, self._model.props.level) + self.props.icon_name = name + + # Update palette + if self._model.props.charging: + status = _STATUS_CHARGING + self.props.badge_name = 'emblem-charging' + elif self._model.props.discharging: + status = _STATUS_DISCHARGING + self.props.badge_name = None + else: + status = _STATUS_FULLY_CHARGED + self.props.badge_name = None + + self._palette.set_level(self._model.props.level) + self._palette.set_status(status) + + def _battery_status_changed_cb(self, pspec, param): + self._update_info() + +class BatteryPalette(Palette): + + def __init__(self, primary_text): + Palette.__init__(self, primary_text) + + self._level = 0 + self._progress_bar = gtk.ProgressBar() + self._progress_bar.show() + self._status_label = gtk.Label() + self._status_label.show() + + vbox = gtk.VBox() + vbox.pack_start(self._progress_bar) + vbox.pack_start(self._status_label) + vbox.show() + + self.set_content(vbox) + + def set_level(self, percent): + self._level = percent + fraction = percent/100.0 + self._progress_bar.set_fraction(fraction) + + def set_status(self, status): + percent_string = ' (%s%%)' % self._level + + if status == _STATUS_CHARGING: + charge_text = _('Battery charging') + percent_string + elif status == _STATUS_DISCHARGING: + charge_text = _('Battery discharging') + percent_string + elif status == _STATUS_FULLY_CHARGED: + charge_text = _('Battery fully charged') + + self._status_label.set_text(charge_text) diff --git a/src/view/devices/deviceview.py b/src/view/devices/deviceview.py new file mode 100644 index 0000000..f58da02 --- /dev/null +++ b/src/view/devices/deviceview.py @@ -0,0 +1,27 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 sugar.graphics.icon import CanvasIcon + +def create(model): + name = 'view.devices.' + model.get_type() + + mod = __import__(name) + components = name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + + return mod.DeviceView(model) diff --git a/src/view/devices/network/Makefile.am b/src/view/devices/network/Makefile.am new file mode 100644 index 0000000..0d215f0 --- /dev/null +++ b/src/view/devices/network/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pkgdatadir)/shell/view/devices/network +sugar_PYTHON = \ + __init__.py \ + mesh.py \ + wired.py \ + wireless.py diff --git a/src/view/devices/network/__init__.py b/src/view/devices/network/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/view/devices/network/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 + diff --git a/src/view/devices/network/mesh.py b/src/view/devices/network/mesh.py new file mode 100644 index 0000000..2543957 --- /dev/null +++ b/src/view/devices/network/mesh.py @@ -0,0 +1,125 @@ +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gtk + +from sugar import profile +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style +from model.devices import device + +from sugar.graphics.palette import Palette +from model.devices.network import wireless + +from hardware import hardwaremanager + +class DeviceView(CanvasIcon): + def __init__(self, model): + CanvasIcon.__init__(self, size=style.MEDIUM_ICON_SIZE, + icon_name='network-mesh') + self._model = model + self._palette = MeshPalette(_("Mesh Network"), model) + self.set_palette(self._palette) + + model.connect('notify::state', self._state_changed_cb) + model.connect('notify::activation-stage', self._state_changed_cb) + self._update_state() + + def _state_changed_cb(self, model, pspec): + self._update_state() + + def _update_state(self): + # FIXME Change icon colors once we have real icons + state = self._model.props.state + self._palette.update_state(state) + + if state == device.STATE_ACTIVATING: + self.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg() + self.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg() + elif state == device.STATE_ACTIVATED: + self.props.xo_color = profile.get_color() + elif state == device.STATE_INACTIVE: + self.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg() + self.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg() + + if state == device.STATE_INACTIVE: + self._palette.set_primary_text(_("Mesh Network")) + else: + chan = wireless.freq_to_channel(self._model.props.frequency) + if chan > 0: + self._palette.set_primary_text(_("Mesh Network") + " %d" % chan) + self._palette.set_mesh_step(self._model.props.mesh_step, state) + +class MeshPalette(Palette): + def __init__(self, primary_text, model): + Palette.__init__(self, primary_text, menu_after_content=True) + self._model = model + + self._step_label = gtk.Label() + self._step_label.show() + + vbox = gtk.VBox() + vbox.pack_start(self._step_label) + vbox.show() + + self.set_content(vbox) + + self._disconnect_item = gtk.MenuItem(_('Disconnect...')) + self._disconnect_item.connect('activate', self._disconnect_activate_cb) + self.menu.append(self._disconnect_item) + + def update_state(self, state): + if state == device.STATE_ACTIVATED: + self._disconnect_item.show() + else: + self._disconnect_item.hide() + + def _disconnect_activate_cb(self, menuitem): + # Disconnection for an mesh means activating the default mesh device + # again without a channel + network_manager = hardwaremanager.get_network_manager() + nm_device = self._model.get_nm_device() + if network_manager and nm_device: + network_manager.set_active_device(nm_device) + + def set_mesh_step(self, step, state): + label = "" + if step == 1: + if state == device.STATE_ACTIVATED: + label = _("Connected to a School Mesh Portal") + elif state == device.STATE_ACTIVATING: + label = _("Looking for a School Mesh Portal...") + elif step == 3: + if state == device.STATE_ACTIVATED: + label = _("Connected to an XO Mesh Portal") + elif state == device.STATE_ACTIVATING: + label = _("Looking for an XO Mesh Portal...") + elif step == 4: + if state == device.STATE_ACTIVATED: + label = _("Connected to a Simple Mesh") + elif state == device.STATE_ACTIVATING: + label = _("Starting a Simple Mesh") + + if len(label): + self._step_label.set_text(label) + else: + import logging + logging.debug("Unhandled mesh step %d" % step) + self._step_label.set_text(_("Unknown Mesh")) + diff --git a/src/view/devices/network/wired.py b/src/view/devices/network/wired.py new file mode 100644 index 0000000..dc83a08 --- /dev/null +++ b/src/view/devices/network/wired.py @@ -0,0 +1,22 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 view.devices import deviceview + +class DeviceView(deviceview.DeviceView): + def __init__(self, model): + deviceview.DeviceView.__init__(self, model) + self.props.icon_name = 'network-wired' diff --git a/src/view/devices/network/wireless.py b/src/view/devices/network/wireless.py new file mode 100644 index 0000000..f4f8869 --- /dev/null +++ b/src/view/devices/network/wireless.py @@ -0,0 +1,132 @@ +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gtk + +from sugar.graphics.icon import get_icon_state +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style +from sugar.graphics.palette import Palette + +from model.devices.network import wireless +from model.devices import device + +from hardware import hardwaremanager +from hardware import nmclient + +_ICON_NAME = 'network-wireless' + +class DeviceView(CanvasIcon): + def __init__(self, model): + CanvasIcon.__init__(self, size=style.MEDIUM_ICON_SIZE) + self._model = model + + meshdev = None + network_manager = hardwaremanager.get_network_manager() + for device in network_manager.get_devices(): + if device.get_type() == nmclient.DEVICE_TYPE_802_11_MESH_OLPC: + meshdev = device + break + + self._palette = WirelessPalette(self._get_palette_primary_text(), meshdev) + self.set_palette(self._palette) + self._counter = 0 + self._palette.set_frequency(self._model.props.frequency) + + model.connect('notify::name', self._name_changed_cb) + model.connect('notify::strength', self._strength_changed_cb) + model.connect('notify::state', self._state_changed_cb) + + self._update_icon() + self._update_state() + + def _get_palette_primary_text(self): + if self._model.props.state == device.STATE_INACTIVE: + return _("Disconnected") + return self._model.props.name + + def _strength_changed_cb(self, model, pspec): + self._update_icon() + # Only update frequency periodically + if self._counter % 4 == 0: + self._palette.set_frequency(self._model.props.frequency) + self._counter += 1 + + def _name_changed_cb(self, model, pspec): + self.palette.set_primary_text(self._get_palette_primary_text()) + + def _state_changed_cb(self, model, pspec): + self._update_state() + self.palette.set_primary_text(self._get_palette_primary_text()) + + def _update_icon(self): + strength = self._model.props.strength + if self._model.props.state == device.STATE_INACTIVE: + strength = 0 + icon_name = get_icon_state(_ICON_NAME, strength) + if icon_name: + self.props.icon_name = icon_name + + def _update_state(self): + # FIXME Change icon colors once we have real icons + state = self._model.props.state + if state == device.STATE_ACTIVATING: + self.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg() + self.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg() + elif state == device.STATE_ACTIVATED: + (stroke, fill) = self._model.get_active_network_colors() + self.props.stroke_color = stroke + self.props.fill_color = fill + elif state == device.STATE_INACTIVE: + self.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg() + self.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg() + +class WirelessPalette(Palette): + def __init__(self, primary_text, meshdev): + Palette.__init__(self, primary_text, menu_after_content=True) + self._meshdev = meshdev + + self._chan_label = gtk.Label() + self._chan_label.show() + + vbox = gtk.VBox() + vbox.pack_start(self._chan_label) + vbox.show() + + if meshdev: + disconnect_item = gtk.MenuItem(_('Disconnect...')) + disconnect_item.connect('activate', self._disconnect_activate_cb) + self.menu.append(disconnect_item) + disconnect_item.show() + + self.set_content(vbox) + + def _disconnect_activate_cb(self, menuitem): + # Disconnection for an AP means activating the default mesh device + network_manager = hardwaremanager.get_network_manager() + if network_manager and self._meshdev: + network_manager.set_active_device(self._meshdev) + + def set_frequency(self, freq): + try: + chan = wireless.freq_to_channel(freq) + except KeyError: + chan = 0 + self._chan_label.set_text("%s: %d" % (_("Channel"), chan)) + diff --git a/src/view/frame/Makefile.am b/src/view/frame/Makefile.am new file mode 100644 index 0000000..02951b9 --- /dev/null +++ b/src/view/frame/Makefile.am @@ -0,0 +1,14 @@ +sugardir = $(pkgdatadir)/shell/view/frame +sugar_PYTHON = \ + __init__.py \ + activitiestray.py \ + activitybutton.py \ + clipboardbox.py \ + clipboardpanelwindow.py \ + frameinvoker.py \ + friendstray.py \ + eventarea.py \ + frame.py \ + overlaybox.py \ + framewindow.py \ + zoomtoolbar.py diff --git a/src/view/frame/__init__.py b/src/view/frame/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/view/frame/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 + diff --git a/src/view/frame/activitiestray.py b/src/view/frame/activitiestray.py new file mode 100644 index 0000000..3dbf955 --- /dev/null +++ b/src/view/frame/activitiestray.py @@ -0,0 +1,156 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 hippo +import logging + +from sugar.graphics.tray import TrayButton +from sugar.graphics.tray import HTray +from sugar.graphics.icon import Icon +from sugar.graphics import style +from sugar import profile +from sugar import activity +from sugar import env + +from activitybutton import ActivityButton + +class InviteButton(TrayButton): + def __init__(self, activity_model, invite): + TrayButton.__init__(self) + + icon = Icon(file=activity_model.get_icon_name(), + xo_color=activity_model.get_color()) + self.set_icon_widget(icon) + icon.show() + + self._invite = invite + + def get_activity_id(self): + return self._invite.get_activity_id() + + def get_bundle_id(self): + return self._invite.get_bundle_id() + + def get_invite(self): + return self._invite + +class ActivitiesTray(hippo.CanvasBox): + def __init__(self, shell): + hippo.CanvasBox.__init__(self, orientation=hippo.ORIENTATION_HORIZONTAL) + + self._shell = shell + self._shell_model = self._shell.get_model() + self._invite_to_item = {} + self._invites = self._shell_model.get_invites() + self._config = self._load_config() + + self._tray = HTray() + self.append(hippo.CanvasWidget(widget=self._tray), hippo.PACK_EXPAND) + self._tray.show() + + registry = activity.get_registry() + registry.get_activities_async(reply_handler=self._get_activities_cb) + + registry.connect('activity-added', self._activity_added_cb) + registry.connect('activity-removed', self._activity_removed_cb) + + for invite in self._invites: + self.add_invite(invite) + self._invites.connect('invite-added', self._invite_added_cb) + self._invites.connect('invite-removed', self._invite_removed_cb) + + def _load_config(self): + config = [] + + f = open(env.get_data_path('activities.defaults'), 'r') + for line in f.readlines(): + line = line.strip() + if line and not line.startswith('#'): + config.append(line) + f.close() + + return config + + def _get_activities_cb(self, activity_list): + known_activities = [] + unknown_activities = [] + name_to_activity = {} + + while activity_list: + info = activity_list.pop() + name_to_activity[info.bundle_id] = info + + if info.bundle_id in self._config: + known_activities.append(info) + else: + unknown_activities.append(info) + + sorted_activities = [] + for name in self._config: + if name in name_to_activity: + sorted_activities.append(name_to_activity[name]) + + for info in sorted_activities + unknown_activities: + if info.show_launcher: + self.add_activity(info) + + def _activity_clicked_cb(self, icon): + self._shell.start_activity(icon.get_bundle_id()) + + def _invite_clicked_cb(self, icon): + self._invites.remove_invite(icon.get_invite()) + self._shell.join_activity(icon.get_bundle_id(), + icon.get_activity_id()) + + def _invite_added_cb(self, invites, invite): + self.add_invite(invite) + + def _invite_removed_cb(self, invites, invite): + self.remove_invite(invite) + + def _remove_activity_cb(self, item): + self._tray.remove_item(item) + + def _activity_added_cb(self, activity_registry, activity_info): + self.add_activity(activity_info) + + def _activity_removed_cb(self, activity_registry, activity_info): + for item in self._tray.get_children(): + if item.get_bundle_id() == activity_info.bundle_id: + self._tray.remove_item(item) + return + + def add_activity(self, activity_info): + item = ActivityButton(activity_info) + item.connect('clicked', self._activity_clicked_cb) + item.connect('remove_activity', self._remove_activity_cb) + self._tray.add_item(item, -1) + item.show() + + def add_invite(self, invite): + mesh = self._shell_model.get_mesh() + activity_model = mesh.get_activity(invite.get_activity_id()) + if activity: + item = InviteButton(activity_model, invite) + item.connect('clicked', self._invite_clicked_cb) + self._tray.add_item(item, 0) + item.show() + + self._invite_to_item[invite] = item + + def remove_invite(self, invite): + self._tray.remove_item(self._invite_to_item[invite]) + del self._invite_to_item[invite] diff --git a/src/view/frame/activitybutton.py b/src/view/frame/activitybutton.py new file mode 100644 index 0000000..0c7c7fb --- /dev/null +++ b/src/view/frame/activitybutton.py @@ -0,0 +1,65 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk +import os +import gobject +from gettext import gettext as _ + +from sugar.graphics.palette import Palette +from sugar.graphics.tray import TrayButton +from sugar.graphics.icon import Icon +from sugar.graphics import style + +from view.frame.frameinvoker import FrameWidgetInvoker + +class ActivityButton(TrayButton, gobject.GObject): + __gtype_name__ = 'SugarActivityButton' + __gsignals__ = { + 'remove_activity': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self, activity_info): + TrayButton.__init__(self) + + icon = Icon(file=activity_info.icon, + stroke_color=style.COLOR_WHITE.get_svg(), + fill_color=style.COLOR_TRANSPARENT.get_svg()) + self.set_icon_widget(icon) + icon.show() + + self._activity_info = activity_info + self.setup_rollover_options() + + def get_bundle_id(self): + return self._activity_info.bundle_id + + def setup_rollover_options(self): + palette = Palette(self._activity_info.name) + self.set_palette(palette) + palette.props.invoker = FrameWidgetInvoker(self) + +#TODO: Disabled this until later, see #4967 +# if os.path.dirname(self._activity_info.path) == os.path.expanduser('~/Activities'): +# menu_item = gtk.MenuItem(_('Remove')) +# menu_item.connect('activate', self.item_remove_cb) +# palette.menu.append(menu_item) +# menu_item.show() + + def item_remove_cb(self, widget): + self.emit('remove_activity') diff --git a/src/view/frame/clipboardbox.py b/src/view/frame/clipboardbox.py new file mode 100644 index 0000000..7702759 --- /dev/null +++ b/src/view/frame/clipboardbox.py @@ -0,0 +1,193 @@ +# Copyright (C) 2007, 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 + +import os +import logging +import tempfile + +import hippo +import gtk + +from sugar import util +from sugar.clipboard import clipboardservice +from sugar.graphics.tray import VTray +from sugar.graphics import style + +from view.clipboardicon import ClipboardIcon + +class _ContextMap: + """Maps a drag context to the clipboard object involved in the dragging.""" + def __init__(self): + self._context_map = {} + + def add_context(self, context, object_id, data_types): + """Establishes the mapping. data_types will serve us for reference- + counting this mapping. + """ + self._context_map[context] = [object_id, data_types] + + def get_object_id(self, context): + """Retrieves the object_id associated with context. + Will release the association when this function was called as many times + as the number of data_types that this clipboard object contains. + """ + [object_id, data_types_left] = self._context_map[context] + + data_types_left = data_types_left - 1 + if data_types_left == 0: + del self._context_map[context] + else: + self._context_map[context] = [object_id, data_types_left] + + return object_id + + def has_context(self, context): + return context in self._context_map + +class ClipboardBox(hippo.CanvasBox): + + MAX_ITEMS = gtk.gdk.screen_height() / style.GRID_CELL_SIZE - 2 + + def __init__(self): + hippo.CanvasBox.__init__(self) + self._icons = {} + self._context_map = _ContextMap() + + self._tray = VTray() + self.append(hippo.CanvasWidget(widget=self._tray), hippo.PACK_EXPAND) + self._tray.show() + + cb_service = clipboardservice.get_instance() + cb_service.connect('object-added', self._object_added_cb) + cb_service.connect('object-deleted', self._object_deleted_cb) + + def owns_clipboard(self): + for icon in self._icons.values(): + if icon.owns_clipboard: + return True + return False + + def _add_selection(self, object_id, selection): + if not selection.data: + return + + logging.debug('ClipboardBox: adding type ' + selection.type) + + cb_service = clipboardservice.get_instance() + if selection.type == 'text/uri-list': + uris = selection.data.split('\n') + if len(uris) > 1: + raise NotImplementedError('Multiple uris in text/uri-list still not supported.') + + cb_service.add_object_format(object_id, + selection.type, + uris[0], + on_disk=True) + else: + cb_service.add_object_format(object_id, + selection.type, + selection.data, + on_disk=False) + + def _object_added_cb(self, cb_service, object_id, name): + if self._icons: + group = self._icons.values()[0] + else: + group = None + + icon = ClipboardIcon(object_id, name, group) + self._tray.add_item(icon, 0) + icon.show() + self._icons[object_id] = icon + + objects_to_delete = self._tray.get_children()[ClipboardBox.MAX_ITEMS:] + for icon in objects_to_delete: + logging.debug('ClipboardBox: deleting surplus object') + cb_service = clipboardservice.get_instance() + cb_service.delete_object(icon.get_object_id()) + + logging.debug('ClipboardBox: ' + object_id + ' was added.') + + def _object_deleted_cb(self, cb_service, object_id): + icon = self._icons[object_id] + self._tray.remove_item(icon) + del self._icons[object_id] + logging.debug('ClipboardBox: ' + object_id + ' was deleted.') + + def drag_motion_cb(self, widget, context, x, y, time): + logging.debug('ClipboardBox._drag_motion_cb') + context.drag_status(gtk.gdk.ACTION_COPY, time) + return True; + + def drag_drop_cb(self, widget, context, x, y, time): + logging.debug('ClipboardBox._drag_drop_cb') + cb_service = clipboardservice.get_instance() + object_id = cb_service.add_object(name="") + + self._context_map.add_context(context, object_id, len(context.targets)) + + if 'XdndDirectSave0' in context.targets: + window = context.source_window + prop_type, format, filename = \ + window.property_get('XdndDirectSave0','text/plain') + + # FIXME query the clipboard service for a filename? + base_dir = tempfile.gettempdir() + dest_filename = util.unique_id() + + name, dot, extension = filename.rpartition('.') + dest_filename += dot + extension + + dest_uri = 'file://' + os.path.join(base_dir, dest_filename) + + window.property_change('XdndDirectSave0', prop_type, format, + gtk.gdk.PROP_MODE_REPLACE, dest_uri) + + widget.drag_get_data(context, 'XdndDirectSave0', time) + else: + for target in context.targets: + if str(target) not in ('TIMESTAMP', 'TARGETS', 'MULTIPLE'): + widget.drag_get_data(context, target, time) + + cb_service.set_object_percent(object_id, percent=100) + + return True + + def drag_data_received_cb(self, widget, context, x, y, selection, targetType, time): + logging.debug('ClipboardBox: got data for target %r' % selection.target) + + object_id = self._context_map.get_object_id(context) + try: + if selection is None: + logging.warn('ClipboardBox: empty selection for target ' + selection.target) + elif selection.target == 'XdndDirectSave0': + if selection.data == 'S': + window = context.source_window + + prop_type, format, dest = \ + window.property_get('XdndDirectSave0','text/plain') + + clipboard = clipboardservice.get_instance() + clipboard.add_object_format( + object_id, 'XdndDirectSave0', dest, on_disk=True) + else: + self._add_selection(object_id, selection) + + finally: + # If it's the last target to be processed, finish the dnd transaction + if not self._context_map.has_context(context): + context.drop_finish(True, gtk.get_current_event_time()) + diff --git a/src/view/frame/clipboardpanelwindow.py b/src/view/frame/clipboardpanelwindow.py new file mode 100644 index 0000000..e579b8c --- /dev/null +++ b/src/view/frame/clipboardpanelwindow.py @@ -0,0 +1,99 @@ +# Copyright (C) 2007, 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 + +import logging +import urlparse + +import gtk +import hippo + +from view.frame.framewindow import FrameWindow +from view.frame.clipboardbox import ClipboardBox +from sugar.clipboard import clipboardservice +from sugar import util + +class ClipboardPanelWindow(FrameWindow): + def __init__(self, frame, orientation): + FrameWindow.__init__(self, orientation) + + self._frame = frame + + # Listening for new clipboard objects + # NOTE: we need to keep a reference to gtk.Clipboard in order to keep + # listening to it. + self._clipboard = gtk.Clipboard() + self._clipboard.connect("owner-change", self._owner_change_cb) + + self._clipboard_box = ClipboardBox() + self.append(self._clipboard_box, hippo.PACK_EXPAND) + + # Receiving dnd drops + self.drag_dest_set(0, [], 0) + self.connect("drag_motion", self._clipboard_box.drag_motion_cb) + self.connect("drag_drop", self._clipboard_box.drag_drop_cb) + self.connect("drag_data_received", + self._clipboard_box.drag_data_received_cb) + + def _owner_change_cb(self, clipboard, event): + logging.debug("owner_change_cb") + + if self._clipboard_box.owns_clipboard(): + return + + cb_service = clipboardservice.get_instance() + key = cb_service.add_object(name="") + cb_service.set_object_percent(key, percent=0) + + targets = clipboard.wait_for_targets() + for target in targets: + if target not in ('TIMESTAMP', 'TARGETS', 'MULTIPLE', 'SAVE_TARGETS'): + logging.debug('Asking for target %s.' % target) + selection = clipboard.wait_for_contents(target) + if not selection: + logging.warning('no data for selection target %s.' % target) + continue + self._add_selection(key, selection) + + cb_service.set_object_percent(key, percent=100) + + def _add_selection(self, key, selection): + if not selection.data: + logging.warning('no data for selection target %s.' % selection.type) + return + + logging.debug('adding type ' + selection.type + '.') + + cb_service = clipboardservice.get_instance() + if selection.type == 'text/uri-list': + uris = selection.get_uris() + + if len(uris) > 1: + raise NotImplementedError('Multiple uris in text/uri-list still not supported.') + uri = uris[0] + + scheme, netloc, path, parameters, query, fragment = urlparse.urlparse(uri) + on_disk = (scheme == 'file') + + cb_service.add_object_format(key, + selection.type, + uri, + on_disk) + else: + cb_service.add_object_format(key, + selection.type, + selection.data, + on_disk=False) + diff --git a/src/view/frame/eventarea.py b/src/view/frame/eventarea.py new file mode 100644 index 0000000..69bb759 --- /dev/null +++ b/src/view/frame/eventarea.py @@ -0,0 +1,106 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 gtk +import gobject +import wnck + +class EventArea(gobject.GObject): + __gsignals__ = { + 'enter': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'leave': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._windows = [] + self._hover = False + + right = gtk.gdk.screen_width() - 1 + bottom = gtk.gdk.screen_height() -1 + + invisible = self._create_invisible(0, 0, 1, 1) + self._windows.append(invisible) + + invisible = self._create_invisible(right, 0, 1, 1) + self._windows.append(invisible) + + invisible = self._create_invisible(0, bottom, 1, 1) + self._windows.append(invisible) + + invisible = self._create_invisible(right, bottom, 1, 1) + self._windows.append(invisible) + + screen = wnck.screen_get_default() + screen.connect('window-stacking-changed', + self._window_stacking_changed_cb) + + def _create_invisible(self, x, y, width, height): + invisible = gtk.Invisible() + invisible.connect('enter-notify-event', self._enter_notify_cb) + invisible.connect('leave-notify-event', self._leave_notify_cb) + + invisible.drag_dest_set(0, [], 0) + invisible.connect('drag_motion', self._drag_motion_cb) + invisible.connect('drag_leave', self._drag_leave_cb) + + invisible.realize() + invisible.window.set_events(gtk.gdk.POINTER_MOTION_MASK | + gtk.gdk.ENTER_NOTIFY_MASK | + gtk.gdk.LEAVE_NOTIFY_MASK) + invisible.window.move_resize(x, y, width, height) + + return invisible + + def _notify_enter(self): + if not self._hover: + self._hover = True + self.emit('enter') + + def _notify_leave(self): + if self._hover: + self._hover = False + self.emit('leave') + + def _enter_notify_cb(self, widget, event): + self._notify_enter() + + def _leave_notify_cb(self, widget, event): + self._notify_leave() + + def _drag_motion_cb(self, widget, drag_context, x, y, timestamp): + drag_context.drag_status(0, timestamp); + self._notify_enter() + return True + + def _drag_leave_cb(self, widget, drag_context, timestamp): + self._notify_leave() + return True + + def show(self): + for window in self._windows: + window.show() + + def hide(self): + for window in self._windows: + window.hide() + + def _window_stacking_changed_cb(self, screen): + for window in self._windows: + window.window.raise_() diff --git a/src/view/frame/frame.py b/src/view/frame/frame.py new file mode 100644 index 0000000..e8f8fa4 --- /dev/null +++ b/src/view/frame/frame.py @@ -0,0 +1,272 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 logging + +import gtk +import gobject +import hippo + +from sugar.graphics import animator +from sugar.graphics import style +from sugar.graphics import palettegroup +from sugar.clipboard import clipboardservice + +from view.frame.eventarea import EventArea +from view.frame.activitiestray import ActivitiesTray +from view.frame.zoomtoolbar import ZoomToolbar +from view.frame.friendstray import FriendsTray +from view.frame.framewindow import FrameWindow +from view.frame.clipboardpanelwindow import ClipboardPanelWindow +from model.shellmodel import ShellModel + +_FRAME_HIDING_DELAY = 500 + +class _Animation(animator.Animation): + def __init__(self, frame, end): + start = frame.current_position + animator.Animation.__init__(self, start, end) + self._frame = frame + + def next_frame(self, current): + self._frame.move(current) + +class _MouseListener(object): + def __init__(self, frame): + self._frame = frame + self._hide_sid = 0 + + def mouse_enter(self): + self._show_frame() + + def mouse_leave(self): + if self._frame.mode == Frame.MODE_MOUSE: + self._hide_frame() + + def _show_frame(self): + if self._hide_sid != 0: + gobject.source_remove(self._hide_sid) + self._frame.show(Frame.MODE_MOUSE) + + def _hide_frame_timeout_cb(self): + self._frame.hide() + return False + + def _hide_frame(self): + if self._hide_sid != 0: + gobject.source_remove(self._hide_sid) + self._hide_sid = gobject.timeout_add( + _FRAME_HIDING_DELAY, self._hide_frame_timeout_cb) + +class _KeyListener(object): + def __init__(self, frame): + self._frame = frame + + def key_press(self): + if self._frame.visible: + if self._frame.mode == Frame.MODE_KEYBOARD: + self._frame.hide() + else: + self._frame.show(Frame.MODE_KEYBOARD) + +class Frame(object): + MODE_MOUSE = 0 + MODE_KEYBOARD = 1 + MODE_NON_INTERACTIVE = 2 + + def __init__(self, shell): + self.mode = None + + self._palette_group = palettegroup.get_group('frame') + self._palette_group.connect('popdown', self._palette_group_popdown_cb) + + self._left_panel = None + self._right_panel = None + self._top_panel = None + self._bottom_panel = None + + self._shell = shell + self.current_position = 0.0 + self._animator = None + + self._event_area = EventArea() + self._event_area.connect('enter', self._enter_corner_cb) + self._event_area.show() + + self._top_panel = self._create_top_panel() + self._bottom_panel = self._create_bottom_panel() + self._left_panel = self._create_left_panel() + self._right_panel = self._create_right_panel() + + screen = gtk.gdk.screen_get_default() + screen.connect('size-changed', self._size_changed_cb) + + cb_service = clipboardservice.get_instance() + cb_service.connect_after('object-added', self._clipboard_object_added_cb) + + self._key_listener = _KeyListener(self) + self._mouse_listener = _MouseListener(self) + + self.move(1.0) + + def is_visible(self): + return self.current_position != 0.0 + + def hide(self): + if self._animator: + self._animator.stop() + + self._animator = animator.Animator(0.5) + self._animator.add(_Animation(self, 0.0)) + self._animator.start() + + self._event_area.show() + + self.mode = None + + def show(self, mode): + if self.visible: + return + if self._animator: + self._animator.stop() + + self._shell.take_activity_screenshot() + + self.mode = mode + + self._animator = animator.Animator(0.5) + self._animator.add(_Animation(self, 1.0)) + self._animator.start() + + self._event_area.hide() + + def move(self, pos): + self.current_position = pos + self._update_position() + + def _is_hover(self): + return (self._top_panel.hover or \ + self._bottom_panel.hover or \ + self._left_panel.hover or \ + self._right_panel.hover) + + def _create_top_panel(self): + panel = self._create_panel(gtk.POS_TOP) + + toolbar = ZoomToolbar(self._shell) + panel.append(hippo.CanvasWidget(widget=toolbar)) + toolbar.show() + + return panel + + def _create_bottom_panel(self): + panel = self._create_panel(gtk.POS_BOTTOM) + + box = ActivitiesTray(self._shell) + panel.append(box, hippo.PACK_EXPAND) + + return panel + + def _create_right_panel(self): + panel = self._create_panel(gtk.POS_RIGHT) + + tray = FriendsTray(self._shell) + panel.append(hippo.CanvasWidget(widget=tray), hippo.PACK_EXPAND) + tray.show() + + return panel + + def _create_left_panel(self): + panel = ClipboardPanelWindow(self, gtk.POS_LEFT) + + self._connect_to_panel(panel) + panel.connect('drag-motion', self._drag_motion_cb) + panel.connect('drag-leave', self._drag_leave_cb) + + return panel + + def _create_panel(self, orientation): + panel = FrameWindow(orientation) + self._connect_to_panel(panel) + + return panel + + def _move_panel(self, panel, pos, x1, y1, x2, y2): + x = (x2 - x1) * pos + x1 + y = (y2 - y1) * pos + y1 + + panel.move(int(x), int(y)) + + # FIXME we should hide and show as necessary to free memory + if not panel.props.visible: + panel.show() + + def _connect_to_panel(self, panel): + panel.connect('enter-notify-event', self._enter_notify_cb) + panel.connect('leave-notify-event', self._leave_notify_cb) + + def _update_position(self): + screen_h = gtk.gdk.screen_height() + screen_w = gtk.gdk.screen_width() + + self._move_panel(self._top_panel, self.current_position, + 0, - self._top_panel.size, 0, 0) + + self._move_panel(self._bottom_panel, self.current_position, + 0, screen_h, 0, screen_h - self._bottom_panel.size) + + self._move_panel(self._left_panel, self.current_position, + - self._left_panel.size, 0, 0, 0) + + self._move_panel(self._right_panel, self.current_position, + screen_w, 0, screen_w - self._right_panel.size, 0) + + def _size_changed_cb(self, screen): + self._update_position() + + def _clipboard_object_added_cb(self, cb_service, object_id, name): + if not self.visible: + self.show(self.MODE_NON_INTERACTIVE) + gobject.timeout_add(2000, lambda: self.hide()) + + def _enter_notify_cb(self, window, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self._mouse_listener.mouse_enter() + + def _leave_notify_cb(self, window, event): + if event.detail == gtk.gdk.NOTIFY_INFERIOR: + return + + if not self._is_hover() and not self._palette_group.is_up(): + self._mouse_listener.mouse_leave() + + def _palette_group_popdown_cb(self, group): + if not self._is_hover(): + self._mouse_listener.mouse_leave() + + def _drag_motion_cb(self, window, context, x, y, time): + self._mouse_listener.mouse_enter() + + def _drag_leave_cb(self, window, drag_context, timestamp): + self._mouse_listener.mouse_leave() + + def _enter_corner_cb(self, event_area): + self._mouse_listener.mouse_enter() + + def notify_key_press(self): + self._key_listener.key_press() + + visible = property(is_visible, None) diff --git a/src/view/frame/frameinvoker.py b/src/view/frame/frameinvoker.py new file mode 100644 index 0000000..07dc9d8 --- /dev/null +++ b/src/view/frame/frameinvoker.py @@ -0,0 +1,39 @@ +# Copyright (C) 2007, Eduardo Silva +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk + +from sugar.graphics import style +from sugar.graphics.palette import Palette +from sugar.graphics.palette import CanvasInvoker +from sugar.graphics.palette import WidgetInvoker + +def _get_screen_area(): + frame_thickness = style.GRID_CELL_SIZE + + x = y = frame_thickness + width = gtk.gdk.screen_width() - frame_thickness + height = gtk.gdk.screen_height() - frame_thickness + + return gtk.gdk.Rectangle(x, y, width, height) + +class FrameWidgetInvoker(WidgetInvoker): + def __init__(self, widget): + WidgetInvoker.__init__(self, widget.child) + + self._position_hint = self.ANCHORED + self._screen_area = _get_screen_area() diff --git a/src/view/frame/framewindow.py b/src/view/frame/framewindow.py new file mode 100644 index 0000000..623d162 --- /dev/null +++ b/src/view/frame/framewindow.py @@ -0,0 +1,104 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gtk +import hippo + +from sugar.graphics import style + +class FrameWindow(gtk.Window): + __gtype_name__ = 'SugarFrameWindow' + + def __init__(self, position): + gtk.Window.__init__(self) + self.hover = False + self.size = style.GRID_CELL_SIZE + style.LINE_WIDTH + + self._position = position + + self.set_decorated(False) + self.connect('realize', self._realize_cb) + self.connect('enter-notify-event', self._enter_notify_cb) + self.connect('leave-notify-event', self._leave_notify_cb) + + self._canvas = hippo.Canvas() + self.add(self._canvas) + self._canvas.show() + + box = hippo.CanvasBox() + self._canvas.set_root(box) + + padding = style.GRID_CELL_SIZE + if self._position == gtk.POS_TOP or self._position == gtk.POS_BOTTOM: + box.props.orientation = hippo.ORIENTATION_HORIZONTAL + box.props.padding_left = padding + box.props.padding_right = padding + box.props.padding_top = 0 + box.props.padding_bottom = 0 + else: + box.props.orientation = hippo.ORIENTATION_VERTICAL + box.props.padding_left = 0 + box.props.padding_right = 0 + box.props.padding_top = padding + box.props.padding_bottom = padding + + self._bg = hippo.CanvasBox( + border_color=style.COLOR_BUTTON_GREY.get_int()) + + border = style.LINE_WIDTH + if position == gtk.POS_TOP: + self._bg.props.orientation = hippo.ORIENTATION_HORIZONTAL + self._bg.props.border_bottom = border + elif position == gtk.POS_BOTTOM: + self._bg.props.orientation = hippo.ORIENTATION_HORIZONTAL + self._bg.props.border_top = border + elif position == gtk.POS_LEFT: + self._bg.props.orientation = hippo.ORIENTATION_VERTICAL + self._bg.props.border_right = border + elif position == gtk.POS_RIGHT: + self._bg.props.orientation = hippo.ORIENTATION_VERTICAL + self._bg.props.border_left = border + + box.append(self._bg, hippo.PACK_EXPAND) + + self._update_size() + + screen = gtk.gdk.screen_get_default() + screen.connect('size-changed', self._size_changed_cb) + + def append(self, child, flags=0): + self._bg.append(child, flags) + + def _update_size(self): + if self._position == gtk.POS_TOP or self._position == gtk.POS_BOTTOM: + self.resize(gtk.gdk.screen_width(), self.size) + else: + self.resize(self.size, gtk.gdk.screen_height()) + + def _realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(False) + + def _enter_notify_cb(self, window, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self.hover = True + + def _leave_notify_cb(self, window, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self.hover = False + + def _size_changed_cb(self, screen): + self._update_size() diff --git a/src/view/frame/friendstray.py b/src/view/frame/friendstray.py new file mode 100644 index 0000000..b34f357 --- /dev/null +++ b/src/view/frame/friendstray.py @@ -0,0 +1,142 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 hippo + +from sugar.presence import presenceservice +from sugar.graphics.tray import VTray, TrayIcon + +from view.BuddyMenu import BuddyMenu +from view.frame.frameinvoker import FrameWidgetInvoker +from model.BuddyModel import BuddyModel + +class FriendIcon(TrayIcon): + def __init__(self, shell, buddy): + TrayIcon.__init__(self, icon_name='computer-xo', + xo_color=buddy.get_color()) + + palette = BuddyMenu(shell, buddy) + self.set_palette(palette) + palette.set_group_id('frame') + palette.props.invoker = FrameWidgetInvoker(self) + +class FriendsTray(VTray): + def __init__(self, shell): + VTray.__init__(self) + + self._shell = shell + self._activity_ps = None + self._joined_hid = -1 + self._left_hid = -1 + self._buddies = {} + + self._pservice = presenceservice.get_instance() + self._pservice.connect('activity-appeared', + self.__activity_appeared_cb) + + self._owner = self._pservice.get_owner() + + # Add initial activities the PS knows about + self._pservice.get_activities_async(reply_handler=self._get_activities_cb) + + home_model = shell.get_model().get_home() + home_model.connect('pending-activity-changed', + self._pending_activity_changed_cb) + + def _get_activities_cb(self, list): + for activity in list: + self.__activity_appeared_cb(self._pservice, activity) + + def add_buddy(self, buddy): + if self._buddies.has_key(buddy.props.key): + return + + model = BuddyModel(buddy=buddy) + + icon = FriendIcon(self._shell, model) + self.add_item(icon) + icon.show() + + self._buddies[buddy.props.key] = icon + + def remove_buddy(self, buddy): + if not self._buddies.has_key(buddy.props.key): + return + + self.remove_item(self._buddies[buddy.props.key]) + del self._buddies[buddy.props.key] + + def clear(self): + for item in self.get_children(): + self.remove_item(item) + self._buddies = {} + + def __activity_appeared_cb(self, pservice, activity_ps): + activity = self._shell.get_current_activity() + if activity and activity_ps.props.id == activity.get_id(): + self._set_activity_ps(activity_ps, True) + + def _set_activity_ps(self, activity_ps, shared_activity): + if self._activity_ps == activity_ps: + return + + if self._joined_hid > 0: + self._activity_ps.disconnect(self._joined_hid) + self._joined_hid = -1 + if self._left_hid > 0: + self._activity_ps.disconnect(self._left_hid) + self._left_hid = -1 + + self._activity_ps = activity_ps + + self.clear() + + if shared_activity is True: + for buddy in activity_ps.get_joined_buddies(): + self.add_buddy(buddy) + + self._joined_hid = activity_ps.connect( + 'buddy-joined', self.__buddy_joined_cb) + self._left_hid = activity_ps.connect( + 'buddy-left', self.__buddy_left_cb) + else: + # only display myself if not shared + self.add_buddy(self._owner) + + def _pending_activity_changed_cb(self, home_model, home_activity): + if home_activity is None: + return + + activity_id = home_activity.get_activity_id() + if activity_id is None: + return + + # check if activity is shared + activity = None + for act in self._pservice.get_activities(): + if activity_id == act.props.id: + activity = act + break + if activity: + self._set_activity_ps(activity, True) + else: + self._set_activity_ps(home_activity, False) + + def __buddy_joined_cb(self, activity, buddy): + self.add_buddy(buddy) + + def __buddy_left_cb(self, activity, buddy): + self.remove_buddy(buddy) diff --git a/src/view/frame/overlaybox.py b/src/view/frame/overlaybox.py new file mode 100644 index 0000000..bb74f18 --- /dev/null +++ b/src/view/frame/overlaybox.py @@ -0,0 +1,32 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 hippo + +from sugar.graphics.iconbutton import IconButton + +class OverlayBox(hippo.CanvasBox): + def __init__(self, shell): + hippo.CanvasBox.__init__(self, orientation=hippo.ORIENTATION_HORIZONTAL) + + self._shell = shell + + icon = IconButton(icon_name='stock-chat') + icon.connect('activated', self._overlay_clicked_cb) + self.append(icon) + + def _overlay_clicked_cb(self, item): + self._shell.toggle_chat_visibility() diff --git a/src/view/frame/zoomtoolbar.py b/src/view/frame/zoomtoolbar.py new file mode 100644 index 0000000..48e63de --- /dev/null +++ b/src/view/frame/zoomtoolbar.py @@ -0,0 +1,84 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gtk + +from sugar.graphics.palette import Palette +from sugar.graphics.toolbutton import ToolButton + +from view.frame.frameinvoker import FrameWidgetInvoker +from model.shellmodel import ShellModel + +class ZoomToolbar(gtk.Toolbar): + def __init__(self, shell): + gtk.Toolbar.__init__(self) + + self._shell = shell + + self.set_show_arrow(False) + + button = ToolButton(icon_name='zoom-neighborhood') + button.connect('clicked', + self._level_clicked_cb, + ShellModel.ZOOM_MESH) + self.insert(button, -1) + button.show() + + palette = Palette(_('Neighborhood')) + palette.props.invoker = FrameWidgetInvoker(button) + palette.set_group_id('frame') + button.set_palette(palette) + + button = ToolButton(icon_name='zoom-groups') + button.connect('clicked', + self._level_clicked_cb, + ShellModel.ZOOM_FRIENDS) + self.insert(button, -1) + button.show() + + palette = Palette(_('Group')) + palette.props.invoker = FrameWidgetInvoker(button) + palette.set_group_id('frame') + button.set_palette(palette) + + button = ToolButton(icon_name='zoom-home') + button.connect('clicked', + self._level_clicked_cb, + ShellModel.ZOOM_HOME) + self.insert(button, -1) + button.show() + + palette = Palette(_('Home')) + palette.props.invoker = FrameWidgetInvoker(button) + palette.set_group_id('frame') + button.set_palette(palette) + + button = ToolButton(icon_name='zoom-activity') + button.connect('clicked', + self._level_clicked_cb, + ShellModel.ZOOM_ACTIVITY) + self.insert(button, -1) + button.show() + + palette = Palette(_('Activity')) + palette.props.invoker = FrameWidgetInvoker(button) + palette.set_group_id('frame') + button.set_palette(palette) + + def _level_clicked_cb(self, button, level): + self._shell.set_zoom_level(level) diff --git a/src/view/home/FriendView.py b/src/view/home/FriendView.py new file mode 100644 index 0000000..786589f --- /dev/null +++ b/src/view/home/FriendView.py @@ -0,0 +1,86 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 hippo +import gobject + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style +from sugar.presence import presenceservice +from sugar import activity + +from view.BuddyIcon import BuddyIcon + +class FriendView(hippo.CanvasBox): + def __init__(self, shell, buddy, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + + self._pservice = presenceservice.get_instance() + + self._buddy = buddy + self._buddy_icon = BuddyIcon(shell, buddy) + self._buddy_icon.props.size = style.LARGE_ICON_SIZE + self.append(self._buddy_icon) + + self._activity_icon = CanvasIcon(size=style.LARGE_ICON_SIZE) + self._activity_icon_visible = False + + if self._buddy.is_present(): + self._buddy_appeared_cb(buddy) + + self._buddy.connect('current-activity-changed', self._buddy_activity_changed_cb) + self._buddy.connect('appeared', self._buddy_appeared_cb) + self._buddy.connect('disappeared', self._buddy_disappeared_cb) + self._buddy.connect('color-changed', self._buddy_color_changed_cb) + + def _get_new_icon_name(self, ps_activity): + registry = activity.get_registry() + activity_info = registry.get_activity(ps_activity.props.type) + if activity_info: + return activity_info.icon + return None + + def _remove_activity_icon(self): + if self._activity_icon_visible: + self.remove(self._activity_icon) + self._activity_icon_visible = False + + def _buddy_activity_changed_cb(self, buddy, ps_activity=None): + if not ps_activity: + self._remove_activity_icon() + return + + # FIXME: use some sort of "unknown activity" icon rather + # than hiding the icon? + name = self._get_new_icon_name(ps_activity) + if name: + self._activity_icon.props.file_name = name + self._activity_icon.props.xo_color = buddy.get_color() + if not self._activity_icon_visible: + self.append(self._activity_icon, hippo.PACK_EXPAND) + self._activity_icon_visible = True + else: + self._remove_activity_icon() + + def _buddy_appeared_cb(self, buddy): + home_activity = self._buddy.get_current_activity() + self._buddy_activity_changed_cb(buddy, home_activity) + + def _buddy_disappeared_cb(self, buddy): + self._buddy_activity_changed_cb(buddy, None) + + def _buddy_color_changed_cb(self, buddy, color): + self._activity_icon.props.xo_color = buddy.get_color() diff --git a/src/view/home/FriendsBox.py b/src/view/home/FriendsBox.py new file mode 100644 index 0000000..e9efc57 --- /dev/null +++ b/src/view/home/FriendsBox.py @@ -0,0 +1,67 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 random + +import hippo +import gobject + +from sugar import profile +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon +from sugar.graphics.palette import Palette + +from view.home.FriendView import FriendView +from view.home.spreadlayout import SpreadLayout + +class FriendsBox(hippo.CanvasBox): + __gtype_name__ = 'SugarFriendsBox' + def __init__(self, shell): + hippo.CanvasBox.__init__(self, background_color=0xe2e2e2ff) + + self._shell = shell + self._friends = {} + + self._layout = SpreadLayout() + self.set_layout(self._layout) + + self._owner_icon = CanvasIcon(icon_name='computer-xo', cache=True, + xo_color=profile.get_color()) + self._owner_icon.props.size = style.LARGE_ICON_SIZE + palette = Palette(profile.get_nick_name()) + self._owner_icon.set_palette(palette) + self._layout.add_center(self._owner_icon) + + friends = self._shell.get_model().get_friends() + + for friend in friends: + self.add_friend(friend) + + friends.connect('friend-added', self._friend_added_cb) + friends.connect('friend-removed', self._friend_removed_cb) + + def add_friend(self, buddy_info): + icon = FriendView(self._shell, buddy_info) + self._layout.add(icon) + + self._friends[buddy_info.get_key()] = icon + + def _friend_added_cb(self, data_model, buddy_info): + self.add_friend(buddy_info) + + def _friend_removed_cb(self, data_model, key): + self._layout.remove(self._friends[key]) + del self._friends[key] diff --git a/src/view/home/HomeBox.py b/src/view/home/HomeBox.py new file mode 100644 index 0000000..8764887 --- /dev/null +++ b/src/view/home/HomeBox.py @@ -0,0 +1,287 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 signal +from gettext import gettext as _ +import re + +import gobject +import gtk +import hippo +import dbus + +from hardware import hardwaremanager +from sugar.graphics import style +from sugar.graphics.palette import Palette +from sugar.profile import get_profile +from sugar import env + +from view.home.activitiesdonut import ActivitiesDonut +from view.devices import deviceview +from view.home.MyIcon import MyIcon +from model.shellmodel import ShellModel +from hardware import schoolserver + +_logger = logging.getLogger('HomeBox') + +class HomeBox(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = 'SugarHomeBox' + + def __init__(self, shell): + hippo.CanvasBox.__init__(self, background_color=0xe2e2e2ff) + + self._redraw_id = None + + shell_model = shell.get_model() + + top_box = hippo.CanvasBox(box_height=style.GRID_CELL_SIZE * 2.5) + self.append(top_box) + + center_box = hippo.CanvasBox(yalign=hippo.ALIGNMENT_CENTER) + self.append(center_box, hippo.PACK_EXPAND) + + bottom_box = hippo.CanvasBox(box_height=style.GRID_CELL_SIZE * 2.5) + self.append(bottom_box) + + self._donut = ActivitiesDonut(shell) + center_box.append(self._donut) + + self._my_icon = _MyIcon(shell, style.XLARGE_ICON_SIZE) + self.append(self._my_icon, hippo.PACK_FIXED) + + self._devices_box = _DevicesBox(shell_model.get_devices()) + bottom_box.append(self._devices_box) + + shell_model.connect('notify::state', + self._shell_state_changed_cb) + + def _shell_state_changed_cb(self, model, pspec): + # FIXME implement this + if model.props.state == ShellModel.STATE_SHUTDOWN: + pass + + def do_allocate(self, width, height, origin_changed): + hippo.CanvasBox.do_allocate(self, width, height, origin_changed) + + [icon_width, icon_height] = self._my_icon.get_allocation() + self.set_position(self._my_icon, (width - icon_width) / 2, + (height - icon_height) / 2) + + _REDRAW_TIMEOUT = 5 * 60 * 1000 # 5 minutes + + def resume(self): + if self._redraw_id is None: + self._redraw_id = gobject.timeout_add(self._REDRAW_TIMEOUT, + self._redraw_activity_ring) + self._redraw_activity_ring() + + def suspend(self): + if self._redraw_id is not None: + gobject.source_remove(self._redraw_id) + self._redraw_id = None + + def _redraw_activity_ring(self): + self._donut.redraw() + return True + + def has_activities(self): + return self._donut.has_activities() + + def enable_xo_palette(self): + self._my_icon.enable_palette() + + def grab_and_rotate(self): + pass + + def rotate(self): + pass + + def release(self): + pass + +class _DevicesBox(hippo.CanvasBox): + def __init__(self, devices_model): + gobject.GObject.__init__(self, + orientation=hippo.ORIENTATION_HORIZONTAL, + xalign=hippo.ALIGNMENT_CENTER) + + self._device_icons = {} + + for device in devices_model: + self._add_device(device) + + devices_model.connect('device-appeared', + self._device_appeared_cb) + devices_model.connect('device-disappeared', + self._device_disappeared_cb) + + def _add_device(self, device): + view = deviceview.create(device) + self.append(view) + self._device_icons[device.get_id()] = view + + def _remove_device(self, device): + self.remove(self._device_icons[device.get_id()]) + del self._device_icons[device.get_id()] + + def _device_appeared_cb(self, model, device): + self._add_device(device) + + def _device_disappeared_cb(self, model, device): + self._remove_device(device) + +class _MyIcon(MyIcon): + def __init__(self, shell, scale): + MyIcon.__init__(self, scale) + + self._power_manager = None + self._shell = shell + self._profile = get_profile() + + def enable_palette(self): + palette = Palette(self._profile.nick_name) + + item = gtk.MenuItem(_('Reboot')) + item.connect('activate', self._reboot_activate_cb) + palette.menu.append(item) + item.show() + + item = gtk.MenuItem(_('Shutdown')) + item.connect('activate', self._shutdown_activate_cb) + palette.menu.append(item) + item.show() + + if not self._profile.is_registered(): + item = gtk.MenuItem(_('Register')) + item.connect('activate', self._register_activate_cb) + palette.menu.append(item) + item.show() + + item = gtk.MenuItem(_('About this XO')) + item.connect('activate', self._about_activate_cb) + palette.menu.append(item) + item.show() + + self.set_palette(palette) + + def _reboot_activate_cb(self, menuitem): + model = self._shell.get_model() + model.props.state = ShellModel.STATE_SHUTDOWN + + pm = self._get_power_manager() + + hw_manager = hardwaremanager.get_manager() + hw_manager.shutdown() + + if env.is_emulator(): + self._close_emulator() + else: + pm.Reboot() + + def _shutdown_activate_cb(self, menuitem): + model = self._shell.get_model() + model.props.state = ShellModel.STATE_SHUTDOWN + + pm = self._get_power_manager() + + hw_manager = hardwaremanager.get_manager() + hw_manager.shutdown() + + if env.is_emulator(): + self._close_emulator() + else: + pm.Shutdown() + + def _register_activate_cb(self, menuitem): + schoolserver.register_laptop() + if self._profile.is_registered(): + self.get_palette().menu.remove(menuitem) + + def _about_activate_cb(self, menuitem): + dialog = gtk.Dialog(_('About this XO'), + self.palette, + gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_OK, gtk.RESPONSE_OK)) + + not_available = _('Not available') + build = self._read_file('/boot/olpc_build') + if build is None: + build = not_available + label_build = gtk.Label('Build: %s' % build) + label_build.set_alignment(0, 0.5) + label_build.show() + dialog.vbox.pack_start(label_build) + + firmware = self._read_file('/ofw/openprom/model') + if firmware is None: + firmware = not_available + else: + firmware = re.split(" +", firmware) + if len(firmware) == 3: + firmware = firmware[1] + label_firmware = gtk.Label('Firmware: %s' % firmware) + label_firmware.set_alignment(0, 0.5) + label_firmware.show() + dialog.vbox.pack_start(label_firmware) + + serial = self._read_file('/ofw/serial-number') + if serial is None: + serial = not_available + label_serial = gtk.Label('Serial Number: %s' % serial) + label_serial.set_alignment(0, 0.5) + label_serial.show() + dialog.vbox.pack_start(label_serial) + + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.connect('response', self._response_cb) + dialog.show() + + def _read_file(self, path): + if os.access(path, os.R_OK) == 0: + _logger.error('read_file() No such file or directory: %s', path) + return None + + fd = open(path, 'r') + value = fd.read() + fd.close() + if value: + value = value.strip('\n') + return value + else: + _logger.error('read_file() No information in file or directory: %s', path) + return None + + def _response_cb(self, widget, response_id): + if response_id == gtk.RESPONSE_OK: + widget.destroy() + + def _close_emulator(self): + if os.environ.has_key('SUGAR_EMULATOR_PID'): + pid = int(os.environ['SUGAR_EMULATOR_PID']) + os.kill(pid, signal.SIGTERM) + + def _get_power_manager(self): + if self._power_manager is None: + bus = dbus.SystemBus() + proxy = bus.get_object('org.freedesktop.Hal', + '/org/freedesktop/Hal/devices/computer') + self._power_manager = dbus.Interface(proxy, \ + 'org.freedesktop.Hal.Device.SystemPowerManagement') + + return self._power_manager diff --git a/src/view/home/HomeWindow.py b/src/view/home/HomeWindow.py new file mode 100644 index 0000000..f1f46e9 --- /dev/null +++ b/src/view/home/HomeWindow.py @@ -0,0 +1,141 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gtk +import hippo +import cairo + +from sugar.graphics import style + +from view.home.MeshBox import MeshBox +from view.home.HomeBox import HomeBox +from view.home.FriendsBox import FriendsBox +from view.home.transitionbox import TransitionBox +from model.shellmodel import ShellModel + +_HOME_PAGE = 0 +_FRIENDS_PAGE = 1 +_MESH_PAGE = 2 +_TRANSITION_PAGE = 3 + +class HomeWindow(gtk.Window): + def __init__(self, shell): + gtk.Window.__init__(self) + + self._shell = shell + self._active = False + self._level = ShellModel.ZOOM_HOME + + self._canvas = hippo.Canvas() + self.add(self._canvas) + self._canvas.show() + + self.set_default_size(gtk.gdk.screen_width(), + gtk.gdk.screen_height()) + + self.realize() + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DESKTOP) + self.connect("key-release-event", self._key_release_cb) + self.connect('focus-in-event', self._focus_in_cb) + self.connect('focus-out-event', self._focus_out_cb) + + self._enter_sid = self.connect('enter-notify-event', + self._enter_notify_event_cb) + self._leave_sid = self.connect('leave-notify-event', + self._leave_notify_event_cb) + self._motion_sid = self.connect('motion-notify-event', + self._motion_notify_event_cb) + + self._home_box = HomeBox(shell) + self._friends_box = FriendsBox(shell) + self._mesh_box = MeshBox(shell) + self._transition_box = TransitionBox() + + self._activate_view() + self._canvas.set_root(self._home_box) + + self._transition_box.connect('completed', + self._transition_completed_cb) + + def _enter_notify_event_cb(self, window, event): + if event.x != gtk.gdk.screen_width() / 2 or \ + event.y != gtk.gdk.screen_height() / 2: + self._mouse_moved() + + def _leave_notify_event_cb(self, window, event): + self._mouse_moved() + + def _motion_notify_event_cb(self, window, event): + self._mouse_moved() + + # We want to enable the XO palette only when the user + # moved away from the default mouse position (screen center). + def _mouse_moved(self): + self._home_box.enable_xo_palette() + self.disconnect(self._leave_sid) + self.disconnect(self._motion_sid) + self.disconnect(self._enter_sid) + + def _key_release_cb(self, widget, event): + keyname = gtk.gdk.keyval_name(event.keyval) + if keyname == "Alt_L": + self._home_box.release() + + def _deactivate_view(self): + if self._level == ShellModel.ZOOM_HOME: + self._home_box.suspend() + elif self._level == ShellModel.ZOOM_MESH: + self._mesh_box.suspend() + + def _activate_view(self): + if self._level == ShellModel.ZOOM_HOME: + self._home_box.resume() + elif self._level == ShellModel.ZOOM_MESH: + self._mesh_box.resume() + + def _focus_in_cb(self, widget, event): + self._activate_view() + + def _focus_out_cb(self, widget, event): + self._deactivate_view() + + def set_zoom_level(self, level): + self._deactivate_view() + self._level = level + self._activate_view() + + self._canvas.set_root(self._transition_box) + + if level == ShellModel.ZOOM_HOME: + size = style.XLARGE_ICON_SIZE + elif level == ShellModel.ZOOM_FRIENDS: + size = style.LARGE_ICON_SIZE + elif level == ShellModel.ZOOM_MESH: + size = style.STANDARD_ICON_SIZE + + self._transition_box.set_size(size) + + def _transition_completed_cb(self, transition_box): + if self._level == ShellModel.ZOOM_HOME: + self._canvas.set_root(self._home_box) + elif self._level == ShellModel.ZOOM_FRIENDS: + self._canvas.set_root(self._friends_box) + elif self._level == ShellModel.ZOOM_MESH: + self._canvas.set_root(self._mesh_box) + self._mesh_box.focus_search_entry() + + def get_home_box(self): + return self._home_box diff --git a/src/view/home/Makefile.am b/src/view/home/Makefile.am new file mode 100644 index 0000000..6916806 --- /dev/null +++ b/src/view/home/Makefile.am @@ -0,0 +1,14 @@ +sugardir = $(pkgdatadir)/shell/view/home +sugar_PYTHON = \ + __init__.py \ + activitiesdonut.py \ + FriendView.py \ + FriendsBox.py \ + HomeBox.py \ + HomeWindow.py \ + MeshBox.py \ + MyIcon.py \ + proc_smaps.py \ + snowflakelayout.py \ + spreadlayout.py \ + transitionbox.py diff --git a/src/view/home/MeshBox.py b/src/view/home/MeshBox.py new file mode 100644 index 0000000..3b7c4a7 --- /dev/null +++ b/src/view/home/MeshBox.py @@ -0,0 +1,615 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 random +from gettext import gettext as _ +import logging + +import hippo +import gobject +import gtk + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style +from sugar.graphics.icon import get_icon_state +from sugar.graphics import style +from sugar.graphics import palette +from sugar.graphics import iconentry +from sugar.graphics.menuitem import MenuItem +from sugar import profile + +from model import accesspointmodel +from model.devices.network import mesh +from model.devices.network import wireless +from hardware import hardwaremanager +from hardware import nmclient +from view.BuddyIcon import BuddyIcon +from view.pulsingicon import PulsingIcon +from view.home.snowflakelayout import SnowflakeLayout +from view.home.spreadlayout import SpreadLayout + +from hardware.nmclient import NM_802_11_CAP_PROTO_WEP, NM_802_11_CAP_PROTO_WPA, NM_802_11_CAP_PROTO_WPA2 + + +_ICON_NAME = 'network-wireless' + +class AccessPointView(PulsingIcon): + def __init__(self, model, mesh_device=None): + PulsingIcon.__init__(self, size=style.STANDARD_ICON_SIZE, cache=True) + self._model = model + self._meshdev = mesh_device + self._disconnect_item = None + self._greyed_out = False + + self.connect('activated', self._activate_cb) + + model.connect('notify::strength', self._strength_changed_cb) + model.connect('notify::name', self._name_changed_cb) + model.connect('notify::state', self._state_changed_cb) + + (stroke, fill) = model.get_nm_network().get_colors() + self._device_stroke = stroke + self._device_fill = fill + + self._palette = self._create_palette() + self.set_palette(self._palette) + + self._update_icon() + self._update_name() + self._update_state() + + # Update badge + caps = model.props.capabilities + if model.get_nm_network().is_favorite(): + self.props.badge_name = "emblem-favorite" + elif (caps & NM_802_11_CAP_PROTO_WEP) or (caps & NM_802_11_CAP_PROTO_WPA) or (caps & NM_802_11_CAP_PROTO_WPA2): + self.props.badge_name = "emblem-locked" + + def _create_palette(self): + p = palette.Palette(self._model.props.name, menu_after_content=True) + if not self._meshdev: + return p + + # Only show disconnect when there's a mesh device, because mesh takes + # priority over the normal wireless device. NM doesn't have a "disconnect" + # method for a device either (for various reasons) so this doesn't + # have a good mapping + self._disconnect_item = gtk.MenuItem(_('Disconnect...')) + self._disconnect_item.connect('activate', self._disconnect_activate_cb) + p.menu.append(self._disconnect_item) + if self._model.props.state == accesspointmodel.STATE_CONNECTED: + self._disconnect_item.show() + return p + + def _disconnect_activate_cb(self, menuitem): + # Disconnection for an AP means activating the default mesh device + network_manager = hardwaremanager.get_network_manager() + if network_manager and self._meshdev: + network_manager.set_active_device(self._meshdev) + + def _strength_changed_cb(self, model, pspec): + self._update_icon() + + def _name_changed_cb(self, model, pspec): + self._update_name() + + def _state_changed_cb(self, model, pspec): + self._update_state() + + def _activate_cb(self, icon): + network_manager = hardwaremanager.get_network_manager() + if network_manager: + device = self._model.get_nm_device() + network = self._model.get_nm_network() + network_manager.set_active_device(device, network) + + def _update_name(self): + self._palette.set_primary_text(self._model.props.name) + + def _update_icon(self): + icon_name = get_icon_state(_ICON_NAME, self._model.props.strength) + if icon_name: + self.props.icon_name = icon_name + + def _update_state(self): + if self._model.props.state == accesspointmodel.STATE_CONNECTING: + if self._disconnect_item: + self._disconnect_item.hide() + self.props.pulse_time = 1.0 + self.props.colors = [ + [ style.Color(self._device_stroke).get_svg(), + style.Color(self._device_fill).get_svg() ], + [ style.Color(self._device_stroke).get_svg(), + '#e2e2e2' ] + ] + elif self._model.props.state == accesspointmodel.STATE_CONNECTED: + if self._disconnect_item: + self._disconnect_item.show() + self.props.pulse_time = 0.0 + self.props.colors = [ + [ '#ffffff', + style.Color(self._device_fill).get_svg() ], + [ '#ffffff', + style.Color(self._device_fill).get_svg() ] + ] + elif self._model.props.state == accesspointmodel.STATE_NOTCONNECTED: + if self._disconnect_item: + self._disconnect_item.hide() + self.props.pulse_time = 0.0 + self.props.colors = [ + [ style.Color(self._device_stroke).get_svg(), + style.Color(self._device_fill).get_svg() ] + ] + + if self._greyed_out: + self.props.pulse_time = 0.0 + self.props.colors = [['#D5D5D5', '#D5D5D5']] + + def set_filter(self, query): + self._greyed_out = self._model.props.name.lower().find(query) == -1 + self._update_state() + +_MESH_ICON_NAME = 'network-mesh' + +class MeshDeviceView(PulsingIcon): + def __init__(self, nm_device, channel): + if not channel in [1, 6, 11]: + raise ValueError("Invalid channel %d" % channel) + + PulsingIcon.__init__(self, size=style.STANDARD_ICON_SIZE, + icon_name=_MESH_ICON_NAME, cache=True) + + self._nm_device = nm_device + self.channel = channel + self.props.badge_name = "badge-channel-%d" % self.channel + self._greyed_out = False + + self._disconnect_item = None + self._palette = self._create_palette() + self.set_palette(self._palette) + + mycolor = profile.get_color() + self._device_fill = mycolor.get_fill_color() + self._device_stroke = mycolor.get_stroke_color() + + self.connect('activated', self._activate_cb) + + self._nm_device.connect('state-changed', self._state_changed_cb) + self._nm_device.connect('activation-stage-changed', self._state_changed_cb) + self._update_state() + + def _create_palette(self): + p = palette.Palette(_("Mesh Network") + " " + str(self.channel), menu_after_content=True) + + self._disconnect_item = gtk.MenuItem(_('Disconnect...')) + self._disconnect_item.connect('activate', self._disconnect_activate_cb) + p.menu.append(self._disconnect_item) + + state = self._nm_device.get_state() + chan = wireless.freq_to_channel(self._nm_device.get_frequency()) + if state == nmclient.DEVICE_STATE_ACTIVATED and chan == self.channel: + self._disconnect_item.show() + return p + + def _disconnect_activate_cb(self, menuitem): + network_manager = hardwaremanager.get_network_manager() + if network_manager: + network_manager.set_active_device(self._nm_device) + + def _activate_cb(self, icon): + network_manager = hardwaremanager.get_network_manager() + if network_manager: + freq = wireless.channel_to_freq(self.channel) + network_manager.set_active_device(self._nm_device, mesh_freq=freq) + + def _state_changed_cb(self, model): + self._update_state() + + def _update_state(self): + state = self._nm_device.get_state() + chan = wireless.freq_to_channel(self._nm_device.get_frequency()) + if self._greyed_out: + self.props.colors = [['#D5D5D5', '#D5D5D5']] + elif state == nmclient.DEVICE_STATE_ACTIVATING and chan == self.channel: + self._disconnect_item.hide() + self.props.pulse_time = 0.75 + self.props.colors = [ + [ style.Color(self._device_stroke).get_svg(), + style.Color(self._device_fill).get_svg() ], + [ style.Color(self._device_stroke).get_svg(), + '#e2e2e2' ] + ] + elif state == nmclient.DEVICE_STATE_ACTIVATED and chan == self.channel: + self._disconnect_item.show() + self.props.pulse_time = 0.0 + self.props.colors = [ + [ '#ffffff', + style.Color(self._device_fill).get_svg() ], + [ '#ffffff', + style.Color(self._device_fill).get_svg() ] + ] + elif state == nmclient.DEVICE_STATE_INACTIVE or chan != self.channel: + self._disconnect_item.hide() + self.props.pulse_time = 0.0 + self.props.colors = [ + [ style.Color(self._device_stroke).get_svg(), + style.Color(self._device_fill).get_svg() ] + ] + else: + raise RuntimeError("Shouldn't get here") + + def set_filter(self, query): + self._greyed_out = (query != '') + self._update_state() + +class ActivityView(hippo.CanvasBox): + def __init__(self, shell, model): + hippo.CanvasBox.__init__(self) + + self._shell = shell + self._model = model + self._icons = {} + + self._layout = SnowflakeLayout() + self.set_layout(self._layout) + + self._icon = self._create_icon() + self._layout.add(self._icon, center=True) + + self._update_palette() + + activity = self._model.activity + activity.connect('notify::name', self._name_changed_cb) + activity.connect('notify::color', self._color_changed_cb) + activity.connect('notify::private', self._private_changed_cb) + activity.connect('joined', self._joined_changed_cb) + #FIXME: 'joined' signal not working, see #5032 + + def _create_icon(self): + icon = CanvasIcon(file_name=self._model.get_icon_name(), + xo_color=self._model.get_color(), cache=True, + size=style.STANDARD_ICON_SIZE) + icon.connect('activated', self._clicked_cb) + return icon + + def _create_palette(self): + p = palette.Palette(self._model.activity.props.name) + + private = self._model.activity.props.private + joined = self._model.activity.props.joined + + if joined: + item = MenuItem(_('Resume'), 'activity-start') + item.connect('activate', self._clicked_cb) + item.show() + p.menu.append(item) + elif not private: + item = MenuItem(_('Join'), 'activity-start') + item.connect('activate', self._clicked_cb) + item.show() + p.menu.append(item) + + return p + + def _update_palette(self): + self._palette = self._create_palette() + self._icon.set_palette(self._palette) + + def has_buddy_icon(self, key): + return self._icons.has_key(key) + + def add_buddy_icon(self, key, icon): + self._icons[key] = icon + self._layout.add(icon) + + def remove_buddy_icon(self, key): + icon = self._icons[key] + del self._icons[key] + icon.destroy() + + def _clicked_cb(self, item): + bundle_id = self._model.get_bundle_id() + self._shell.join_activity(bundle_id, self._model.get_id()) + + def set_filter(self, query): + text_to_check = self._model.activity.props.name.lower() + \ + self._model.activity.props.type.lower() + if text_to_check.find(query) == -1: + self._icon.props.stroke_color = '#D5D5D5' + self._icon.props.fill_color = '#E5E5E5' + else: + self._icon.props.xo_color = self._model.get_color() + + for key, icon in self._icons.iteritems(): + if hasattr(icon, 'set_filter'): + icon.set_filter(query) + + def _name_changed_cb(self, activity, pspec): + self._update_palette() + + def _color_changed_cb(self, activity, pspec): + self._layout.remove(self._icon) + self._icon = self._create_icon() + self._layout.add(self._icon, center=True) + self._icon.set_palette(self._palette) + + def _private_changed_cb(self, activity, pspec): + self._update_palette() + + def _joined_changed_cb(self, widget, event): + logging.debug('ActivityView._joined_changed_cb: AAAA!!!!') + +_AUTOSEARCH_TIMEOUT = 1000 + +class MeshToolbar(gtk.Toolbar): + __gtype_name__ = 'MeshToolbar' + + __gsignals__ = { + 'query-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])) + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + self._query = None + self._autosearch_timer = None + + self._add_separator() + + tool_item = gtk.ToolItem() + tool_item.set_expand(True) + self.insert(tool_item, -1) + tool_item.show() + + self._search_entry = iconentry.IconEntry() + self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, 'system-search') + self._search_entry.add_clear_button() + self._search_entry.connect('activate', self._entry_activated_cb) + self._search_entry.connect('changed', self._entry_changed_cb) + tool_item.add(self._search_entry) + self._search_entry.show() + + self._add_separator() + + def _add_separator(self): + separator = gtk.SeparatorToolItem() + separator.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) + separator.props.draw = False + self.insert(separator, -1) + separator.show() + + def _entry_activated_cb(self, entry): + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + new_query = entry.props.text + if self._query != new_query: + self._query = new_query + self.emit('query-changed', self._query) + + def _entry_changed_cb(self, entry): + if not entry.props.text: + entry.activate() + return + + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT, + self._autosearch_timer_cb) + + def _autosearch_timer_cb(self): + logging.debug('_autosearch_timer_cb') + self._autosearch_timer = None + self._search_entry.activate() + return False + +class MeshBox(hippo.CanvasBox): + def __init__(self, shell): + hippo.CanvasBox.__init__(self) + + self._shell = shell + self._model = shell.get_model().get_mesh() + self._buddies = {} + self._activities = {} + self._access_points = {} + self._mesh = {} + self._buddy_to_activity = {} + self._suspended = True + self._query = '' + + self._toolbar = MeshToolbar() + self._toolbar.connect('query-changed', self._toolbar_query_changed_cb) + self.append(hippo.CanvasWidget(widget=self._toolbar)) + + self._layout_box = hippo.CanvasBox(background_color=0xe2e2e2ff) + self.append(self._layout_box, hippo.PACK_EXPAND) + + self._layout = SpreadLayout() + self._layout_box.set_layout(self._layout) + + for buddy_model in self._model.get_buddies(): + self._add_alone_buddy(buddy_model) + + self._model.connect('buddy-added', self._buddy_added_cb) + self._model.connect('buddy-removed', self._buddy_removed_cb) + self._model.connect('buddy-moved', self._buddy_moved_cb) + + for activity_model in self._model.get_activities(): + self._add_activity(activity_model) + + self._model.connect('activity-added', self._activity_added_cb) + self._model.connect('activity-removed', self._activity_removed_cb) + + for ap_model in self._model.get_access_points(): + self._add_access_point(ap_model) + + self._model.connect('access-point-added', + self._access_point_added_cb) + self._model.connect('access-point-removed', + self._access_point_removed_cb) + + if self._model.get_mesh(): + self._mesh_added_cb(self._model, self._model.get_mesh()) + + self._model.connect('mesh-added', + self._mesh_added_cb) + self._model.connect('mesh-removed', + self._mesh_removed_cb) + + def _mesh_added_cb(self, model, meshdev): + self._add_mesh_icon(meshdev, 1) + self._add_mesh_icon(meshdev, 6) + self._add_mesh_icon(meshdev, 11) + + def _mesh_removed_cb(self, model): + self._remove_mesh_icon(1) + self._remove_mesh_icon(6) + self._remove_mesh_icon(11) + + def _buddy_added_cb(self, model, buddy_model): + self._add_alone_buddy(buddy_model) + + def _buddy_removed_cb(self, model, buddy_model): + self._remove_buddy(buddy_model) + + def _buddy_moved_cb(self, model, buddy_model, activity_model): + # Owner doesn't move from the center + if buddy_model.is_owner(): + return + self._move_buddy(buddy_model, activity_model) + + def _activity_added_cb(self, model, activity_model): + self._add_activity(activity_model) + + def _activity_removed_cb(self, model, activity_model): + self._remove_activity(activity_model) + + def _access_point_added_cb(self, model, ap_model): + self._add_access_point(ap_model) + + def _access_point_removed_cb(self, model, ap_model): + self._remove_access_point(ap_model) + + def _add_mesh_icon(self, meshdev, channel): + if self._mesh.has_key(channel): + self._remove_mesh_icon(channel) + if not meshdev: + return + self._mesh[channel] = MeshDeviceView(meshdev, channel) + self._layout.add(self._mesh[channel]) + + def _remove_mesh_icon(self, channel): + if not self._mesh.has_key(channel): + return + self._layout.remove(self._mesh[channel]) + del self._mesh[channel] + + def _add_alone_buddy(self, buddy_model): + icon = BuddyIcon(self._shell, buddy_model) + if buddy_model.is_owner(): + vertical_offset = - style.GRID_CELL_SIZE + self._layout.add_center(icon, vertical_offset) + else: + self._layout.add(icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + self._buddies[buddy_model.get_key()] = icon + + def _remove_alone_buddy(self, buddy_model): + icon = self._buddies[buddy_model.get_key()] + self._layout.remove(icon) + del self._buddies[buddy_model.get_key()] + icon.destroy() + + def _remove_buddy(self, buddy_model): + key = buddy_model.get_key() + if self._buddies.has_key(key): + self._remove_alone_buddy(buddy_model) + else: + for activity in self._activities.values(): + if activity.has_buddy_icon(key): + activity.remove_buddy_icon(key) + + def _move_buddy(self, buddy_model, activity_model): + key = buddy_model.get_key() + + self._remove_buddy(buddy_model) + + if activity_model == None: + self._add_alone_buddy(buddy_model) + elif activity_model.get_id() in self._activities: + activity = self._activities[activity_model.get_id()] + + icon = BuddyIcon(self._shell, buddy_model, + style.SMALL_ICON_SIZE) + activity.add_buddy_icon(buddy_model.get_key(), icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + def _add_activity(self, activity_model): + icon = ActivityView(self._shell, activity_model) + self._layout.add(icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + self._activities[activity_model.get_id()] = icon + + def _remove_activity(self, activity_model): + icon = self._activities[activity_model.get_id()] + self._layout.remove(icon) + del self._activities[activity_model.get_id()] + icon.destroy() + + def _add_access_point(self, ap_model): + meshdev = self._model.get_mesh() + icon = AccessPointView(ap_model, meshdev) + self._layout.add(icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + self._access_points[ap_model.get_id()] = icon + + def _remove_access_point(self, ap_model): + icon = self._access_points[ap_model.get_id()] + self._layout.remove(icon) + del self._access_points[ap_model.get_id()] + + def suspend(self): + if not self._suspended: + self._suspended = True + for ap in self._access_points.values(): + ap.props.paused = True + + def resume(self): + if self._suspended: + self._suspended = False + for ap in self._access_points.values(): + ap.props.paused = False + + def _toolbar_query_changed_cb(self, toolbar, query): + self._query = query.lower() + for icon in self._layout_box.get_children(): + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + def focus_search_entry(self): + self._toolbar._search_entry.grab_focus() diff --git a/src/view/home/MyIcon.py b/src/view/home/MyIcon.py new file mode 100644 index 0000000..af0f6ce --- /dev/null +++ b/src/view/home/MyIcon.py @@ -0,0 +1,24 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 sugar.graphics.icon import CanvasIcon +from sugar import profile + +class MyIcon(CanvasIcon): + def __init__(self, size): + CanvasIcon.__init__(self, size=size, + icon_name='computer-xo', + xo_color=profile.get_color()) diff --git a/src/view/home/__init__.py b/src/view/home/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/view/home/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 + diff --git a/src/view/home/activitiesdonut.py b/src/view/home/activitiesdonut.py new file mode 100755 index 0000000..8e09006 --- /dev/null +++ b/src/view/home/activitiesdonut.py @@ -0,0 +1,556 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 colorsys +from gettext import gettext as _ +import logging +import math +import os + +import hippo +import gobject +import gtk + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.palette import Palette +from sugar.graphics import style +from sugar.graphics import xocolor +from sugar import profile +import proc_smaps + +_MAX_ACTIVITIES = 6 +_MIN_WEDGE_SIZE = 1.0 / _MAX_ACTIVITIES +_DONUT_SIZE = style.zoom(450) + +# TODO: rgb_to_html and html_to_rgb are useful elsewhere +# we should put this in a common module +def rgb_to_html(r, g, b): + """ (r, g, b) tuple (in float format) -> #RRGGBB """ + return '#%02x%02x%02x' % (int(r * 255), int(g * 255), int(b * 255)) + +def html_to_rgb(html_color): + """ #RRGGBB -> (r, g, b) tuple (in float format) """ + html_color = html_color.strip() + if html_color[0] == '#': + html_color = html_color[1:] + if len(html_color) != 6: + raise ValueError, "input #%s is not in #RRGGBB format" % html_color + r, g, b = html_color[:2], html_color[2:4], html_color[4:] + r, g, b = [int(n, 16) for n in (r, g, b)] + r, g, b = (r / 255.0, g / 255.0, b / 255.0) + return (r, g, b) + +class ActivityIcon(CanvasIcon): + _INTERVAL = 200 + + __gsignals__ = { + 'resume': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'stop': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self, activity): + self._orig_color = activity.get_icon_color() + self._icon_colors = self._compute_icon_colors() + + self._direction = 0 + self._level_max = len(self._icon_colors) - 1 + self._level = self._level_max + color = self._icon_colors[self._level] + + CanvasIcon.__init__(self, xo_color=color, cache=True, + size=style.MEDIUM_ICON_SIZE) + + icon_path = activity.get_icon_path() + if icon_path: + self.props.file_name = icon_path + else: + self.props.icon_name = 'image-missing' + + self._activity = activity + self._pulse_id = 0 + + self.size = _MIN_WEDGE_SIZE + + palette = Palette(_('Starting...')) + self.set_palette(palette) + + if activity.props.launching: + self._start_pulsing() + activity.connect('notify::launching', self._launching_changed_cb) + else: + self._setup_palette() + + def _setup_palette(self): + palette = self.get_palette() + + palette.set_primary_text(self._activity.get_title()) + + resume_menu_item = MenuItem(_('Resume'), 'activity-start') + resume_menu_item.connect('activate', self._resume_activate_cb) + palette.menu.append(resume_menu_item) + resume_menu_item.show() + + # FIXME: kludge + if self._activity.get_type() != "org.laptop.JournalActivity": + stop_menu_item = MenuItem(_('Stop'), 'activity-stop') + stop_menu_item.connect('activate', self._stop_activate_cb) + palette.menu.append(stop_menu_item) + stop_menu_item.show() + + def _launching_changed_cb(self, activity, pspec): + if not activity.props.launching: + self._stop_pulsing() + self._setup_palette() + + def __del__(self): + self._cleanup() + + def _cleanup(self): + if self._pulse_id: + gobject.source_remove(self._pulse_id) + self._pulse_id = 0 + + def _compute_icon_colors(self): + _LEVEL_MAX = 1.6 + _LEVEL_STEP = 0.16 + _LEVEL_MIN = 0.0 + icon_colors = {} + level = _LEVEL_MIN + for i in range(0, int(_LEVEL_MAX / _LEVEL_STEP)): + icon_colors[i] = self._get_icon_color_for_level(level) + level += _LEVEL_STEP + return icon_colors + + def _get_icon_color_for_level(self, level): + factor = math.sin(level) + h, s, v = colorsys.rgb_to_hsv(*html_to_rgb(self._orig_color.get_fill_color())) + new_fill = rgb_to_html(*colorsys.hsv_to_rgb(h, s * factor, v)) + h, s, v = colorsys.rgb_to_hsv(*html_to_rgb(self._orig_color.get_stroke_color())) + new_stroke = rgb_to_html(*colorsys.hsv_to_rgb(h, s * factor, v)) + return xocolor.XoColor("%s,%s" % (new_stroke, new_fill)) + + def _pulse_cb(self): + if self._direction == 1: + self._level += 1 + if self._level > self._level_max: + self._direction = 0 + self._level = self._level_max + elif self._direction == 0: + self._level -= 1 + if self._level <= 0: + self._direction = 1 + self._level = 0 + + self.props.xo_color = self._icon_colors[self._level] + self.emit_paint_needed(0, 0, -1, -1) + return True + + def _start_pulsing(self): + if self._pulse_id: + return + + self._pulse_id = gobject.timeout_add(self._INTERVAL, self._pulse_cb) + + def _stop_pulsing(self): + if not self._pulse_id: + return + + self._cleanup() + self._level = 100.0 + self.props.xo_color = self._orig_color + + def _resume_activate_cb(self, menuitem): + self.emit('resume') + + def _stop_activate_cb(self, menuitem): + self.emit('stop') + + def get_activity(self): + return self._activity + +class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = 'SugarActivitiesDonut' + def __init__(self, shell, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + + self._activities = [] + self._shell = shell + self._angles = [] + self._shell_mappings = proc_smaps.get_shared_mapping_names(os.getpid()) + + self._layout = _Layout() + self.set_layout(self._layout) + + self._model = shell.get_model().get_home() + self._model.connect('activity-added', self._activity_added_cb) + self._model.connect('activity-removed', self._activity_removed_cb) + self._model.connect('pending-activity-changed', self._activity_changed_cb) + + self.connect('button-release-event', self._button_release_event_cb) + + def _get_icon_from_activity(self, activity): + for icon in self._activities: + if icon.get_activity().equals(activity): + return icon + + def _activity_added_cb(self, model, activity): + self._add_activity(activity) + + def _activity_removed_cb(self, model, activity): + self._remove_activity(activity) + + def _activity_changed_cb(self, model, activity): + self.emit_paint_needed(0, 0, -1, -1) + + def _remove_activity(self, activity): + icon = self._get_icon_from_activity(activity) + if icon: + self.remove(icon) + icon._cleanup() + self._activities.remove(icon) + self._compute_angles() + + def _add_activity(self, activity): + icon = ActivityIcon(activity) + icon.connect('resume', self._activity_icon_resumed_cb) + icon.connect('stop', self._activity_icon_stop_cb) + self.append(icon, hippo.PACK_FIXED) + + self._activities.append(icon) + self._compute_angles() + + def _activity_icon_resumed_cb(self, icon): + activity = icon.get_activity() + activity_host = self._shell.get_activity(activity.get_activity_id()) + if activity_host: + activity_host.present() + else: + logging.error("Could not find ActivityHost for activity %s" % + activity.get_activity_id()) + + def _activity_icon_stop_cb(self, icon): + activity = icon.get_activity() + activity_host = self._shell.get_activity(activity.get_activity_id()) + if activity_host: + activity_host.close() + else: + logging.error("Could not find ActivityHost for activity %s" % + activity.get_activity_id()) + + def _get_activity(self, x, y): + # Compute the distance from the center. + [width, height] = self.get_allocation() + x -= width / 2 + y -= height / 2 + r = math.hypot(x, y) + + # Ignore the click if it's not inside the donut + if r < self._get_inner_radius() or r > self._get_radius(): + return None + + # Now figure out where in the donut the click was. + angle = math.atan2(-y, -x) + math.pi + + # Unfortunately, _get_angles() doesn't count from 0 to 2pi, it + # counts from roughly pi/2 to roughly 5pi/2. So we have to + # compare its return values against both angle and angle+2pi + high_angle = angle + 2 * math.pi + + for index, activity in enumerate(self._model): + [angle_start, angle_end] = self._get_angles(index) + if angle_start < angle and angle_end > angle: + return activity + elif angle_start < high_angle and angle_end > high_angle: + return activity + + return None + + def _button_release_event_cb(self, item, event): + activity = self._get_activity(event.x, event.y) + if activity is None: + return False + + activity_host = self._shell.get_activity(activity.get_activity_id()) + if activity_host: + activity_host.present() + return True + + def _set_fixed_arc_size(self): + """Set fixed arc size""" + + n = len(self._activities) + if n > _MAX_ACTIVITIES: + size = 1.0 / n + else: + size = 1.0 / _MAX_ACTIVITIES + + for act in self._activities: + act.size = size + + def _update_activity_sizes(self): + """Currently the size of an activity on the donut does not + represent it's memory usage. This is disabled because it was + either not working perfectly or a little confusing. See #3605""" + self._set_fixed_arc_size() + return + + # Get the memory mappings of each process that hosts an + # activity, and count how many activity instances each + # activity process hosts, and how many processes are mapping + # each shared library, etc + process_mappings = {} + num_activities = {} + num_mappings = {} + unknown_size_activities = 0 + for activity in self._model: + pid = activity.get_pid() + if not pid: + # Still starting up, hasn't opened a window yet + unknown_size_activities += 1 + continue + + if num_activities.has_key(pid): + num_activities[pid] += 1 + continue + + try: + mappings = proc_smaps.get_mappings(pid, self._shell_mappings) + for mapping in mappings: + if mapping.shared > 0: + if num_mappings.has_key(mapping.name): + num_mappings[mapping.name] += 1 + else: + num_mappings[mapping.name] = 1 + process_mappings[pid] = mappings + num_activities[pid] = 1 + except Exception, e: + logging.warn('ActivitiesDonut: could not read /proc/%s/smaps: %r' + % (pid, e)) + + # Compute total memory used per process + process_size = {} + total_activity_size = 0 + for activity in self._model: + pid = activity.get_pid() + if not process_mappings.has_key(pid): + continue + + mappings = process_mappings[pid] + size = 0 + for mapping in mappings: + size += mapping.private + if mapping.shared > 0: + num = num_mappings[mapping.name] + size += mapping.shared / num + process_size[pid] = size + total_activity_size += size / num_activities[pid] + + # Now, see how much free memory is left. + free_memory = 0 + try: + meminfo = open('/proc/meminfo') + for line in meminfo.readlines(): + if line.startswith('MemFree:') or line.startswith('SwapFree:'): + free_memory += int(line[9:-3]) + meminfo.close() + except IOError: + logging.warn('ActivitiesDonut: could not read /proc/meminfo') + except (IndexError, ValueError): + logging.warn('ActivitiesDonut: /proc/meminfo was not in ' + + 'expected format') + + total_memory = float(total_activity_size + free_memory) + + # Each activity has an ideal size of: + # process_size[pid] / num_activities[pid] / total_memory + # (And the free memory wedge is ideally free_memory / + # total_memory) However, no activity wedge is allowed to be + # smaller than _MIN_WEDGE_SIZE. This means the small + # activities will use up extra space, which would make the + # ring overflow. We fix that by reducing the large activities + # and the free space proportionately. If there are activities + # of unknown size, they are simply carved out of the free + # space. + + free_percent = free_memory / total_memory + activity_sizes = [] + overflow = 0.0 + reducible = free_percent + for icon in self._activities: + pid = icon.get_activity().get_pid() + if process_size.has_key(pid): + icon.size = (process_size[pid] / num_activities[pid] / + total_memory) + if icon.size < _MIN_WEDGE_SIZE: + overflow += _MIN_WEDGE_SIZE - icon.size + icon.size = _MIN_WEDGE_SIZE + else: + reducible += icon.size - _MIN_WEDGE_SIZE + else: + icon.size = _MIN_WEDGE_SIZE + + if reducible > 0.0: + reduction = overflow / reducible + if unknown_size_activities > 0: + unknown_percent = _MIN_WEDGE_SIZE * unknown_size_activities + if (free_percent * (1 - reduction) < unknown_percent): + # The free wedge won't be large enough to fit the + # unknown-size activities. So adjust things + overflow += unknown_percent - free_percent + reducible -= free_percent + reduction = overflow / reducible + + if reduction > 0.0: + for icon in self._activities: + if icon.size > _MIN_WEDGE_SIZE: + icon.size -= (icon.size - _MIN_WEDGE_SIZE) * reduction + + def _compute_angles(self): + self._angles = [] + if len(self._activities) == 0: + return + + # Normally we don't _update_activity_sizes() when launching a + # new activity; but if the new wedge would overflow the ring + # then we have no choice. + total = reduce(lambda s1,s2: s1 + s2, + [icon.size for icon in self._activities]) + if total > 1.0: + self._update_activity_sizes() + + # The first wedge (Journal) should be centered at 6 o'clock + size = self._activities[0].size or _MIN_WEDGE_SIZE + angle = (math.pi - size * 2 * math.pi) / 2 + self._angles.append(angle) + + for icon in self._activities: + size = icon.size or _MIN_WEDGE_SIZE + self._angles.append(self._angles[-1] + size * 2 * math.pi) + + def redraw(self): + self._update_activity_sizes() + self._compute_angles() + self.emit_request_changed() + + def _get_angles(self, index): + return [self._angles[index], + self._angles[(index + 1) % len(self._angles)]] + + def _get_radius(self): + [width, height] = self.get_allocation() + return min(width, height) / 2 + + def _get_inner_radius(self): + return self._get_radius() * 0.5 + + def do_paint_below_children(self, cr, damaged_box): + [width, height] = self.get_allocation() + + cr.translate(width / 2, height / 2) + + radius = self._get_radius() + + # Outer Ring + cr.set_source_rgb(0xf1 / 255.0, 0xf1 / 255.0, 0xf1 / 255.0) + cr.arc(0, 0, radius, 0, 2 * math.pi) + cr.fill() + + # Selected Wedge + current_activity = self._model.get_pending_activity() + if current_activity is not None: + selected_index = self._model.index(current_activity) + [angle_start, angle_end] = self._get_angles(selected_index) + + cr.new_path() + cr.move_to(0, 0) + cr.line_to(radius * math.cos(angle_start), + radius * math.sin(angle_start)) + cr.arc(0, 0, radius, angle_start, angle_end) + cr.line_to(0, 0) + cr.set_source_rgb(1, 1, 1) + cr.fill() + + # Edges + if len(self._model): + n_edges = len(self._model) + 1 + else: + n_edges = 0 + + for i in range(0, n_edges): + cr.new_path() + cr.move_to(0, 0) + [angle, unused_angle] = self._get_angles(i) + cr.line_to(radius * math.cos(angle), + radius * math.sin(angle)) + + cr.set_source_rgb(0xe2 / 255.0, 0xe2 / 255.0, 0xe2 / 255.0) + cr.set_line_width(4) + cr.stroke_preserve() + + # Inner Ring + cr.new_path() + cr.arc(0, 0, self._get_inner_radius(), 0, 2 * math.pi) + cr.set_source_rgb(0xe2 / 255.0, 0xe2 / 255.0, 0xe2 / 255.0) + cr.fill() + + def do_allocate(self, width, height, origin_changed): + hippo.CanvasBox.do_allocate(self, width, height, origin_changed) + + radius = (self._get_inner_radius() + self._get_radius()) / 2 + + for i, icon in enumerate(self._activities): + [angle_start, angle_end] = self._get_angles(i) + angle = angle_start + (angle_end - angle_start) / 2 + + [icon_width, icon_height] = icon.get_allocation() + + x = int(radius * math.cos(angle)) - icon_width / 2 + y = int(radius * math.sin(angle)) - icon_height / 2 + + self.set_position(icon, x + width / 2, y + height / 2) + +class _Layout(gobject.GObject,hippo.CanvasLayout): + __gtype_name__ = 'SugarDonutLayout' + def __init__(self): + gobject.GObject.__init__(self) + + def do_set_box(self, box): + self._box = box + + def do_get_height_request(self, for_width): + return _DONUT_SIZE, _DONUT_SIZE + + def do_get_width_request(self): + return _DONUT_SIZE, _DONUT_SIZE + + def do_allocate(self, x, y, width, height, + req_width, req_height, origin_changed): + for child in self._box.get_layout_children(): + min_width, child_width = child.get_width_request() + min_height, child_height = child.get_height_request(child_width) + + [angle_start, angle_end] = self._box._get_angles(i) + angle = angle_start + (angle_end - angle_start) / 2 + + x = int(radius * math.cos(angle)) - icon_width / 2 + y = int(radius * math.sin(angle)) - icon_height / 2 + + child.allocate(x + (width - child_width) / 2, + y + (height - child_height) / 2, + icon_width, icon_height, origin_changed) diff --git a/src/view/home/proc_smaps.py b/src/view/home/proc_smaps.py new file mode 100755 index 0000000..6e1680f --- /dev/null +++ b/src/view/home/proc_smaps.py @@ -0,0 +1,107 @@ +# Copyright (C) 2007 Red Hat, Inc. +# +# 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 + +# /proc/PID/maps consists of a number of lines like this: +# 00400000-004b1000 r-xp 00000000 fd:00 5767206 /bin/bash +# 006b1000-006bb000 rw-p 000b1000 fd:00 5767206 /bin/bash +# 006bb000-006c0000 rw-p 006bb000 00:00 0 +# ... +# The fields are: address, permissions, offset, device, inode, and +# (for non-anonymous mappings) pathname. +# +# /proc/PID/smaps gives additional information for each mapping: +# 00400000-004b1000 r-xp 00000000 fd:00 5767206 /bin/bash +# Size: 708 kB +# Rss: 476 kB +# Shared_Clean: 468 kB +# Shared_Dirty: 0 kB +# Private_Clean: 8 kB +# Private_Dirty: 0 kB +# Referenced: 0 kb +# +# The "Referenced" line only appears in kernel 2.6.22 and later. + +def get_shared_mapping_names(pid): + """Returns a set of the files for which PID has a shared mapping""" + + mappings = set() + infile = open("/proc/%s/maps" % pid, "r") + for line in infile: + # sharable mappings are non-anonymous and either read-only + # (permissions "r-..") or writable but explicitly marked + # shared ("rw.s") + fields = line.split() + if len(fields) < 6 or not fields[5].startswith('/'): + continue + if fields[1][0] != 'r' or (fields[1][1] == 'w' and fields[1][3] != 's'): + continue + mappings.add(fields[5]) + infile.close() + return mappings + +_smaps_lines_per_entry = None + +def get_mappings(pid, ignored_shared_mappings): + """Returns a list of (name, private, shared) tuples describing the + memory mappings of PID. Shared mappings named in + ignored_shared_mappings are ignored + """ + + global _smaps_lines_per_entry + if _smaps_lines_per_entry is None: + if os.path.isfile('/proc/%s/clear_refs' % os.getpid()): + _smaps_lines_per_entry = 8 + else: + _smaps_lines_per_entry = 7 + + mappings = [] + + smapfile = "/proc/%s/smaps" % pid + infile = open(smapfile, "r") + input = infile.read() + infile.close() + lines = input.splitlines() + + for line_idx in range(0, len(lines), _smaps_lines_per_entry): + name_idx = lines[line_idx].find('/') + if name_idx == -1: + name = None + else: + name = lines[line_idx][name_idx:] + + private_clean = int(lines[line_idx + 5][14:-3]) + private_dirty = int(lines[line_idx + 6][14:-3]) + if name in ignored_shared_mappings: + shared_clean = 0 + shared_dirty = 0 + else: + shared_clean = int(lines[line_idx + 3][14:-3]) + shared_dirty = int(lines[line_idx + 4][14:-3]) + + mapping = Mapping(name, private_clean + private_dirty, + shared_clean + shared_dirty) + mappings.append (mapping) + + return mappings + +class Mapping: + def __init__ (self, name, private, shared): + self.name = name + self.private = private + self.shared = shared diff --git a/src/view/home/snowflakelayout.py b/src/view/home/snowflakelayout.py new file mode 100644 index 0000000..1eb58cf --- /dev/null +++ b/src/view/home/snowflakelayout.py @@ -0,0 +1,108 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import math + +import gobject +import hippo + +from sugar.graphics import style + +_BASE_DISTANCE = style.zoom(15) +_CHILDREN_FACTOR = style.zoom(3) + +class SnowflakeLayout(gobject.GObject,hippo.CanvasLayout): + __gtype_name__ = 'SugarSnowflakeLayout' + def __init__(self): + gobject.GObject.__init__(self) + self._nflakes = 0 + + def add(self, child, center=False): + if not center: + self._nflakes += 1 + + self._box.append(child) + + box_child = self._box.find_box_child(child) + box_child.is_center = center + + def remove(self, child): + box_child = self._box.find_box_child(child) + if not box_child.is_center: + self._nflakes -= 1 + + self._box.remove(child) + + def do_set_box(self, box): + self._box = box + + def do_get_height_request(self, for_width): + size = self._calculate_size() + return (size, size) + + def do_get_width_request(self): + size = self._calculate_size() + return (size, size) + + def do_allocate(self, x, y, width, height, + req_width, req_height, origin_changed): + r = self._get_radius() + index = 0 + + for child in self._box.get_layout_children(): + cx = x + width / 2 + cy = x + height / 2 + + min_width, child_width = child.get_width_request() + min_height, child_height = child.get_height_request(child_width) + + if child.is_center: + child.allocate(x + (width - child_width) / 2, + y + (height - child_height) / 2, + child_width, child_height, origin_changed) + else: + angle = 2 * math.pi * index / self._nflakes + + dx = math.cos(angle) * r + dy = math.sin(angle) * r + + child_x = int(x + (width - child_width) / 2 + dx) + child_y = int(y + (height - child_height) / 2 + dy) + + child.allocate(child_x, child_y, child_width, + child_height, origin_changed) + + index += 1 + + def _get_radius(self): + radius = int(_BASE_DISTANCE + _CHILDREN_FACTOR * self._nflakes) + for child in self._box.get_layout_children(): + if child.is_center: + [min_w, child_w] = child.get_width_request() + [min_h, child_h] = child.get_height_request(child_w) + radius += max(child_w, child_h) / 2 + + return radius + + def _calculate_size(self): + thickness = 0 + for child in self._box.get_layout_children(): + [min_width, child_width] = child.get_width_request() + [min_height, child_height] = child.get_height_request(child_width) + thickness = max(thickness, max(child_width, child_height)) + + return self._get_radius() * 2 + thickness diff --git a/src/view/home/spreadlayout.py b/src/view/home/spreadlayout.py new file mode 100644 index 0000000..3463169 --- /dev/null +++ b/src/view/home/spreadlayout.py @@ -0,0 +1,246 @@ +# Copyright (C) 2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +from numpy import array +from random import random + +import hippo +import gobject +import gtk + +from sugar.graphics import style + +_PLACE_TRIALS = 20 +_MAX_WEIGHT = 255 +_CELL_SIZE = 4 + +class _Grid(gobject.GObject): + __gsignals__ = { + 'child-changed' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + def __init__(self, width, height): + gobject.GObject.__init__(self) + + self.width = width + self.height = height + self._children = [] + self._collisions = [] + self._collisions_sid = 0 + + self._array = array([0], dtype='b') + self._array.resize(width * height) + + def add(self, child, width, height): + trials = _PLACE_TRIALS + weight = _MAX_WEIGHT + while trials > 0 and weight: + x = int(random() * (self.width - width)) + y = int(random() * (self.height - height)) + + rect = gtk.gdk.Rectangle(x, y, width, height) + new_weight = self._compute_weight(rect) + if weight > new_weight: + weight = new_weight + + trials -= 1 + + child.grid_rect = rect + child.locked = False + + self._add_child(child) + + if weight > 0: + self._detect_collisions(child) + + def remove(self, child): + self._children.remove(child) + self._remove_weight(child.grid_rect) + child.grid_rect = None + + def _add_child(self, child): + self._children.append(child) + self.add_weight(child.grid_rect) + + def _move_child(self, child, new_rect): + self._remove_weight(child.grid_rect) + self.add_weight(new_rect) + + child.grid_rect = new_rect + + self.emit('child-changed', child) + + def _shift_child(self, child): + rect = child.grid_rect + weight = self._compute_weight(rect) + new_rects = [] + + if (rect.x + rect.width < self.width - 1): + new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y, + rect.width, rect.height)) + + if (rect.x - 1 > 0): + new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y, + rect.width, rect.height)) + + if (rect.y + rect.height < self.height - 1): + new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y + 1, + rect.width, rect.height)) + + if (rect.y - 1 > 0): + new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y - 1, + rect.width, rect.height)) + + best_rect = None + for new_rect in new_rects: + new_weight = self._compute_weight(new_rect) + if new_weight < weight: + best_rect = new_rect + weight = new_weight + + if best_rect: + self._move_child(child, best_rect) + + return weight + + + def _solve_collisions(self): + for collision in self._collisions[:]: + weight = self._shift_child(collision) + if not weight: + self._collisions.remove(collision) + + return (len(self._collisions) > 0) + + def _detect_collisions(self, child): + collision_found = False + for c in self._children: + intersection = child.grid_rect.intersect(c.grid_rect) + if c != child and intersection.width > 0: + if c not in self._collisions: + collision_found = True + self._collisions.append(c) + + if collision_found: + if child not in self._collisions: + self._collisions.append(child) + +# if len(self._collisions) and not self._collisions_sid: +# self._collisions_sid = gobject.idle_add(self._solve_collisions) + + def add_weight(self, rect): + for i in range(rect.x, rect.x + rect.width): + for j in range(rect.y, rect.y + rect.height): + self[j, i] += 1 + + def _remove_weight(self, rect): + for i in range(rect.x, rect.x + rect.width): + for j in range(rect.y, rect.y + rect.height): + self[j, i] -= 1 + + def _compute_weight(self, rect): + weight = 0 + + for i in range(rect.x, rect.x + rect.width): + for j in range(rect.y, rect.y + rect.height): + weight += self[j, i] + + return weight + + def __getitem__(self, (row, col)): + return self._array[col + row * self.width] + + def __setitem__(self, (row, col), value): + self._array[col + row * self.width] = value + + +class SpreadLayout(gobject.GObject, hippo.CanvasLayout): + __gtype_name__ = 'SugarSpreadLayout' + def __init__(self): + gobject.GObject.__init__(self) + + min_width, width = self.do_get_width_request() + min_height, height = self.do_get_height_request(width) + + self._grid = _Grid(width / _CELL_SIZE, height / _CELL_SIZE) + self._grid.connect('child-changed', self._grid_child_changed_cb) + + def add_center(self, child, vertical_offset=0): + self._box.append(child) + + width, height = self._get_child_grid_size(child) + rect = gtk.gdk.Rectangle(int((self._grid.width - width) / 2), + int((self._grid.height - height) / 2), + width + 1, height + 1) + self._grid.add_weight(rect) + + box_child = self._box.find_box_child(child) + box_child.grid_rect = None + box_child.vertical_offset = vertical_offset + + def add(self, child): + self._box.append(child) + + width, height = self._get_child_grid_size(child) + box_child = self._box.find_box_child(child) + self._grid.add(box_child, width, height) + + def remove(self, child): + box_child = self._box.find_box_child(child) + self._grid.remove(box_child) + + self._box.remove(child) + + def do_set_box(self, box): + self._box = box + + def do_get_height_request(self, for_width): + return 0, gtk.gdk.screen_height() - style.GRID_CELL_SIZE + + def do_get_width_request(self): + return 0, gtk.gdk.screen_width() + + def do_allocate(self, x, y, width, height, + req_width, req_height, origin_changed): + for child in self._box.get_layout_children(): + # We need to always get requests to not confuse hippo + min_w, child_width = child.get_width_request() + min_h, child_height = child.get_height_request(child_width) + + rect = child.grid_rect + if child.grid_rect: + child.allocate(rect.x * _CELL_SIZE, + rect.y * _CELL_SIZE, + rect.width * _CELL_SIZE, + rect.height * _CELL_SIZE, + origin_changed) + else: + vertical_offset = child.vertical_offset + child_x = x + (width - child_width) / 2 + child_y = y + (height - child_height + vertical_offset) / 2 + child.allocate(child_x, child_y, child_width, child_height, + origin_changed) + + def _get_child_grid_size(self, child): + min_width, width = child.get_width_request() + min_height, height = child.get_height_request(width) + + return int(width / _CELL_SIZE), int(height / _CELL_SIZE) + + def _grid_child_changed_cb(self, grid, box_child): + box_child.item.emit_request_changed() diff --git a/src/view/home/transitionbox.py b/src/view/home/transitionbox.py new file mode 100644 index 0000000..f1ba4fb --- /dev/null +++ b/src/view/home/transitionbox.py @@ -0,0 +1,93 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 hippo +import gobject + +from sugar.graphics import style +from sugar.graphics import animator + +from view.home.MyIcon import MyIcon +from view.home.spreadlayout import SpreadLayout + +class _Animation(animator.Animation): + def __init__(self, icon, start_size, end_size): + animator.Animation.__init__(self, 0.0, 1.0) + + self._icon = icon + self.start_size = start_size + self.end_size = end_size + + def next_frame(self, current): + d = (self.end_size - self.start_size) * current + self._icon.props.size = self.start_size + d + +class _Layout(gobject.GObject,hippo.CanvasLayout): + __gtype_name__ = 'SugarTransitionBoxLayout' + def __init__(self): + gobject.GObject.__init__(self) + + def do_set_box(self, box): + self._box = box + + def do_get_height_request(self, for_width): + return 0, 0 + + def do_get_width_request(self): + return 0, 0 + + def do_allocate(self, x, y, width, height, + req_width, req_height, origin_changed): + for child in self._box.get_layout_children(): + min_width, child_width = child.get_width_request() + min_height, child_height = child.get_height_request(child_width) + + child.allocate(x + (width - child_width) / 2, + y + (height - child_height) / 2, + child_width, child_height, origin_changed) + +class TransitionBox(hippo.CanvasBox): + __gtype_name__ = 'SugarTransitionBox' + + __gsignals__ = { + 'completed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self): + hippo.CanvasBox.__init__(self, background_color=0xe2e2e2ff) + + self._size = style.XLARGE_ICON_SIZE + + self._layout = _Layout() + self.set_layout(self._layout) + + self._my_icon = MyIcon(self._size) + self.append(self._my_icon) + + self._animator = animator.Animator(0.3) + self._animator.connect('completed', self._animation_completed_cb) + + def _animation_completed_cb(self, anim): + self.emit('completed') + + def set_size(self, size): + self._animator.remove_all() + self._animator.add(_Animation(self._my_icon, self._size, size)) + self._animator.start() + + self._size = size + diff --git a/src/view/keyhandler.py b/src/view/keyhandler.py new file mode 100644 index 0000000..b07f46c --- /dev/null +++ b/src/view/keyhandler.py @@ -0,0 +1,237 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 signal +import logging +import subprocess + +import dbus +import gtk + +from hardware import hardwaremanager +from model.shellmodel import ShellModel +from sugar._sugarext import KeyGrabber + +_BRIGHTNESS_STEP = 2 +_VOLUME_STEP = 10 +_BRIGHTNESS_MAX = 15 +_VOLUME_MAX = 100 + +_actions_table = { + 'F1' : 'zoom_mesh', + 'F2' : 'zoom_friends', + 'F3' : 'zoom_home', + 'F4' : 'zoom_activity', + 'F9' : 'brightness_down', + 'F10' : 'brightness_up', + 'F9' : 'brightness_min', + 'F10' : 'brightness_max', + 'F11' : 'volume_down', + 'F12' : 'volume_up', + 'F11' : 'volume_min', + 'F12' : 'volume_max', + '1' : 'screenshot', + 'f' : 'frame', + '0x93' : 'frame', + 'o' : 'overlay', + '0xE0' : 'overlay', + '0xEB' : 'rotate', + 'r' : 'rotate', + 'q' : 'quit_emulator', + 'Tab' : 'next_window', + 'n' : 'next_window', + 'Tab' : 'previous_window', + 'p' : 'previous_window', + 'Escape' : 'close_window', + 'q' : 'close_window', + '0xDC' : 'open_search', + 'o' : 'open_search', + 's' : 'say_text' +} + +J_DBUS_SERVICE = 'org.laptop.Journal' +J_DBUS_PATH = '/org/laptop/Journal' +J_DBUS_INTERFACE = 'org.laptop.Journal' + +SPEECH_DBUS_SERVICE = 'org.laptop.Speech' +SPEECH_DBUS_PATH = '/org/laptop/Speech' +SPEECH_DBUS_INTERFACE = 'org.laptop.Speech' + +class KeyHandler(object): + def __init__(self, shell): + self._shell = shell + self._screen_rotation = 0 + self._key_pressed = None + self._keycode_pressed = 0 + self._keystate_pressed = 0 + self._speech_proxy = None + + self._key_grabber = KeyGrabber() + self._key_grabber.connect('key-pressed', + self._key_pressed_cb) + + for key in _actions_table.keys(): + self._key_grabber.grab(key) + + def _change_volume(self, step=None, value=None): + hw_manager = hardwaremanager.get_manager() + + if step is not None: + volume = hw_manager.get_volume() + step + elif value is not None: + volume = value + + volume = min(max(0, volume), _VOLUME_MAX) + + hw_manager.set_volume(volume) + hw_manager.set_mute(volume == 0) + + def _change_brightness(self, step=None, value=None): + hw_manager = hardwaremanager.get_manager() + + if step is not None: + level = hw_manager.get_display_brightness() + step + elif value is not None: + level = value + + level = min(max(0, level), _BRIGHTNESS_MAX) + + hw_manager.set_display_brightness(level) + if level == 0: + hw_manager.set_display_mode(hardwaremanager.B_AND_W_MODE) + else: + hw_manager.set_display_mode(hardwaremanager.COLOR_MODE) + + def _get_speech_proxy(self): + if self._speech_proxy is None: + bus = dbus.SessionBus() + speech_obj = bus.get_object(SPEECH_DBUS_SERVICE, SPEECH_DBUS_PATH) + self._speech_proxy = dbus.Interface(speech_obj, SPEECH_DBUS_INTERFACE) + return self._speech_proxy + + def _on_speech_err(self, ex): + logging.error("An error occurred with the ESpeak service: %r" % (ex, )) + + def _primary_selection_cb(self, clipboard, text, user_data): + logging.debug('KeyHandler._primary_selection_cb: %r' % text) + if text: + self._get_speech_proxy().SayText(text, reply_handler=lambda: None, \ + error_handler=self._on_speech_err) + + def handle_say_text(self): + clipboard = gtk.clipboard_get(selection="PRIMARY") + clipboard.request_text(self._primary_selection_cb) + + def handle_previous_window(self): + self._shell.activate_previous_activity() + + def handle_next_window(self): + self._shell.activate_next_activity() + + def handle_close_window(self): + self._shell.close_current_activity() + + def handle_zoom_mesh(self): + self._shell.set_zoom_level(ShellModel.ZOOM_MESH) + + def handle_zoom_friends(self): + self._shell.set_zoom_level(ShellModel.ZOOM_FRIENDS) + + def handle_zoom_home(self): + self._shell.set_zoom_level(ShellModel.ZOOM_HOME) + + def handle_zoom_activity(self): + self._shell.set_zoom_level(ShellModel.ZOOM_ACTIVITY) + + def handle_brightness_max(self): + self._change_brightness(value=_BRIGHTNESS_MAX) + + def handle_brightness_min(self): + self._change_brightness(value=0) + + def handle_volume_max(self): + self._change_volume(value=_VOLUME_MAX) + + def handle_volume_min(self): + self._change_volume(value=0) + + def handle_brightness_up(self): + self._change_brightness(step=_BRIGHTNESS_STEP) + + def handle_brightness_down(self): + self._change_brightness(step=-_BRIGHTNESS_STEP) + + def handle_volume_up(self): + self._change_volume(step=_VOLUME_STEP) + + def handle_volume_down(self): + self._change_volume(step=-_VOLUME_STEP) + + def handle_screenshot(self): + self._shell.take_screenshot() + + def handle_frame(self): + self._shell.get_frame().notify_key_press() + + def handle_overlay(self): + self._shell.toggle_chat_visibility() + + def handle_rotate(self): + states = [ 'normal', 'left', 'inverted', 'right'] + + self._screen_rotation += 1 + if self._screen_rotation == len(states): + self._screen_rotation = 0 + + subprocess.Popen(['xrandr', '-o', states[self._screen_rotation]]) + + def handle_quit_emulator(self): + if os.environ.has_key('SUGAR_EMULATOR_PID'): + pid = int(os.environ['SUGAR_EMULATOR_PID']) + os.kill(pid, signal.SIGTERM) + + def focus_journal_search(self): + bus = dbus.SessionBus() + obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) + journal = dbus.Interface(obj, J_DBUS_INTERFACE) + journal.FocusSearch({}) + + def handle_open_search(self): + self.focus_journal_search() + + def _key_pressed_cb(self, grabber, keycode, state): + key = grabber.get_key(keycode, state) + logging.debug('_key_pressed_cb: %i %i %s' % (keycode, state, key)) + if key: + self._key_pressed = key + self._keycode_pressed = keycode + self._keystate_pressed = state + + """ + status = gtk.gdk.keyboard_grab(gtk.gdk.get_default_root_window(), + owner_events=False, time=0L) + if status != gtk.gdk.GRAB_SUCCESS: + logging.error("KeyHandler._key_pressed_cb(): keyboard grab failed: " + status) + """ + + action = _actions_table[key] + method = getattr(self, 'handle_' + action) + method() + + return True + + return False diff --git a/src/view/pulsingicon.py b/src/view/pulsingicon.py new file mode 100644 index 0000000..9e7b3d9 --- /dev/null +++ b/src/view/pulsingicon.py @@ -0,0 +1,90 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 gobject + +from sugar.graphics.icon import CanvasIcon + +class PulsingIcon(CanvasIcon): + __gproperties__ = { + 'paused' : (bool, None, None, False, + gobject.PARAM_READWRITE), + 'colors' : (object, None, None, + gobject.PARAM_READWRITE), + 'pulse-time' : (float, None, None, + 0.0, 500.0, 0.0, + gobject.PARAM_READWRITE), + } + + def __init__(self, **kwargs): + self._paused = False + self._pulse_time = 0.0 + self._colors = None + self._pulse_sid = 0 + self._pos = 0 + + CanvasIcon.__init__(self, **kwargs) + + def do_set_property(self, pspec, value): + CanvasIcon.do_set_property(self, pspec, value) + + if pspec.name == 'pulse-time': + self._pulse_time = value + self._stop() + if not self._paused and self._pulse_time > 0.0: + self._start() + elif pspec.name == 'colors': + self._colors = value + self._pos = 0 + self._update_colors() + elif pspec.name == 'paused': + self._paused = value + if not self._paused and self._pulse_time > 0.0: + self._start() + else: + self._stop() + + def do_get_property(self, pspec): + CanvasIcon.do_get_property(self, pspec) + + if pspec.name == 'pulse-time': + return self._pulse_time + elif pspec.name == 'colors': + return self._colors + + def _update_colors(self): + self.props.stroke_color = self._colors[self._pos][0] + self.props.fill_color = self._colors[self._pos][1] + + def _pulse_timeout(self): + if self._colors: + self._update_colors() + + self._pos += 1 + if self._pos == len(self._colors): + self._pos = 0 + + return True + + def _start(self): + if self._pulse_sid == 0: + self._pulse_sid = gobject.timeout_add( + int(self._pulse_time * 1000), self._pulse_timeout) + + def _stop(self): + if self._pulse_sid: + gobject.source_remove(self._pulse_sid) + self._pulse_sid = 0 -- cgit v0.9.1