diff options
author | Simon McVittie <simon.mcvittie@collabora.co.uk> | 2007-08-11 12:16:13 (GMT) |
---|---|---|
committer | Simon McVittie <simon.mcvittie@collabora.co.uk> | 2007-08-11 12:16:13 (GMT) |
commit | 7368429ad6feadd9e81757433b68b63d9d27c04f (patch) | |
tree | 39090b5e1c19f2d6ba63d9b07477668efff9a7c9 | |
parent | df3069a9317fc804b62bd3624d9591fb2e09df0f (diff) | |
parent | dcef110223e312d44955ca4aa1e2f306b9cb9e12 (diff) |
Merge branch 'master' of git+ssh://dev.laptop.org/git/sugar
62 files changed, 933 insertions, 603 deletions
@@ -38,6 +38,7 @@ mkinstalldirs po/Makefile.in.in po/POTFILES po/*.gmo +po/.intltool-merge-cache sugar/__installed__.py tools/sugar-setup-activity threadframe @@ -53,4 +54,5 @@ browser/sugar-marshal.h bin/sugar shell/extensions/_extensions.c data/sugar.gtkrc +data/sugar.xml data/sugar-xo.gtkrc @@ -1,3 +1,4 @@ +* Draw an invoker that is connected with the palette for toolbuttons. (benzea) * Fix traceback when reading in saved WPA2 network configs (dcbw) * #2475 Retrieve correctly the file path for files in removable devices. (tomeu) * #2119 If config is missing start intro. (marco) diff --git a/configure.ac b/configure.ac index 2e7e339..4363ac9 100644 --- a/configure.ac +++ b/configure.ac @@ -21,8 +21,10 @@ PKG_CHECK_MODULES(SHELL, pygtk-2.0 gtk+-2.0) PKG_CHECK_MODULES(NATIVE_FACTORY, dbus-1) -PKG_CHECK_MODULES(LIB, gtk+-2.0) -PKG_CHECK_MODULES(LIB_BINDINGS, pygtk-2.0) +PKG_CHECK_MODULES(LIBUI, gtk+-2.0) +PKG_CHECK_MODULES(LIBUI_BINDINGS, pygtk-2.0) + +PKG_CHECK_MODULES(LIB_BINDINGS, pygobject-2.0) PYGTK_DEFSDIR=`$PKG_CONFIG --variable=defsdir pygtk-2.0` AC_SUBST(PYGTK_DEFSDIR) @@ -45,7 +47,7 @@ data/Makefile lib/Makefile lib/ui/Makefile services/Makefile -services/clipboard/Makefile +services/shell/Makefile shell/Makefile shell/intro/Makefile shell/hardware/Makefile @@ -64,7 +66,7 @@ services/console/Makefile services/console/interface/Makefile services/console/interface/xo/Makefile services/console/interface/memphis/plugins/clean_size/Makefile -services/console/interface/memphis/plugins/dirty_size/Makefile +services/console/interface/memphis/plugins/smaps/Makefile services/console/interface/memphis/plugins/Makefile services/console/interface/memphis/plugins/memphis_init/Makefile services/console/interface/memphis/plugins/cpu/Makefile diff --git a/data/Makefile.am b/data/Makefile.am index b2fd17a..4c613ab 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -14,5 +14,23 @@ GTKRC_FILES = \ sugar.gtkrc \ sugar-xo.gtkrc -EXTRA_DIST = $(sugar_DATA) em.py gtkrc.em + +mime_xml_in_files = sugar.xml.in +mime_xml_files = $(mime_xml_in_files:.xml.in=.xml) +@INTLTOOL_XML_RULE@ + +mimedir = $(datadir)/mime/packages +mime_DATA = $(mime_xml_files) + +install-data-hook: + if [ -z "$$DESTDIR" ]; then \ + update-mime-database "$(datadir)/mime"; \ + fi + +uninstall-hook: + if [ -z "$$DESTDIR" ]; then \ + update-mime-database "$(datadir)/mime"; \ + fi + +EXTRA_DIST = $(sugar_DATA) $(mime_xml_in_files) em.py gtkrc.em CLEANFILES = $(GTKRC_FILES) diff --git a/data/sugar.xml.in b/data/sugar.xml.in new file mode 100644 index 0000000..39f6026 --- /dev/null +++ b/data/sugar.xml.in @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info"> + <mime-type type="application/vnd.olpc-x-sugar"> + <_comment>Sugar activity bundle</_comment> + <glob pattern="*.xo"/> + </mime-type> +</mime-info>
\ No newline at end of file diff --git a/lib/ui/Makefile.am b/lib/ui/Makefile.am index 3640a08..bae36c6 100644 --- a/lib/ui/Makefile.am +++ b/lib/ui/Makefile.am @@ -1,10 +1,10 @@ libsugarui_la_CPPFLAGS = \ - $(LIB_CFLAGS) + $(LIBUI_CFLAGS) noinst_LTLIBRARIES = libsugarui.la libsugarui_la_LIBADD = \ - $(LIB_LIBS) + $(LIBUI_LIBS) libsugarui_la_SOURCES = \ $(BUILT_SOURCES) \ diff --git a/lib/ui/sugar-menu.c b/lib/ui/sugar-menu.c index 6ed482e..f19dc4b 100644 --- a/lib/ui/sugar-menu.c +++ b/lib/ui/sugar-menu.c @@ -28,57 +28,36 @@ static void sugar_menu_init (SugarMenu *menu); G_DEFINE_TYPE(SugarMenu, sugar_menu, GTK_TYPE_MENU) void -sugar_menu_popup(SugarMenu *menu, - int x, - int y) +sugar_menu_set_active(SugarMenu *menu, gboolean active) { - GtkWidget *window; - - window = GTK_MENU(menu)->toplevel; - g_return_if_fail(window != NULL); - - GTK_MENU_SHELL(menu)->active = TRUE; - - gtk_widget_show(GTK_WIDGET(menu)); - - gtk_window_move(GTK_WINDOW(window), x, y); - gtk_widget_show(window); + GTK_MENU_SHELL(menu)->active = active; } void -sugar_menu_popdown(SugarMenu *menu) +sugar_menu_embed(SugarMenu *menu, GtkContainer *parent) { - GtkWidget *window; - - window = GTK_MENU(menu)->toplevel; - g_return_if_fail(window != NULL); - - GTK_MENU_SHELL(menu)->active = FALSE; + menu->orig_toplevel = GTK_MENU(menu)->toplevel; - gtk_widget_hide(GTK_WIDGET(menu)); - gtk_widget_hide(window); + GTK_MENU(menu)->toplevel = gtk_widget_get_toplevel(GTK_WIDGET(parent)); + gtk_widget_reparent(GTK_WIDGET(menu), GTK_WIDGET(parent)); } -static void -sugar_menu_size_request (GtkWidget *widget, - GtkRequisition *requisition) +void +sugar_menu_unembed(SugarMenu *menu) { - SugarMenu *menu = SUGAR_MENU(widget); - - (* GTK_WIDGET_CLASS (sugar_menu_parent_class)->size_request) (widget, requisition); - - requisition->width = MAX(requisition->width, menu->min_width); + if (menu->orig_toplevel) { + GTK_MENU(menu)->toplevel = menu->orig_toplevel; + gtk_widget_reparent(GTK_WIDGET(menu), GTK_WIDGET(menu->orig_toplevel)); + } } static void sugar_menu_class_init(SugarMenuClass *menu_class) { - GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(menu_class); - - widget_class->size_request = sugar_menu_size_request; } static void sugar_menu_init(SugarMenu *menu) { + menu->orig_toplevel = NULL; } diff --git a/lib/ui/sugar-menu.h b/lib/ui/sugar-menu.h index 24e2865..8773a31 100644 --- a/lib/ui/sugar-menu.h +++ b/lib/ui/sugar-menu.h @@ -35,8 +35,9 @@ typedef struct _SugarMenuClass SugarMenuClass; #define SUGAR_MENU_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS((object), SUGAR_TYPE_MENU, SugarMenuClass)) struct _SugarMenu { - GtkMenu base_instance; + GtkMenu base_instance; + GtkWidget *orig_toplevel; int min_width; }; @@ -44,13 +45,11 @@ struct _SugarMenuClass { GtkMenuClass base_class; }; -GType sugar_menu_get_type (void); -void sugar_menu_popup (SugarMenu *menu, - int x, - int y); -void sugar_menu_set_min_width (SugarMenu *menu, - int min_width); -void sugar_menu_popdown (SugarMenu *menu); +GType sugar_menu_get_type (void); +void sugar_menu_set_active (SugarMenu *menu, + gboolean active); +void sugar_menu_embed (SugarMenu *menu, + GtkContainer *parent); G_END_DECLS diff --git a/services/Makefile.am b/services/Makefile.am index 7d8e351..e230030 100644 --- a/services/Makefile.am +++ b/services/Makefile.am @@ -1 +1 @@ -SUBDIRS = clipboard console +SUBDIRS = shell console diff --git a/services/console/interface/logviewer/logviewer.py b/services/console/interface/logviewer/logviewer.py index 3d90f09..8aaf347 100644 --- a/services/console/interface/logviewer/logviewer.py +++ b/services/console/interface/logviewer/logviewer.py @@ -28,29 +28,29 @@ import pango from sugar import env class MultiLogView(gtk.VBox): - def __init__(self, path): + def __init__(self, path, extra_files): self._active_log = None - self._iters = [] - + self._extra_files = extra_files + # Creating Main treeview with Actitivities list - tv_menu = gtk.TreeView() - tv_menu.connect('cursor-changed', self._load_log) - tv_menu.set_rules_hint(True) + self._tv_menu = gtk.TreeView() + self._tv_menu.connect('cursor-changed', self._load_log) + self._tv_menu.set_rules_hint(True) # Set width box_width = gtk.gdk.screen_width() * 80 / 100 - tv_menu.set_size_request(box_width*25/100, 0) + self._tv_menu.set_size_request(box_width*25/100, 0) - self.store_menu = gtk.TreeStore(str) - tv_menu.set_model(self.store_menu) + self._store_menu = gtk.TreeStore(str) + self._tv_menu.set_model(self._store_menu) - self._add_column(tv_menu, 'Sugar logs', 0) + self._add_column(self._tv_menu, 'Sugar logs', 0) self._logs_path = os.path.join(env.get_profile_path(), 'logs') self._activity = {} # Activities menu self.hbox = gtk.HBox(False, 3) - self.hbox.pack_start(tv_menu, True, True, 0) + self.hbox.pack_start(self._tv_menu, True, True, 0) # Activity log, set width self._view = LogView() @@ -59,52 +59,62 @@ class MultiLogView(gtk.VBox): self.hbox.pack_start(self._view, True, True, 0) self.hbox.show_all() - gobject.timeout_add(1000, self._update, tv_menu) + gobject.timeout_add(1000, self._update) # Load the log information in View (textview) def _load_log(self, treeview): treeselection = treeview.get_selection() - treestore, iter = treeselection.get_selected() - + # Get current selection - act_log = self.store_menu.get_value(iter, 0) - + act_log = self._store_menu.get_value(iter, 0) + # Set buffer and scroll down self._view.textview.set_buffer(self._activity[act_log]) self._view.textview.scroll_to_mark(self._activity[act_log].get_insert(), 0); self._active_log = act_log - - def _update(self, tv_menu): + + def _update(self): # Searching log files for logfile in os.listdir(self._logs_path): full_log_path = os.path.join(self._logs_path, logfile) - - if os.path.isdir(full_log_path): - continue - - if not self._activity.has_key(logfile): - self._add_activity(logfile) - model = LogBuffer(full_log_path) - self._activity[logfile] = model - - self._activity[logfile].update() - written = self._activity[logfile]._written - - # Load the first iter - if self._active_log == None: - self._active_log = logfile - iter = tv_menu.get_model().get_iter_root() - tv_menu.get_selection().select_iter(iter) - self._load_log(tv_menu) - - if written > 0 and self._active_log == logfile: - self._view.textview.scroll_to_mark(self._activity[logfile].get_insert(), 0); + self._add_log_file(full_log_path) + + for ext in self._extra_files: + self._add_log_file(ext) return True - + + def _get_filename_from_path(self, path): + return path.split('/')[-1] + + def _add_log_file(self, path): + if os.path.isdir(path): + return False + + logfile = self._get_filename_from_path(path) + + if not self._activity.has_key(logfile): + self._add_activity(logfile) + model = LogBuffer(path) + self._activity[logfile] = model + + self._activity[logfile].update() + written = self._activity[logfile]._written + + # Load the first iter + if self._active_log == None: + self._active_log = logfile + iter = self._tv_menu.get_model().get_iter_root() + self._tv_menu.get_selection().select_iter(iter) + self._load_log(self._tv_menu) + + if written > 0 and self._active_log == logfile: + self._view.textview.scroll_to_mark(self._activity[logfile].get_insert(), 0) + + def _add_activity(self, name): - self._insert_row(self.store_menu, None, name) + self._insert_row(self._store_menu, None, name) # Add a new column to the main treeview, (code from Memphis) def _add_column(self, treeview, column_name, index): @@ -171,9 +181,20 @@ class LogView(gtk.ScrolledWindow): self.textview.show() class Interface: - def __init__(self): path = None - viewer = MultiLogView(path) + xserver_logfile = self._get_xserver_logfile_path() + + # Aditional files to watch in logviewer + ext_files = [] + ext_files.append(xserver_logfile) + + viewer = MultiLogView(path, ext_files) self.widget = viewer.hbox + # Get the Xorg log file path, we have two ways to get the path: do a system + # call and exec a 'xset -q' or just read directly the file that we know + # always be the right one for a XO machine... + def _get_xserver_logfile_path(self): + path = "/var/log/Xorg.0.log" + return path diff --git a/services/console/interface/memphis/memphis.py b/services/console/interface/memphis/memphis.py index 0dd52fc..5b1ce40 100644 --- a/services/console/interface/memphis/memphis.py +++ b/services/console/interface/memphis/memphis.py @@ -130,15 +130,14 @@ class Data: treeview.set_model(self.store) def _start_memphis(self, button): - # Update information every 1.5 second button.hide() self.interface.button_stop.show() self._running_status = True - gobject.timeout_add(1500, self.load_data, self.treeview) + self._gid = gobject.timeout_add(1500, self.load_data, self.treeview) def _stop_memphis(self, button): - + gobject.source_remove(self._gid) self._running_status = False button.hide() self.interface.button_start.show() diff --git a/services/console/interface/memphis/plugins/Makefile.am b/services/console/interface/memphis/plugins/Makefile.am index a18eafe..d026419 100644 --- a/services/console/interface/memphis/plugins/Makefile.am +++ b/services/console/interface/memphis/plugins/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = clean_size cpu dirty_size memphis_init +SUBDIRS = clean_size cpu smaps memphis_init sugardir = $(pkgdatadir)/services/console/interface/memphis/plugins sugar_PYTHON = diff --git a/services/console/interface/memphis/plugins/dirty_size/__init__.py b/services/console/interface/memphis/plugins/dirty_size/__init__.py deleted file mode 100644 index f8e9e0a..0000000 --- a/services/console/interface/memphis/plugins/dirty_size/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ - -import info - - -INTERNALS = { - # Basic information - 'PLGNAME': "Dirty Size", - 'TABNAME': None, # No tabbed plugin - 'AUTHOR': "Eduardo Silva", - 'DESC': "Get dirty size memory usage", - - # Plugin API - 'Plg': None, # Plugin object - - 'top_data': [int], # Top data types needed by memphis core plugin - 'top_cols': ["PDRSS (kb)"] - } diff --git a/services/console/interface/memphis/plugins/memphis_init/info.py b/services/console/interface/memphis/plugins/memphis_init/info.py index 6e524c7..667645c 100644 --- a/services/console/interface/memphis/plugins/memphis_init/info.py +++ b/services/console/interface/memphis/plugins/memphis_init/info.py @@ -11,3 +11,4 @@ def plg_on_top_data_refresh(self, ppinfo): data = [ppinfo['pid'], ppinfo['name'], ppinfo['state_name']] return data + diff --git a/services/console/interface/memphis/plugins/dirty_size/Makefile.am b/services/console/interface/memphis/plugins/smaps/Makefile.am index 1f81dfb..1f81dfb 100644 --- a/services/console/interface/memphis/plugins/dirty_size/Makefile.am +++ b/services/console/interface/memphis/plugins/smaps/Makefile.am diff --git a/services/console/interface/memphis/plugins/dirty_size/README b/services/console/interface/memphis/plugins/smaps/README index ee4d1a5..ee4d1a5 100644 --- a/services/console/interface/memphis/plugins/dirty_size/README +++ b/services/console/interface/memphis/plugins/smaps/README diff --git a/services/console/interface/memphis/plugins/smaps/__init__.py b/services/console/interface/memphis/plugins/smaps/__init__.py new file mode 100644 index 0000000..5977d4b --- /dev/null +++ b/services/console/interface/memphis/plugins/smaps/__init__.py @@ -0,0 +1,17 @@ + +import info + + +INTERNALS = { + # Basic information + 'PLGNAME': "SMaps", + 'TABNAME': None, # No tabbed plugin + 'AUTHOR': "Eduardo Silva", + 'DESC': "Get dirty size and reference memory usage", + + # Plugin API + 'Plg': None, # Plugin object + + 'top_data': [int, int], # Top data types needed by memphis core plugin + 'top_cols': ["PDRSS (kb)", "Referenced (kb)"] + } diff --git a/services/console/interface/memphis/plugins/dirty_size/info.py b/services/console/interface/memphis/plugins/smaps/info.py index 54a2e7e..998a1a2 100644 --- a/services/console/interface/memphis/plugins/dirty_size/info.py +++ b/services/console/interface/memphis/plugins/smaps/info.py @@ -8,13 +8,12 @@ def plg_on_top_data_refresh(self, ppinfo): - - dirty_sizes = get_dirty(self, ppinfo['pid']) + smaps = get_data(self, ppinfo['pid']) - # memhis need an array - return [dirty_sizes['private']] + # memphis need an array + return [smaps['private_dirty'], smaps['referenced']] -def get_dirty(pself, pid): +def get_data(pself, pid): ProcAnalysis = pself.INTERNALS['Plg'].proc_analysis(pid) - return ProcAnalysis.DirtyRSS() + return ProcAnalysis.SMaps() diff --git a/services/console/interface/terminal/terminal.py b/services/console/interface/terminal/terminal.py index 5eebfb3..4aba858 100644 --- a/services/console/interface/terminal/terminal.py +++ b/services/console/interface/terminal/terminal.py @@ -32,7 +32,7 @@ class Terminal(gtk.HBox): self._vte = vte.Terminal() self._configure_vte() - self._vte.set_size(30, 5) + self._vte.set_size(100, 5) self._vte.set_size_request(200, 450) self._vte.show() self.pack_start(self._vte) diff --git a/services/console/lib/procmem/analysis.py b/services/console/lib/procmem/analysis.py index d2a247a..e9d7aec 100644 --- a/services/console/lib/procmem/analysis.py +++ b/services/console/lib/procmem/analysis.py @@ -7,20 +7,22 @@ class Analysis: def __init__(self, pid): self.pid = pid - def DirtyRSS(self): + def SMaps(self): smaps = proc_smaps.ProcSmaps(self.pid) - dirty = [] + private_dirty = 0 + shared_dirty = 0 + referenced = 0 - private = 0 - shared = 0 - for map in smaps.mappings: - private += map.private_dirty - shared += map.shared_dirty + private_dirty += map.private_dirty + shared_dirty += map.shared_dirty + referenced += map.referenced - dirty = {"private": int(private), "shared": int(shared)} + smaps = {"private_dirty": int(private_dirty), \ + "shared_dirty": int(shared_dirty),\ + "referenced": int(referenced)} - return dirty + return smaps def ApproxRealMemoryUsage(self): maps = proc_smaps.ProcMaps(self.pid) diff --git a/services/console/lib/procmem/proc.py b/services/console/lib/procmem/proc.py index adc2f6b..d50242b 100644 --- a/services/console/lib/procmem/proc.py +++ b/services/console/lib/procmem/proc.py @@ -1,4 +1,6 @@ -import sys, os +import os +import re +import sys import string class ProcInfo: @@ -36,10 +38,12 @@ class ProcInfo: return None # Parsing data , check 'man 5 proc' for details - data = infile.read().split() - + stat_data = infile.read() infile.close() - + + process_name = self._get_process_name(stat_data) + data = self._get_safe_split(stat_data) + state_dic = { 'R': 'Running', 'S': 'Sleeping', @@ -48,27 +52,34 @@ class ProcInfo: 'T': 'Traced/Stopped', 'W': 'Paging' } - + # user and group owners pidstat = os.stat(pidfile) - info = { - 'pid': int(data[0]), # Process ID - 'name': data[1].strip('()'), # Process name - 'state': data[2], # Process State, ex: R|S|D|Z|T|W - 'state_name': state_dic[data[2]], # Process State name, ex: Running, sleeping, Zombie, etc - 'ppid': int(data[3]), # Parent process ID - 'utime': int(data[13]), # Used jiffies in user mode - 'stime': int(data[14]), # Used jiffies in kernel mode - 'start_time': int(data[21]), # Process time from system boot (jiffies) - 'vsize': int(data[22]), # Virtual memory size used (bytes) - 'rss': int(data[23])*4, # Resident Set Size (bytes) + 'pid': int(data[0]), # Process ID + 'name': process_name, + 'state': data[2], # Process State, ex: R|S|D|Z|T|W + 'state_name': state_dic[data[2]], # Process State name, ex: Running, sleeping, Zombie, etc + 'ppid': int(data[3]), # Parent process ID + 'utime': int(data[13]), # Used jiffies in user mode + 'stime': int(data[14]), # Used jiffies in kernel mode + 'start_time': int(data[21]), # Process time from system boot (jiffies) + 'vsize': int(data[22]), # Virtual memory size used (bytes) + 'rss': int(data[23])*4, # Resident Set Size (bytes) 'user_id': pidstat.st_uid, # process owner 'group_id': pidstat.st_gid # owner group } - + return info - + + # Return the process name + def _get_process_name(self, data): + m = re.search("\(.*\)", data) + return m.string[m.start()+1:m.end()-1] + + def _get_safe_split(self, data): + new_data = re.sub("\(.*\)", '()', data) + return new_data.split() # Returns the CPU usage expressed in Jiffies def get_CPU_usage(self, cpu_hz, used_jiffies, start_time): diff --git a/services/console/lib/procmem/proc_smaps.py b/services/console/lib/procmem/proc_smaps.py index ce93cb2..422866c 100644 --- a/services/console/lib/procmem/proc_smaps.py +++ b/services/console/lib/procmem/proc_smaps.py @@ -36,7 +36,8 @@ class ProcSmaps: # Shared_Dirty: 0 kB # Private_Clean: 8 kB # Private_Dirty: 0 kB - + # Referenced: 4 kb -> Introduced in kernel 2.6.22 + while num_lines > 0: fields = lines[line_idx].split (" ", 5) if len (fields) == 6: @@ -51,13 +52,20 @@ class ProcSmaps: shared_dirty = self.parse_smaps_size_line (lines[line_idx + 4]) private_clean = self.parse_smaps_size_line (lines[line_idx + 5]) private_dirty = self.parse_smaps_size_line (lines[line_idx + 6]) + referenced = self.parse_smaps_size_line (lines[line_idx + 7]) name = name.strip () - mapping = Mapping (size, rss, shared_clean, shared_dirty, private_clean, private_dirty, permissions, name) + mapping = Mapping (size, rss, shared_clean, shared_dirty, \ + private_clean, private_dirty, referenced, permissions, name) self.mappings.append (mapping) - num_lines -= 7 - line_idx += 7 + num_lines -= 8 + line_idx += 8 + + self._clear_reference(pid) + + def _clear_reference(self, pid): + os.system("echo 1 > /proc/%s/clear_refs" % pid) # Parses a line of the form "foo: 42 kB" and returns an integer for the "42" field def parse_smaps_size_line (self, line): @@ -66,13 +74,15 @@ class ProcSmaps: return int(fields[1]) class Mapping: - def __init__ (self, size, rss, shared_clean, shared_dirty, private_clean, private_dirty, permissions, name): + def __init__ (self, size, rss, shared_clean, shared_dirty, \ + private_clean, private_dirty, referenced, permissions, name): self.size = size self.rss = rss self.shared_clean = shared_clean self.shared_dirty = shared_dirty self.private_clean = private_clean self.private_dirty = private_dirty + self.referenced = referenced self.permissions = permissions self.name = name diff --git a/services/console/sugar-console b/services/console/sugar-console index af709a6..357b7fe 100755 --- a/services/console/sugar-console +++ b/services/console/sugar-console @@ -6,7 +6,15 @@ pygtk.require('2.0') import os import sys from sugar import env +from sugar import util sys.path.append(env.get_service_path('console')) +# change to the user's home directory if it is set +# root if not +os.chdir(os.environ.get('HOME', '/')) + +#set the process title so it shows up as sugar-console not python +util.set_proc_title('sugar-console') + import console diff --git a/services/clipboard/Makefile.am b/services/shell/Makefile.am index e5a03e4..b34b974 100644 --- a/services/clipboard/Makefile.am +++ b/services/shell/Makefile.am @@ -1,29 +1,37 @@ servicedir = $(datadir)/dbus-1/services service_in_files = \ + org.laptop.ActivityRegistry.service.in \ org.laptop.Clipboard.service.in \ org.laptop.ObjectTypeRegistry.service.in service_DATA = \ + org.laptop.ActivityRegistry.service \ org.laptop.Clipboard.service \ org.laptop.ObjectTypeRegistry.service +org.laptop.ActivityRegistry.service: org.laptop.ActivityRegistry.service.in Makefile + @sed -e "s|\@bindir\@|$(bindir)|" $< > $@ + org.laptop.Clipboard.service: org.laptop.Clipboard.service.in Makefile @sed -e "s|\@bindir\@|$(bindir)|" $< > $@ org.laptop.ObjectTypeRegistry.service: org.laptop.ObjectTypeRegistry.service.in Makefile @sed -e "s|\@bindir\@|$(bindir)|" $< > $@ -sugardir = $(pkgdatadir)/services/clipboard +sugardir = $(pkgdatadir)/services/shell -sugar_PYTHON = \ - __init__.py \ - clipboardobject.py \ - clipboardservice.py \ +sugar_PYTHON = \ + __init__.py \ + activityregistryservice.py \ + bundleregistry.py \ + clipboardobject.py \ + clipboardservice.py \ objecttypeservice.py -bin_SCRIPTS = sugar-clipboard +bin_SCRIPTS = sugar-shell-service DISTCLEANFILES = $(service_DATA) EXTRA_DIST = $(service_in_files) $(bin_SCRIPTS) + diff --git a/services/clipboard/__init__.py b/services/shell/__init__.py index 52b82c8..52b82c8 100644 --- a/services/clipboard/__init__.py +++ b/services/shell/__init__.py diff --git a/services/shell/activityregistryservice.py b/services/shell/activityregistryservice.py new file mode 100644 index 0000000..44c9969 --- /dev/null +++ b/services/shell/activityregistryservice.py @@ -0,0 +1,114 @@ +# Copyright (C) 2006-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 dbus +import dbus.service + +import bundleregistry + +_ACTIVITY_REGISTRY_SERVICE_NAME = 'org.laptop.ActivityRegistry' +_ACTIVITY_REGISTRY_IFACE = 'org.laptop.ActivityRegistry' +_ACTIVITY_REGISTRY_PATH = '/org/laptop/ActivityRegistry' + +class ActivityRegistry(dbus.service.Object): + def __init__(self): + bus = dbus.SessionBus() + bus_name = dbus.service.BusName(_ACTIVITY_REGISTRY_SERVICE_NAME, bus=bus) + dbus.service.Object.__init__(self, bus_name, _ACTIVITY_REGISTRY_PATH) + + bundle_registry = bundleregistry.get_registry() + bundle_registry.connect('bundle-added', self._bundle_added_cb) + + @dbus.service.method(_ACTIVITY_REGISTRY_IFACE, + in_signature='s', out_signature='b') + def AddBundle(self, bundle_path): + '''Register the activity bundle with the global registry + + bundle_path -- path to the activity bundle's root directory, + that is, the directory with activity/activity.info as a + child of the directory. + + The bundleregistry.BundleRegistry is responsible for setting + up a set of d-bus service mappings for each available activity. + ''' + registry = bundleregistry.get_registry() + return registry.add_bundle(bundle_path) + + @dbus.service.method(_ACTIVITY_REGISTRY_IFACE, + in_signature='', out_signature='aa{sv}') + def GetActivities(self): + result = [] + registry = bundleregistry.get_registry() + for bundle in registry: + result.append(self._bundle_to_dict(bundle)) + return result + + @dbus.service.method(_ACTIVITY_REGISTRY_IFACE, + in_signature='s', out_signature='a{sv}') + def GetActivity(self, service_name): + registry = bundleregistry.get_registry() + bundle = registry.get_bundle(service_name) + if not bundle: + return {} + + return self._bundle_to_dict(bundle) + + @dbus.service.method(_ACTIVITY_REGISTRY_IFACE, + in_signature='s', out_signature='aa{sv}') + def FindActivity(self, name): + result = [] + key = name.lower() + + for bundle in bundleregistry.get_registry(): + name = bundle.get_name().lower() + service_name = bundle.get_service_name().lower() + if name.find(key) != -1 or service_name.find(key) != -1: + result.append(self._bundle_to_dict(bundle)) + + return result + + @dbus.service.method(_ACTIVITY_REGISTRY_IFACE, + in_signature='s', out_signature='aa{sv}') + def GetActivitiesForType(self, mime_type): + result = [] + registry = bundleregistry.get_registry() + for bundle in registry.get_activities_for_type(mime_type): + result.append(self._bundle_to_dict(bundle)) + return result + + @dbus.service.signal(_ACTIVITY_REGISTRY_IFACE, signature='a{sv}') + def ActivityAdded(self, activity_info): + pass + + def _bundle_to_dict(self, bundle): + return {'name': bundle.get_name(), + 'icon': bundle.get_icon(), + 'service_name': bundle.get_service_name(), + 'path': bundle.get_path(), + 'show_launcher': bundle.get_show_launcher()} + + def _bundle_added_cb(self, bundle_registry, bundle): + self.ActivityAdded(self._bundle_to_dict(bundle)) + +_instance = None + +def get_instance(): + global _instance + if not _instance: + _instance = ActivityRegistry() + return _instance + diff --git a/shell/model/bundleregistry.py b/services/shell/bundleregistry.py index bc8eec9..65a2348 100644 --- a/shell/model/bundleregistry.py +++ b/services/shell/bundleregistry.py @@ -106,6 +106,13 @@ class BundleRegistry(gobject.GObject): else: return False + def get_activities_for_type(self, mime_type): + result = [] + for bundle in self._bundles.values(): + if bundle.get_mime_types() and mime_type in bundle.get_mime_types(): + result.append(bundle) + return result + def get_registry(): return _bundle_registry diff --git a/services/clipboard/clipboardobject.py b/services/shell/clipboardobject.py index d751274..bc51f47 100644 --- a/services/clipboard/clipboardobject.py +++ b/services/shell/clipboardobject.py @@ -19,9 +19,9 @@ import logging import urlparse from sugar.objects import mime -from sugar import activity import objecttypeservice +import bundleregistry class ClipboardObject: @@ -66,30 +66,15 @@ class ClipboardObject: return '' def get_activity(self): - logging.debug('get_activity') - mapping = {'text/html' : 'org.laptop.WebActivity', - 'image/jpeg' : 'org.laptop.WebActivity', - 'image/gif' : 'org.laptop.WebActivity', - 'image/png' : 'org.laptop.WebActivity', - 'text/plain' : 'org.laptop.AbiWordActivity', - 'text/rtf' : 'org.laptop.AbiWordActivity', - 'text/richtext' : 'org.laptop.AbiWordActivity', - 'application/pdf' : 'org.laptop.sugar.ReadActivity', - 'application/x-squeak-project' : 'org.vpri.EtoysActivity'} mime = self.get_mime_type() if not mime: return '' - """ - registry = activity.get_registry() + + registry = bundleregistry.get_registry() activities = registry.get_activities_for_type(self.get_mime_type()) # TODO: should we return several activities? if activities: - return activities[0] - else: - return '' - """ - if mapping.has_key(mime): - return mapping[mime] + return activities[0].get_service_name() else: return '' @@ -101,8 +86,6 @@ class ClipboardObject: def add_format(self, format): self._formats[format.get_type()] = format - # We want to get the activity early in order to prevent a DBus lockup. - activity = self.get_activity() def get_formats(self): return self._formats diff --git a/services/clipboard/clipboardservice.py b/services/shell/clipboardservice.py index 639f29c..19958a7 100644 --- a/services/clipboard/clipboardservice.py +++ b/services/shell/clipboardservice.py @@ -74,7 +74,7 @@ class ClipboardService(dbus.service.Object): def add_object_format(self, object_path, format_type, data, on_disk): logging.debug('ClipboardService.add_object_format') cb_object = self._objects[str(object_path)] - + if on_disk and cb_object.get_percent() == 100: new_uri = self._copy_file(data) cb_object.add_format(Format(format_type, new_uri, on_disk)) diff --git a/services/clipboard/objecttypeservice.py b/services/shell/objecttypeservice.py index e12398e..e12398e 100644 --- a/services/clipboard/objecttypeservice.py +++ b/services/shell/objecttypeservice.py diff --git a/services/shell/org.laptop.ActivityRegistry.service.in b/services/shell/org.laptop.ActivityRegistry.service.in new file mode 100644 index 0000000..ab6647c --- /dev/null +++ b/services/shell/org.laptop.ActivityRegistry.service.in @@ -0,0 +1,4 @@ +[D-BUS Service] +Name = org.laptop.ActivityRegistry +Exec = @bindir@/sugar-shell-service + diff --git a/services/clipboard/org.laptop.Clipboard.service.in b/services/shell/org.laptop.Clipboard.service.in index b38bf2b..7ce3f6e 100644 --- a/services/clipboard/org.laptop.Clipboard.service.in +++ b/services/shell/org.laptop.Clipboard.service.in @@ -1,4 +1,4 @@ [D-BUS Service] Name = org.laptop.Clipboard -Exec = @bindir@/sugar-clipboard +Exec = @bindir@/sugar-shell-service diff --git a/services/clipboard/org.laptop.ObjectTypeRegistry.service.in b/services/shell/org.laptop.ObjectTypeRegistry.service.in index 66477eb..563a600 100644 --- a/services/clipboard/org.laptop.ObjectTypeRegistry.service.in +++ b/services/shell/org.laptop.ObjectTypeRegistry.service.in @@ -1,4 +1,4 @@ [D-BUS Service] Name = org.laptop.ObjectTypeRegistry -Exec = @bindir@/sugar-clipboard +Exec = @bindir@/sugar-shell-service diff --git a/services/clipboard/sugar-clipboard b/services/shell/sugar-shell-service index 4cffa33..370c2ea 100755 --- a/services/clipboard/sugar-clipboard +++ b/services/shell/sugar-shell-service @@ -23,28 +23,31 @@ import os import logging from sugar import logger -logger.start('clipboard') +logger.start('shellservice') import gobject import dbus.glib from sugar import env -sys.path.append(env.get_service_path('clipboard')) +sys.path.append(env.get_service_path('shell')) import clipboardservice import objecttypeservice +import activityregistryservice -logging.info('Starting clipboard service.') +logging.info('Starting shell service.') gobject.threads_init() dbus.glib.threads_init() clipboard_service = clipboardservice.get_instance() object_type_registry = objecttypeservice.get_instance() +activity_registry = activityregistryservice.get_instance() loop = gobject.MainLoop() try: loop.run() except KeyboardInterrupt: print 'Ctrl+C pressed, exiting...' + diff --git a/shell/model/Makefile.am b/shell/model/Makefile.am index 486ad09..0b7d14c 100644 --- a/shell/model/Makefile.am +++ b/shell/model/Makefile.am @@ -4,7 +4,6 @@ sugardir = $(pkgdatadir)/shell/model sugar_PYTHON = \ __init__.py \ accesspointmodel.py \ - bundleregistry.py \ BuddyModel.py \ Friends.py \ Invites.py \ diff --git a/shell/model/MeshModel.py b/shell/model/MeshModel.py index 44974e2..82980c3 100644 --- a/shell/model/MeshModel.py +++ b/shell/model/MeshModel.py @@ -18,29 +18,29 @@ import gobject from sugar.graphics.xocolor import XoColor from sugar.presence import presenceservice +from sugar import activity -from model import bundleregistry 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): + def __init__(self, activity, activity_info): self._activity = activity - self._bundle = bundle + self._activity_info = activity_info def get_id(self): return self._activity.props.id def get_icon_name(self): - return self._bundle.get_icon() + return self._activity_info.icon def get_color(self): return XoColor(self._activity.props.color) def get_service_name(self): - return self._bundle.get_service_name() + return self._activity_info.service_name def get_title(self): return self._activity.props.name @@ -75,7 +75,6 @@ class MeshModel(gobject.GObject): self._buddies = {} self._access_points = {} self._mesh = None - self._bundle_registry = bundleregistry.get_registry() self._pservice = presenceservice.get_instance() self._pservice.connect("activity-appeared", @@ -196,13 +195,14 @@ class MeshModel(gobject.GObject): def _activity_appeared_cb(self, pservice, activity): self._check_activity(activity) - def _check_activity(self, activity): - bundle = self._bundle_registry.get_bundle(activity.props.type) - if not bundle: + def _check_activity(self, presence_activity): + registry = activity.get_registry() + activity_info = registry.get_activity(presence_activity.props.type) + if not activity_info: return - if self.has_activity(activity.props.id): + if self.has_activity(presence_activity.props.id): return - self.add_activity(bundle, activity) + self.add_activity(activity_info, presence_activity) def has_activity(self, activity_id): return self._activities.has_key(activity_id) @@ -213,8 +213,8 @@ class MeshModel(gobject.GObject): else: return None - def add_activity(self, bundle, activity): - model = ActivityModel(activity, bundle) + def add_activity(self, activity_info, activity): + model = ActivityModel(activity, activity_info) self._activities[model.get_id()] = model self.emit('activity-added', model) diff --git a/shell/model/homeactivity.py b/shell/model/homeactivity.py index c45e5c7..e95fe9a 100644 --- a/shell/model/homeactivity.py +++ b/shell/model/homeactivity.py @@ -44,10 +44,10 @@ class HomeActivity(gobject.GObject): gobject.PARAM_READWRITE), } - def __init__(self, bundle, activity_id): + def __init__(self, activity_info, activity_id): """Initialise the HomeActivity - bundle -- sugar.activity.bundle.Bundle instance, + 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. @@ -61,7 +61,7 @@ class HomeActivity(gobject.GObject): self._pid = None self._service = None self._activity_id = activity_id - self._bundle = bundle + self._activity_info = activity_info self._launch_time = time.time() self._launching = False @@ -99,9 +99,9 @@ class HomeActivity(gobject.GObject): return self._window.get_name() def get_icon_name(self): - """Retrieve the bundle's icon (file) name""" - if self._bundle: - return self._bundle.get_icon() + """Retrieve the activity's icon (file) name""" + if self._activity_info: + return self._activity_info.icon else: return 'theme:stock-missing' @@ -156,9 +156,9 @@ class HomeActivity(gobject.GObject): return self._window def get_type(self): - """Retrieve bundle's "service_name" for future reference""" - if self._bundle: - return self._bundle.get_service_name() + """Retrieve activity_info's "service_name" for future reference""" + if self._activity_info: + return self._activity_info.service_name else: return None diff --git a/shell/model/homemodel.py b/shell/model/homemodel.py index 7d1039b..dac434a 100644 --- a/shell/model/homemodel.py +++ b/shell/model/homemodel.py @@ -21,9 +21,9 @@ import wnck import dbus from sugar import wm +from sugar import activity from model.homeactivity import HomeActivity -from model import bundleregistry class HomeModel(gobject.GObject): """Model of the "Home" view (activity management) @@ -51,15 +51,18 @@ class HomeModel(gobject.GObject): ([gobject.TYPE_PYOBJECT])), 'active-activity-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([gobject.TYPE_PYOBJECT])) + ([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._bundle_registry = bundleregistry.get_registry() - self._current_activity = None + self._active_activity = None + self._pending_activity = None screen = wnck.screen_get_default() screen.connect('window-opened', self._window_opened_cb) @@ -67,8 +70,55 @@ class HomeModel(gobject.GObject): screen.connect('active-window-changed', self._active_window_changed_cb) - def get_current_activity(self): - return self._current_activity + 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.set_active(False, + reply_handler=self._set_active_success, + error_handler=self._set_active_error) + if home_activity: + service = home_activity.get_service() + if service: + service.set_active(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) @@ -84,65 +134,48 @@ class HomeModel(gobject.GObject): def _window_opened_cb(self, screen, window): if window.get_window_type() == wnck.WINDOW_NORMAL: - activity = None + home_activity = None activity_id = wm.get_activity_id(window) - bundle_id = wm.get_bundle_id(window) - if bundle_id: - bundle = self._bundle_registry.get_bundle(bundle_id) + service_name = wm.get_bundle_id(window) + if service_name: + registry = activity.get_registry() + activity_info = registry.get_activity(service_name) else: - bundle = None + activity_info = None if activity_id: - activity = self._get_activity_by_id(activity_id) + home_activity = self._get_activity_by_id(activity_id) - if not activity: - activity = HomeActivity(bundle, activity_id) - self._add_activity(activity) + if not home_activity: + home_activity = HomeActivity(activity_info, activity_id) + self._add_activity(home_activity) - activity.set_window(window) + home_activity.set_window(window) - activity.props.launching = False - self.emit('activity-started', activity) + 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()) - if not self._activities: - self.emit('active-activity-changed', None) - self._notify_activity_activation(self._current_activity, None) def _get_activity_by_xid(self, xid): - for activity in self._activities: - if activity.get_xid() == xid: - return activity + 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 activity in self._activities: - if activity.get_activity_id() == activity_id: - return activity + for home_activity in self._activities: + if home_activity.get_activity_id() == activity_id: + return home_activity return None - def _notify_activity_activation(self, old_activity, new_activity): - if old_activity == new_activity: - return - - if old_activity: - service = old_activity.get_service() - if service: - service.set_active(False, - reply_handler=self._set_active_success, - error_handler=self._set_active_error) - - if new_activity: - service = new_activity.get_service() - if service: - service.set_active(True, - reply_handler=self._set_active_success, - error_handler=self._set_active_error) - def _set_active_success(self): pass @@ -151,55 +184,58 @@ class HomeModel(gobject.GObject): def _active_window_changed_cb(self, screen): window = screen.get_active_window() - if window == None: - self.emit('active-activity-changed', None) - self._notify_activity_activation(self._current_activity, None) - return - if window.get_window_type() != wnck.WINDOW_NORMAL: + if window is None or window.get_window_type() != wnck.WINDOW_NORMAL: return xid = window.get_xid() - act = self._get_activity_by_xid(window.get_xid()) - if act: - self._notify_activity_activation(self._current_activity, act) - self._current_activity = act - else: - self._notify_activity_activation(self._current_activity, None) - self._current_activity = None + act = self._get_activity_by_xid(xid) + if act is None: logging.error('Model for window %d does not exist.' % xid) - - self.emit('active-activity-changed', self._current_activity) - - def _add_activity(self, activity): - self._activities.append(activity) - self.emit('activity-added', activity) - - def _remove_activity(self, activity): - if activity == self._current_activity: - self._current_activity = None - - self.emit('activity-removed', activity) - self._activities.remove(activity) + self._set_pending_activity(act) + self._set_active_activity(act) + + 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) + # 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): - activity = self._get_activity_by_xid(xid) - if activity: - self._remove_activity(activity) + 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): - bundle = self._bundle_registry.get_bundle(service_name) - if not bundle: + 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) - activity = HomeActivity(bundle, activity_id) - activity.props.launching = True - self._add_activity(activity) + home_activity = HomeActivity(activity_info, activity_id) + home_activity.props.launching = True + self._add_activity(home_activity) def notify_activity_launch_failed(self, activity_id): - activity = self._get_activity_by_id(activity_id) - if activity: - logging.debug("Activity %s (%s) launch failed" % (activity_id, activity.get_type())) - self._remove_activity(activity) + 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) diff --git a/shell/shellservice.py b/shell/shellservice.py index 5728e44..d577a44 100644 --- a/shell/shellservice.py +++ b/shell/shellservice.py @@ -17,10 +17,7 @@ """D-bus service providing access to the shell's functionality""" import dbus -from model import bundleregistry - _DBUS_SERVICE = "org.laptop.Shell" -_DBUS_ACTIVITY_REGISTRY_IFACE = "org.laptop.Shell.ActivityRegistry" _DBUS_SHELL_IFACE = "org.laptop.Shell" _DBUS_OWNER_IFACE = "org.laptop.Shell.Owner" _DBUS_PATH = "/org/laptop/Shell" @@ -56,9 +53,6 @@ class ShellService(dbus.service.Object): self._home_model.connect('active-activity-changed', self._cur_activity_changed_cb) - bundle_registry = bundleregistry.get_registry() - bundle_registry.connect('bundle-added', self._bundle_added_cb) - bus = dbus.SessionBus() bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus) dbus.service.Object.__init__(self, bus_name, _DBUS_PATH) @@ -83,60 +77,6 @@ class ShellService(dbus.service.Object): def NotifyLaunchFailure(self, activity_id): self._shell.notify_launch_failure(activity_id) - @dbus.service.method(_DBUS_ACTIVITY_REGISTRY_IFACE, - in_signature="s", out_signature="b") - def AddBundle(self, bundle_path): - """Register the activity bundle with the global registry - - bundle_path -- path to the activity bundle's root directory, - that is, the directory with activity/activity.info as a - child of the directory. - - The bundleregistry.BundleRegistry is responsible for setting - up a set of d-bus service mappings for each available activity. - """ - registry = bundleregistry.get_registry() - return registry.add_bundle(bundle_path) - - @dbus.service.method(_DBUS_ACTIVITY_REGISTRY_IFACE, - in_signature="s", out_signature="a{sv}") - def GetActivity(self, service_name): - registry = bundleregistry.get_registry() - bundle = registry.get_bundle(service_name) - if not bundle: - return {} - - return self._bundle_to_dict(bundle) - - @dbus.service.method(_DBUS_ACTIVITY_REGISTRY_IFACE, - in_signature="s", out_signature="aa{sv}") - def FindActivity(self, name): - result = [] - key = name.lower() - - for bundle in bundleregistry.get_registry(): - name = bundle.get_name().lower() - service_name = bundle.get_service_name().lower() - if name.find(key) != -1 or service_name.find(key) != -1: - result.append(self._bundle_to_dict(bundle)) - - return result - - @dbus.service.method(_DBUS_ACTIVITY_REGISTRY_IFACE, - in_signature="s", out_signature="aa{sv}") - def GetActivitiesForType(self, mime_type): - result = [] - - for bundle in bundleregistry.get_registry(): - if bundle.get_mime_types() and mime_type in bundle.get_mime_types(): - result.append(self._bundle_to_dict(bundle)) - - return result - - @dbus.service.signal(_DBUS_ACTIVITY_REGISTRY_IFACE, signature="a{sv}") - def ActivityAdded(self, activity_info): - pass - @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s") def ColorChanged(self, color): pass @@ -169,12 +109,3 @@ class ShellService(dbus.service.Object): if new_id: self.CurrentActivityChanged(new_id) - def _bundle_to_dict(self, bundle): - return {'name': bundle.get_name(), - 'icon': bundle.get_icon(), - 'service_name': bundle.get_service_name(), - 'path': bundle.get_path()} - - def _bundle_added_cb(self, bundle_registry, bundle): - self.ActivityAdded(self._bundle_to_dict(bundle)) - diff --git a/shell/view/BuddyMenu.py b/shell/view/BuddyMenu.py index 3162ab1..e3efb5c 100644 --- a/shell/view/BuddyMenu.py +++ b/shell/view/BuddyMenu.py @@ -85,18 +85,19 @@ class BuddyMenu(Palette): else: menu_item = MenuItem(_('Make friend'), 'stock-add') menu_item.connect('activate', self._make_friend_cb) - self.append_menu_item(menu_item) + + self.menu.append(menu_item) menu_item.show() - activity = shell_model.get_home().get_current_activity() + activity = self._shell.get_current_activity() if activity != None: - activity_ps = pservice.get_activity(activity.get_activity_id()) + activity_ps = pservice.get_activity(activity.get_id()) # FIXME check that the buddy is not in the activity already menu_item = MenuItem(_('Invite'), 'stock-invite') menu_item.connect('activate', self._invite_friend_cb) - self.append_menu_item(menu_item) + self.menu.append(menu_item) menu_item.show() def _buddy_icon_changed_cb(self, buddy): diff --git a/shell/view/Shell.py b/shell/view/Shell.py index 697dc1c..044cbde 100644 --- a/shell/view/Shell.py +++ b/shell/view/Shell.py @@ -26,6 +26,7 @@ import gtk import wnck from sugar.activity.activityhandle import ActivityHandle +from sugar import activity from sugar.activity import activityfactory from sugar.datastore import datastore from sugar import profile @@ -34,7 +35,6 @@ from view.ActivityHost import ActivityHost from view.frame.frame import Frame from view.keyhandler import KeyHandler from view.home.HomeWindow import HomeWindow -from model import bundleregistry from model.shellmodel import ShellModel from hardware import hardwaremanager @@ -47,6 +47,7 @@ class Shell(gobject.GObject): self._hosts = {} self._screen = wnck.screen_get_default() self._current_host = None + self._pending_host = None self._screen_rotation = 0 self._key_handler = KeyHandler(self) @@ -65,6 +66,8 @@ class Shell(gobject.GObject): 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) # Unfreeze the display when it's stable hw_manager = hardwaremanager.get_manager() @@ -100,6 +103,12 @@ class Shell(gobject.GObject): 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 @@ -107,16 +116,16 @@ class Shell(gobject.GObject): return self._frame def join_activity(self, bundle_id, activity_id): - activity = self.get_activity(activity_id) - if activity: - activity.present() + 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 - breg = bundleregistry.get_registry() - bundle = breg.get_bundle(bundle_id) + 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 @@ -156,6 +165,8 @@ class Shell(gobject.GObject): return 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) diff --git a/shell/view/clipboardicon.py b/shell/view/clipboardicon.py index a47104d..2e60e36 100644 --- a/shell/view/clipboardicon.py +++ b/shell/view/clipboardicon.py @@ -73,9 +73,9 @@ class ClipboardIcon(CanvasIcon): self._selected = selected if selected: if not self._hover: - self.props.background_color = style.COLOR_PANEL_GREY.get_int() + self.props.background_color = style.COLOR_SELECTION_GREY.get_int() else: - self.props.background_color = style.COLOR_TOOLBAR_GREY.get_int() + self.props.background_color = style.COLOR_PANEL_GREY.get_int() def set_state(self, name, percent, icon_name, preview, activity): cb_service = clipboardservice.get_instance() @@ -107,11 +107,11 @@ class ClipboardIcon(CanvasIcon): def prelight(self, enter): if enter: self._hover = True - self.props.background_color = color.BLACK.get_int() + self.props.background_color = style.COLOR_BLACK.get_int() else: self._hover = False if self._selected: - self.props.background_color = color.DESKTOP_BACKGROUND.get_int() + self.props.background_color = style.COLOR_SELECTION_GREY.get_int() else: - self.props.background_color = color.TOOLBAR_BACKGROUND.get_int() + self.props.background_color = style.COLOR_PANEL_GREY.get_int() diff --git a/shell/view/clipboardmenu.py b/shell/view/clipboardmenu.py index 35802dc..28ea0bb 100644 --- a/shell/view/clipboardmenu.py +++ b/shell/view/clipboardmenu.py @@ -64,11 +64,13 @@ class ClipboardMenu(Palette): self._remove_item = MenuItem(_('Remove'), 'stock-remove') self._remove_item.connect('activate', self._remove_item_activate_cb) - self.append_menu_item(self._remove_item) + self.menu.append(self._remove_item) + self._remove_item.show() self._open_item = MenuItem(_('Open'), 'stock-keep') self._open_item.connect('activate', self._open_item_activate_cb) - self.append_menu_item(self._open_item) + self.menu.append(self._open_item) + self._open_item.show() #self._stop_item = MenuItem(_('Stop download'), 'stock-close') # TODO: Implement stopping downloads @@ -77,7 +79,8 @@ class ClipboardMenu(Palette): self._journal_item = MenuItem(_('Add to journal'), 'document-save') self._journal_item.connect('activate', self._journal_item_activate_cb) - self.append_menu_item(self._journal_item) + self.menu.append(self._journal_item) + self._journal_item.show() self._update_items_visibility(installable) @@ -120,32 +123,8 @@ class ClipboardMenu(Palette): def _open_item_activate_cb(self, menu_item): if self._percent < 100: return - jobject = self._copy_to_journal() - # TODO: we cannot simply call resume() right now because we would lock - # the shell as we are sharing the same loop as the shell service. - #jobject.resume() - - # TODO: take this out when we fix the mess that is the shell/shellservice. - from model import bundleregistry - from sugar.activity.bundle import Bundle - from sugar.activity import activityfactory - if jobject.is_bundle(): - bundle = Bundle(jobject.file_path) - if not bundle.is_installed(): - bundle.install() - - activityfactory.create(bundle.get_service_name()) - else: - service_name = None - mime_type = jobject.metadata['mime_type'] - for bundle in bundleregistry.get_registry(): - if bundle.get_mime_types() and mime_type in bundle.get_mime_types(): - service_name = bundle.get_service_name() - break - if service_name: - activityfactory.create_with_object_id(service_name, - jobject.object_id) + jobject.resume() def _remove_item_activate_cb(self, menu_item): cb_service = clipboardservice.get_instance() diff --git a/shell/view/frame/ActivitiesBox.py b/shell/view/frame/ActivitiesBox.py index a46e8e9..909a5f2 100644 --- a/shell/view/frame/ActivitiesBox.py +++ b/shell/view/frame/ActivitiesBox.py @@ -20,30 +20,33 @@ import logging from sugar.graphics.palette import Palette from sugar.graphics.xocolor import XoColor from sugar.graphics.iconbutton import IconButton +from sugar.graphics import style from sugar import profile +from sugar import activity -from model import bundleregistry from frameinvoker import FrameCanvasInvoker class ActivityButton(IconButton): - def __init__(self, activity): - IconButton.__init__(self, icon_name=activity.get_icon()) + def __init__(self, activity_info): + IconButton.__init__(self, icon_name=activity_info.icon, + stroke_color=style.COLOR_WHITE, + fill_color=style.COLOR_TRANSPARENT) - palette = Palette(activity.get_name()) + palette = Palette(activity_info.name) palette.props.invoker = FrameCanvasInvoker(self) palette.set_group_id('frame') self.set_palette(palette) - self._activity = activity + self._activity_info = activity_info def get_bundle_id(self): - return self._activity.get_service_name() + return self._activity_info.service_name class InviteButton(IconButton): - def __init__(self, activity, invite): - IconButton.__init__(self, icon_name=activity.get_icon()) + def __init__(self, activity_model, invite): + IconButton.__init__(self, icon_name=activity_model.get_color()) - self.props.xo_color = activity.get_color() + self.props.xo_color = activity_model.get_color() self._invite = invite def get_activity_id(self): @@ -64,12 +67,12 @@ class ActivitiesBox(hippo.CanvasBox): self._invite_to_item = {} self._invites = self._shell_model.get_invites() - bundle_registry = bundleregistry.get_registry() - for bundle in bundle_registry: - if bundle.get_show_launcher(): - self.add_activity(bundle) + registry = activity.get_registry() + for activity_info in registry.get_activities(): + if activity_info.show_launcher: + self.add_activity(activity_info) - bundle_registry.connect('bundle-added', self._bundle_added_cb) + registry.connect('activity-added', self._activity_added_cb) for invite in self._invites: self.add_invite(invite) @@ -90,19 +93,19 @@ class ActivitiesBox(hippo.CanvasBox): def _invite_removed_cb(self, invites, invite): self.remove_invite(invite) - def _bundle_added_cb(self, bundle_registry, bundle): - self.add_activity(bundle) + def _activity_added_cb(self, activity_registry, activity_info): + self.add_activity(activity_info) - def add_activity(self, activity): - item = ActivityButton(activity) + def add_activity(self, activity_info): + item = ActivityButton(activity_info) item.connect('activated', self._activity_clicked_cb) self.append(item, 0) def add_invite(self, invite): mesh = self._shell_model.get_mesh() - activity = mesh.get_activity(invite.get_activity_id()) + activity_model = mesh.get_activity(invite.get_activity_id()) if activity: - item = InviteButton(activity, invite) + item = InviteButton(activity_model, invite) item.connect('activated', self._invite_clicked_cb) self.append(item, 0) diff --git a/shell/view/home/FriendView.py b/shell/view/home/FriendView.py index c585312..ed05892 100644 --- a/shell/view/home/FriendView.py +++ b/shell/view/home/FriendView.py @@ -20,8 +20,8 @@ import gobject from sugar.graphics.canvasicon import CanvasIcon from sugar.graphics import style from sugar.presence import presenceservice +from sugar import activity -from model import bundleregistry from view.BuddyIcon import BuddyIcon class FriendView(hippo.CanvasBox): @@ -46,9 +46,9 @@ class FriendView(hippo.CanvasBox): self._buddy.connect('disappeared', self._buddy_disappeared_cb) self._buddy.connect('color-changed', self._buddy_color_changed_cb) - def _get_new_icon_name(self, activity): - registry = bundleregistry.get_registry() - bundle = registry.get_bundle(activity.get_type()) + def _get_new_icon_name(self, home_activity): + registry = activity.get_registry() + bundle = registry.get_bundle(home_activity.get_type()) if bundle: return bundle.get_icon() return None @@ -58,14 +58,14 @@ class FriendView(hippo.CanvasBox): self.remove(self._activity_icon) self._activity_icon_visible = False - def _buddy_activity_changed_cb(self, buddy, activity=None): - if not activity: + def _buddy_activity_changed_cb(self, buddy, home_activity=None): + if not home_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(activity) + name = self._get_new_icon_name(home_activity) if name: self._activity_icon.props.icon_name = name self._activity_icon.props.xo_color = buddy.get_color() @@ -76,8 +76,8 @@ class FriendView(hippo.CanvasBox): self._remove_activity_icon() def _buddy_appeared_cb(self, buddy): - activity = self._buddy.get_current_activity() - self._buddy_activity_changed_cb(buddy, activity) + 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) diff --git a/shell/view/home/HomeBox.py b/shell/view/home/HomeBox.py index bfb4265..2fa2183 100644 --- a/shell/view/home/HomeBox.py +++ b/shell/view/home/HomeBox.py @@ -130,7 +130,7 @@ class HomeMyIcon(MyIcon): shutdown_menu_item = gtk.MenuItem(_('Shutdown')) shutdown_menu_item.connect('activate', self._shutdown_activate_cb) - palette.append_menu_item(shutdown_menu_item) + palette.menu.append(shutdown_menu_item) shutdown_menu_item.show() self.set_palette(palette) diff --git a/shell/view/home/activitiesdonut.py b/shell/view/home/activitiesdonut.py index 0c690b2..3b12a09 100644 --- a/shell/view/home/activitiesdonut.py +++ b/shell/view/home/activitiesdonut.py @@ -88,14 +88,14 @@ class ActivityIcon(CanvasIcon): resume_menu_item = gtk.MenuItem(_('Resume')) resume_menu_item.connect('activate', self._resume_activate_cb) - palette.append_menu_item(resume_menu_item) + palette.menu.append(resume_menu_item) resume_menu_item.show() # FIXME: kludge if self._activity.get_type() != "org.laptop.JournalActivity": stop_menu_item = gtk.MenuItem(_('Stop')) stop_menu_item.connect('activate', self._stop_activate_cb) - palette.append_menu_item(stop_menu_item) + palette.menu.append(stop_menu_item) stop_menu_item.show() def _launching_changed_cb(self, activity, pspec): @@ -189,7 +189,7 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): 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('active-activity-changed', self._activity_changed_cb) + self._model.connect('pending-activity-changed', self._activity_changed_cb) self.connect('button-release-event', self._button_release_event_cb) @@ -385,7 +385,7 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): cr.fill() # Selected Wedge - current_activity = self._model.get_current_activity() + 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) diff --git a/sugar/Makefile.am b/sugar/Makefile.am index 5fcb387..dffca33 100644 --- a/sugar/Makefile.am +++ b/sugar/Makefile.am @@ -12,21 +12,19 @@ sugar_PYTHON = \ util.py \ wm.py -INCLUDES = \ - $(LIB_CFLAGS) \ - $(LIB_BINDINGS_CFLAGS) \ - $(PYTHON_INCLUDES) \ - -I$(top_srcdir)/lib \ - -I$(top_srcdir)/lib/ui - pkgpyexecdir = $(pythondir)/sugar pkgpyexec_LTLIBRARIES = _sugarext.la _sugaruiext.la +_sugarext_la_CFLAGS = \ + $(LIB_CFLAGS) \ + $(LIB_BINDINGS_CFLAGS) \ + $(PYTHON_INCLUDES) \ + -I$(top_srcdir)/lib + _sugarext_la_LDFLAGS = -module -avoid-version _sugarext_la_LIBADD = \ $(LIB_BINDINGS_LIBS) \ - $(LIB_LIBS) \ $(top_builddir)/lib/libsugar.la _sugarext_la_SOURCES = \ @@ -36,10 +34,16 @@ nodist__sugarext_la_SOURCES = _sugarext.c _sugarext.c: _sugarext.defs _sugarext.override +_sugaruiext_la_CFLAGS = \ + $(LIBUI_CFLAGS) \ + $(LIBUI_BINDINGS_CFLAGS) \ + $(PYTHON_INCLUDES) \ + -I$(top_srcdir)/lib/ui + _sugaruiext_la_LDFLAGS = -module -avoid-version _sugaruiext_la_LIBADD = \ - $(LIB_BINDINGS_LIBS) \ - $(LIB_LIBS) \ + $(LIBUI_BINDINGS_LIBS) \ + $(LIBUI_LIBS) \ $(top_builddir)/lib/ui/libsugarui.la _sugaruiext_la_SOURCES = \ diff --git a/sugar/_sugaruiext.defs b/sugar/_sugaruiext.defs index c4efbbc..3c011e1 100644 --- a/sugar/_sugaruiext.defs +++ b/sugar/_sugaruiext.defs @@ -26,19 +26,27 @@ ;; From sugar-menu.h -(define-method popup +(define-method set_active (of-object "SugarMenu") - (c-name "sugar_menu_popup") + (c-name "sugar_menu_set_active") (return-type "none") (parameters - '("gint" "x") - '("gint" "y") + '("gboolean" "active") ) ) -(define-method popdown +(define-method embed (of-object "SugarMenu") - (c-name "sugar_menu_popdown") + (c-name "sugar_menu_embed") + (return-type "none") + (parameters + '("GtkContainer" "container") + ) +) + +(define-method unembed + (of-object "SugarMenu") + (c-name "sugar_menu_unembed") (return-type "none") ) diff --git a/sugar/_sugaruiext.override b/sugar/_sugaruiext.override index 02e900e..6daafc3 100644 --- a/sugar/_sugaruiext.override +++ b/sugar/_sugaruiext.override @@ -18,6 +18,7 @@ modulename _sugarext import gobject.GObject as PyGObject_Type import gtk.Entry as PyGtkEntry_Type import gtk.Menu as PyGtkMenu_Type +import gtk.Container as PyGtkContainer_Type import gtk.gdk.Window as PyGdkWindow_Type %% ignore-glob diff --git a/sugar/activity/activityfactory.py b/sugar/activity/activityfactory.py index 404e5f4..b1d55eb 100644 --- a/sugar/activity/activityfactory.py +++ b/sugar/activity/activityfactory.py @@ -114,7 +114,7 @@ class ActivityCreationHandler(gobject.GObject): self._factory.create(self._activity_handle.get_dict(), timeout=120 * 1000, - reply_handler=self._no_reply_handler, + reply_handler=self._create_reply_handler, error_handler=self._create_error_handler) def get_activity_id(self): @@ -137,7 +137,10 @@ class ActivityCreationHandler(gobject.GObject): def _activate_error_handler(self, err): logging.debug("Activity activation request failed %s" % err) - def _create_reply_handler(self, xid): + def _create_reply_handler(self, xid=None): + if xid is None: + self._create_error_handler('D-Bus error') + return logging.debug("Activity created %s (%s)." % (self._activity_handle.activity_id, self._service_name)) diff --git a/sugar/activity/bundle.py b/sugar/activity/bundle.py index a9c246d..d361c62 100644 --- a/sugar/activity/bundle.py +++ b/sugar/activity/bundle.py @@ -41,6 +41,7 @@ class NotInstalledException(Exception): pass class InvalidPathException(Exception): pass class ZipExtractException(Exception): pass class RegistrationException(Exception): pass +class MalformedBundleException(Exception): pass class Bundle: """Metadata description of a given application/activity @@ -265,10 +266,12 @@ class Bundle: if not bundle_root_dir: bundle_root_dir = file_name.split('/')[0] if not bundle_root_dir.endswith('.activity'): - raise 'Incorrect bundle.' + raise MalformedBundleException( + 'The activity directory name must end with .activity') else: if not file_name.startswith(bundle_root_dir): - raise 'Incorrect bundle.' + raise MalformedBundleException( + 'All files in the bundle must be inside the activity directory') return bundle_root_dir @@ -293,11 +296,8 @@ class Bundle: raise ZipExtractException self._init_with_path(bundle_path) - - bus = dbus.SessionBus() - proxy_obj = bus.get_object(_DBUS_SHELL_SERVICE, _DBUS_SHELL_PATH) - dbus_service = dbus.Interface(proxy_obj, _DBUS_ACTIVITY_REGISTRY_IFACE) - if not dbus_service.AddBundle(bundle_path): + + if not activity.get_registry().add_bundle(bundle_path): raise RegistrationException def deinstall(self): diff --git a/sugar/activity/bundlebuilder.py b/sugar/activity/bundlebuilder.py index b255cfb..3bbe454 100644 --- a/sugar/activity/bundlebuilder.py +++ b/sugar/activity/bundlebuilder.py @@ -162,8 +162,7 @@ def _get_mo_list(manifest): for lang in _get_po_list(manifest).keys(): filename = _get_service_name() + '.mo' - mo_list.append(os.path.join(_get_source_path(), 'locale', - lang, 'LC_MESSAGES', filename)) + mo_list.append(os.path.join('locale', lang, 'LC_MESSAGES', filename)) return mo_list diff --git a/sugar/activity/registry.py b/sugar/activity/registry.py index b19abee..1483a78 100644 --- a/sugar/activity/registry.py +++ b/sugar/activity/registry.py @@ -1,4 +1,5 @@ # Copyright (C) 2006-2007 Red Hat, Inc. +# 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 @@ -18,29 +19,39 @@ import logging import dbus +import gobject -_SHELL_SERVICE = "org.laptop.Shell" -_SHELL_PATH = "/org/laptop/Shell" -_REGISTRY_IFACE = "org.laptop.Shell.ActivityRegistry" +_ACTIVITY_REGISTRY_SERVICE_NAME = 'org.laptop.ActivityRegistry' +_ACTIVITY_REGISTRY_IFACE = 'org.laptop.ActivityRegistry' +_ACTIVITY_REGISTRY_PATH = '/org/laptop/ActivityRegistry' def _activity_info_from_dict(info_dict): if not info_dict: return None return ActivityInfo(info_dict['name'], info_dict['icon'], - info_dict['service_name'], info_dict['path']) + info_dict['service_name'], info_dict['path'], + info_dict['show_launcher']) class ActivityInfo(object): - def __init__(self, name, icon, service_name, path): + def __init__(self, name, icon, service_name, path, show_launcher): self.name = name self.icon = icon self.service_name = service_name self.path = path + self.show_launcher = show_launcher -class ActivityRegistry(object): +class ActivityRegistry(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } def __init__(self): + gobject.GObject.__init__(self) + bus = dbus.SessionBus() - bus_object = bus.get_object(_SHELL_SERVICE, _SHELL_PATH) - self._registry = dbus.Interface(bus_object, _REGISTRY_IFACE) + bus_object = bus.get_object(_ACTIVITY_REGISTRY_SERVICE_NAME, + _ACTIVITY_REGISTRY_PATH) + self._registry = dbus.Interface(bus_object, _ACTIVITY_REGISTRY_IFACE) self._registry.connect_to_signal('ActivityAdded', self._activity_added_cb) # Two caches fo saving some travel across dbus. @@ -55,6 +66,10 @@ class ActivityRegistry(object): return result + def get_activities(self): + info_list = self._registry.GetActivities() + return self._convert_info_list(info_list) + def get_activity(self, service_name): if self._service_name_to_activity_info.has_key(service_name): return self._service_name_to_activity_info[service_name] @@ -79,10 +94,14 @@ class ActivityRegistry(object): self._mime_type_to_activities[mime_type] = activities return activities - def _activity_added_cb(self, bundle): + def add_bundle(self, bundle_path): + return self._registry.AddBundle(bundle_path) + + def _activity_added_cb(self, info_dict): logging.debug('ActivityRegistry._activity_added_cb: flushing caches') self._service_name_to_activity_info.clear() self._mime_type_to_activities.clear() + self.emit('activity-added', _activity_info_from_dict(info_dict)) _registry = None diff --git a/sugar/graphics/canvasicon.py b/sugar/graphics/canvasicon.py index e879f05..39f1358 100644 --- a/sugar/graphics/canvasicon.py +++ b/sugar/graphics/canvasicon.py @@ -231,13 +231,13 @@ class CanvasIcon(hippo.CanvasBox, hippo.CanvasItem): stroke_color = None if self._active: if self._fill_color: - fill_color = self._fill_color.get_html() + fill_color = self._fill_color.get_svg() if self._stroke_color: - stroke_color = self._stroke_color.get_html() + stroke_color = self._stroke_color.get_svg() else: - stroke_color = color.ICON_STROKE_INACTIVE.get_html() + stroke_color = color.ICON_STROKE_INACTIVE.get_svg() if self._fill_color: - fill_color = self._fill_color.get_html() + fill_color = self._fill_color.get_svg() return [fill_color, stroke_color] def _get_handle(self, name, handle): diff --git a/sugar/graphics/iconbutton.py b/sugar/graphics/iconbutton.py index 85ea4e4..ba9fad8 100644 --- a/sugar/graphics/iconbutton.py +++ b/sugar/graphics/iconbutton.py @@ -53,7 +53,7 @@ class IconButton(CanvasIcon, hippo.CanvasItem): if self.props.active: self.props.background_color = 0x000000FF else: - self.props.background_color = 0x404040FF + self.props.background_color = 0x00000000 def _icon_clicked_cb(self, button): if self._palette: diff --git a/sugar/graphics/objectchooser.py b/sugar/graphics/objectchooser.py index c75cec0..5b09e13 100644 --- a/sugar/graphics/objectchooser.py +++ b/sugar/graphics/objectchooser.py @@ -21,12 +21,12 @@ import time import gtk import hippo -from sugar.graphics.frame import Frame from sugar.activity.bundle import Bundle from sugar.date import Date from sugar.graphics import style from sugar.graphics.canvasicon import CanvasIcon from sugar.graphics.xocolor import XoColor +from sugar.graphics.canvasroundbox import CanvasRoundBox from sugar.datastore import datastore from sugar import activity from sugar.objects import objecttype @@ -95,12 +95,12 @@ class ObjectChooser(gtk.Dialog): else: return None -class CollapsedEntry(Frame): +class CollapsedEntry(CanvasRoundBox): _DATE_COL_WIDTH = style.zoom(100) _BUDDIES_COL_WIDTH = style.zoom(50) def __init__(self, jobject): - Frame.__init__(self) + CanvasRoundBox.__init__(self) self.props.box_height = style.zoom(75) self.props.spacing = style.DEFAULT_SPACING self.props.border_color = style.COLOR_BLACK.get_int() diff --git a/sugar/graphics/palette.py b/sugar/graphics/palette.py index 45ac057..368a0f6 100644 --- a/sugar/graphics/palette.py +++ b/sugar/graphics/palette.py @@ -36,7 +36,40 @@ _RIGHT_TOP = 5 _TOP_LEFT = 6 _TOP_RIGHT = 7 -class Palette(gobject.GObject): + +# Helper function to find the gap position and size of widget a +def _calculate_gap(a, b): + # Test for each side if the palette and invoker are + # adjacent to each other. + gap = True + + if a.y + a.height == b.y: + gap_side = gtk.POS_BOTTOM + elif a.x + a.width == b.x: + gap_side = gtk.POS_RIGHT + elif a.x == b.x + b.width: + gap_side = gtk.POS_LEFT + elif a.y == b.y + b.height: + gap_side = gtk.POS_TOP + else: + gap = False + + if gap: + if gap_side == gtk.POS_BOTTOM or gap_side == gtk.POS_TOP: + gap_start = min(a.width, max(0, b.x - a.x)) + gap_size = max(0, min(a.width, + (b.x + b.width) - a.x) - gap_start) + elif gap_side == gtk.POS_RIGHT or gap_side == gtk.POS_LEFT: + gap_start = min(a.height, max(0, b.y - a.y)) + gap_size = max(0, min(a.height, + (b.y + b.height) - a.y) - gap_start) + + if gap and gap_size > 0: + return (gap_side, gap_start, gap_size) + else: + return False + +class Palette(gtk.Window): DEFAULT = 0 AT_CURSOR = 1 AROUND = 2 @@ -54,7 +87,9 @@ class Palette(gobject.GObject): 'invoker' : (object, None, None, gobject.PARAM_READWRITE), 'position' : (gobject.TYPE_INT, None, None, 0, 6, - 0, gobject.PARAM_READWRITE) + 0, gobject.PARAM_READWRITE), + 'draw-gap' : (bool, None, None, False, + gobject.PARAM_READWRITE) } __gsignals__ = { @@ -65,16 +100,21 @@ class Palette(gobject.GObject): } def __init__(self, label, accel_path=None): - gobject.GObject.__init__(self) + gtk.Window.__init__(self) + + self.set_decorated(False) + self.set_resizable(False) + self.connect('realize', self._realize_cb) self._full_request = [0, 0] self._cursor_x = 0 self._cursor_y = 0 - self._state = self._SECONDARY + self._state = self._PRIMARY self._invoker = None self._group_id = None self._up = False self._position = self.DEFAULT + self._draw_gap = False self._palette_popup_sid = None self._popup_anim = animator.Animator(0.3, 10) @@ -86,60 +126,70 @@ class Palette(gobject.GObject): self._popdown_anim = animator.Animator(0.6, 10) self._popdown_anim.add(_PopdownAnimation(self)) - self._menu = _sugaruiext.Menu() + vbox = gtk.VBox() + vbox.set_border_width(style.DEFAULT_PADDING) - self._primary = _PrimaryMenuItem(label, accel_path) - self._menu.append(self._primary) - self._primary.show() + self._label = gtk.Label() + vbox.pack_start(self._label, False) - self._separator = gtk.SeparatorMenuItem() - self._menu.append(self._separator) + self._secondary_box = gtk.VBox() + vbox.pack_start(self._secondary_box) - self._content = _ContentMenuItem() - self._menu.append(self._content) + self._separator = gtk.HSeparator() + self._secondary_box.pack_start(self._separator) - self._button_bar = _ButtonBarMenuItem() - self._menu.append(self._button_bar) + self._menu_box = gtk.VBox() + self._secondary_box.pack_start(self._menu_box) + self._menu_box.show() - self._menu.connect('enter-notify-event', - self._enter_notify_event_cb) - self._menu.connect('leave-notify-event', - self._leave_notify_event_cb) + self._content = gtk.VBox() + self._secondary_box.pack_start(self._content) + self._content.show() - def is_up(self): - return self._up + self.action_bar = PaletteActionBar() + self._secondary_box.pack_start(self.action_bar) + self.action_bar.show() - def set_primary_text(self, label, accel_path=None): - self._primary.set_label(label, accel_path) + self.add(vbox) + vbox.show() - def append_menu_item(self, item): - self._separator.show() - self._menu.insert(item, len(self._menu.get_children()) - 2) + self.menu = _Menu(self) + self.menu.show() - def insert_menu_item(self, item, index=-1): - self._separator.show() - if index < 0: - self._menu.insert(item, len(self._menu.get_children()) - 2) - else: - self._menu.insert(item, index + 2) + self.connect('enter-notify-event', + self._enter_notify_event_cb) + self.connect('leave-notify-event', + self._leave_notify_event_cb) + + self.set_primary_text(label, accel_path) - def remove_menu_item(self, index): - if index > len(self._menu.get_children()) - 4: - raise ValueError('index %i out of range' % index) - self._menu.remove(self._menu.get_children()[index + 2]) - if len(self._menu.get_children()) == 0: - self._separator.hide() + def is_up(self): + return self._up + + def get_rect(self): + win_x, win_y = self.window.get_origin() + rectangle = self.get_allocation() - def menu_item_count(self): - return len(self._menu.get_children()) - 4 + x = win_x + rectangle.x + y = win_y + rectangle.y + width = rectangle.width + height = rectangle.height + return gtk.gdk.Rectangle(x, y, width, height) + + def set_primary_text(self, label, accel_path=None): + self._label.set_text(label) + self._label.show() + def set_content(self, widget): - self._content.set_widget(widget) - self._content.show() + if len(self._content.get_children()) > 0: + self.remove(self._content.get_children()[0]) + + if widget is not None: + self._content.add(widget) - def append_button(self, button): - self._button_bar.append_button(button) - self._button_bar.show() + self._update_accept_focus() + self._update_separator() def set_group_id(self, group_id): if self._group_id: @@ -154,9 +204,11 @@ class Palette(gobject.GObject): self._invoker = value self._invoker.connect('mouse-enter', self._invoker_mouse_enter_cb) self._invoker.connect('mouse-leave', self._invoker_mouse_leave_cb) - self._invoker.connect('focus-out', self._invoker_focus_out_cb) elif pspec.name == 'position': self._position = value + elif pspec.name == 'draw-gap': + self._draw_gap = value + self.queue_draw() else: raise AssertionError @@ -165,9 +217,57 @@ class Palette(gobject.GObject): return self._invoker elif pspec.name == 'position': return self._position + elif pspec.name == 'draw-gap': + return self._draw_gap else: raise AssertionError + def do_size_allocate(self, allocation): + gtk.Window.do_size_allocate(self, allocation) + self.queue_draw() + + def do_expose_event(self, event): + # We want to draw a border with a beautiful gap + if self._draw_gap: + invoker = self._invoker.get_rect() + palette = self.get_rect() + + gap = _calculate_gap(palette, invoker) + else: + gap = False + + if gap: + self.style.paint_box_gap(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_IN, event.area, self, "palette", + 0, 0, + self.allocation.width, + self.allocation.height, + gap[0], gap[1], gap[2]) + else: + self.style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_IN, event.area, self, "palette", + 0, 0, + self.allocation.width, + self.allocation.height) + + # Fall trough to the container expose handler. + # (Leaving out the window expose handler which redraws everything) + gtk.Bin.do_expose_event(self, event) + + def _update_separator(self): + visible = len(self.menu.get_children()) > 0 or \ + len(self._content.get_children()) > 0 + self._separator.props.visible = visible + + def _update_accept_focus(self): + accept_focus = len(self._content.get_children()) + if self.window: + self.window.set_accept_focus(accept_focus) + + def _realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self._update_accept_focus() + def _in_screen(self, x, y): [width, height] = self._full_request screen_area = self._invoker.get_screen_area() @@ -182,7 +282,7 @@ class Palette(gobject.GObject): if inv_rect == None: inv_rect = self._invoker.get_rect() - palette_width, palette_height = self._menu.size_request() + palette_width, palette_height = self.size_request() x = inv_rect.x + inv_rect.width * invoker_halign + \ palette_width * palette_halign @@ -241,12 +341,12 @@ class Palette(gobject.GObject): def _update_full_request(self): state = self._state - self._menu.set_size_request(-1, -1) + self.set_size_request(-1, -1) self._set_state(self._SECONDARY) - self._full_request = self._menu.size_request() + self._full_request = self.size_request() - self._menu.set_size_request(self._full_request[0], -1) + self.set_size_request(self._full_request[0], -1) self._set_state(state) @@ -282,7 +382,7 @@ class Palette(gobject.GObject): elif position == self.TOP: x, y = self._get_top_position() - self._menu.popup(x, y) + self.move(x, y) def _show(self): if self._up: @@ -291,11 +391,12 @@ class Palette(gobject.GObject): self._update_cursor_position() self._update_full_request() - self._invoker.connect_to_parent() - self._palette_popup_sid = _palette_observer.connect('popup', - self._palette_observer_popup_cb) + self._palette_popup_sid = _palette_observer.connect( + 'popup', self._palette_observer_popup_cb) self._update_position() + self.menu.set_active(True) + self.show() self._up = True _palette_observer.emit('popup', self) @@ -305,7 +406,8 @@ class Palette(gobject.GObject): if not self._palette_popup_sid is None: _palette_observer.disconnect(self._palette_popup_sid) self._palette_popup_sid = None - self._menu.popdown() + self.menu.set_active(False) + self.hide() self._up = False self.emit('popdown') @@ -329,25 +431,11 @@ class Palette(gobject.GObject): return if state == self._PRIMARY: - self._primary.show() - for menu_item in self._menu.get_children()[1:]: - menu_item.hide() + self.menu.unembed() + self._secondary_box.hide() elif state == self._SECONDARY: - middle_menu_items = self._menu.get_children() - middle_menu_items = middle_menu_items[2:len(middle_menu_items) - 2] - if middle_menu_items or \ - not self._content.is_empty() or \ - not self._button_bar.is_empty(): - self._separator.show() - - for menu_item in middle_menu_items: - menu_item.show() - - if not self._content.is_empty(): - self._content.show() - - if not self._button_bar.is_empty(): - self._button_bar.show() + self.menu.embed(self._menu_box) + self._secondary_box.show() self._state = state @@ -357,68 +445,54 @@ class Palette(gobject.GObject): def _invoker_mouse_leave_cb(self, invoker): self.popdown() - def _invoker_focus_out_cb(self, invoker): - self._hide() - def _enter_notify_event_cb(self, widget, event): - if event.detail == gtk.gdk.NOTIFY_NONLINEAR: + if event.detail != gtk.gdk.NOTIFY_INFERIOR: self._popdown_anim.stop() self._secondary_anim.start() def _leave_notify_event_cb(self, widget, event): - if event.detail == gtk.gdk.NOTIFY_NONLINEAR: + if event.detail != gtk.gdk.NOTIFY_INFERIOR: self.popdown() def _palette_observer_popup_cb(self, observer, palette): if self != palette: self._hide() -class _PrimaryMenuItem(gtk.MenuItem): - def __init__(self, label, accel_path): - gtk.MenuItem.__init__(self) - self.set_border_width(style.DEFAULT_PADDING) - self._set_label(label, accel_path) +class PaletteActionBar(gtk.HButtonBox): + def add_action(label, icon_name=None): + button = Button(label) - def set_label(self, label, accel_path): - self.remove(self._label) - self._set_label(label, accel_path) + if icon_name: + icon = Icon(icon_name) + button.set_image(icon) + icon.show() - def _set_label(self, label, accel_path): - self._label = gtk.AccelLabel(label) - self._label.set_accel_widget(self) + self.pack_start(button) + button.show() - if accel_path: - self.set_accel_path(accel_path) - self._label.set_alignment(0.0, 0.5) +class _Menu(_sugaruiext.Menu): + __gtype_name__ = 'SugarPaletteMenu' - self.add(self._label) - self._label.show() - -class _ContentMenuItem(gtk.MenuItem): - def __init__(self): - gtk.MenuItem.__init__(self) - - def set_widget(self, widget): - if self.child: - self.remove(self.child) - self.add(widget) - - def is_empty(self): - return self.child is None or not self.child.props.visible + def __init__(self, palette): + _sugaruiext.Menu.__init__(self) + self._palette = palette -class _ButtonBarMenuItem(gtk.MenuItem): - def __init__(self): - gtk.MenuItem.__init__(self) + def do_insert(self, item, position): + _sugaruiext.Menu.do_insert(self, item, position) + self._palette._update_separator() - self._hbar = gtk.HButtonBox() - self.add(self._hbar) - self._hbar.show() + def do_expose_event(self, event): + # Ignore the Menu expose, just do the MenuShell expose to prevent any + # border from being drawn here. A border is drawn by the palette object + # around everything. + gtk.MenuShell.do_expose_event(self, event) - def append_button(self, button): - self._hbar.pack_start(button) + def do_grab_notify(self, was_grabbed): + # Ignore grab_notify as the menu would close otherwise + pass - def is_empty(self): - return len(self._hbar.get_children()) == 0 + def do_deactivate(self): + self._palette._hide() class _PopupAnimation(animator.Animation): def __init__(self, palette): @@ -469,13 +543,6 @@ class Invoker(gobject.GObject): height = gtk.gdk.screen_height() return gtk.gdk.Rectangle(0, 0, width, height) - def connect_to_parent(self): - window = self.get_toplevel() - window.connect('focus-out-event', self._window_focus_out_event_cb) - - def _window_focus_out_event_cb(self, widget, event): - self.emit('focus-out') - class WidgetInvoker(Invoker): def __init__(self, widget): Invoker.__init__(self) @@ -495,6 +562,37 @@ class WidgetInvoker(Invoker): return gtk.gdk.Rectangle(x, y, width, height) + def draw_invoker_rect(self, event, palette): + style = self._widget.style + if palette.is_up(): + gap = _calculate_gap(self.get_rect(), palette.get_rect()) + + if gap: + style.paint_box_gap(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_IN, event.area, self._widget, + "palette-invoker", + self._widget.allocation.x, + self._widget.allocation.y, + self._widget.allocation.width, + self._widget.allocation.height, + gap[0], gap[1], gap[2]) + else: + style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_IN, event.area, self._widget, + "palette-invoker", + self._widget.allocation.x, + self._widget.allocation.y, + self._widget.allocation.width, + self._widget.allocation.height) + else: + style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_NONE, event.area, self._widget, + "palette-invoker", + self._widget.allocation.x, + self._widget.allocation.y, + self._widget.allocation.width, + self._widget.allocation.height) + def _enter_notify_event_cb(self, widget, event): self.emit('mouse-enter') diff --git a/sugar/graphics/radiotoolbutton.py b/sugar/graphics/radiotoolbutton.py index 94ff6ba..fb584ee 100644 --- a/sugar/graphics/radiotoolbutton.py +++ b/sugar/graphics/radiotoolbutton.py @@ -22,6 +22,8 @@ from sugar.graphics.icon import Icon from sugar.graphics.palette import Palette, WidgetInvoker class RadioToolButton(gtk.RadioToolButton): + __gtype_name__ = "SugarRadioToolButton" + def __init__(self, named_icon=None, group=None): gtk.RadioToolButton.__init__(self, group=group) self._palette = None @@ -38,9 +40,25 @@ class RadioToolButton(gtk.RadioToolButton): def set_palette(self, palette): self._palette = palette self._palette.props.invoker = WidgetInvoker(self.child) + self._palette.props.draw_gap = True + + self._palette.connect("popup", self._palette_changed) + self._palette.connect("popdown", self._palette_changed) def set_tooltip(self, text): self._palette = Palette(text) self._palette.props.invoker = WidgetInvoker(self.child) + + def do_expose_event(self, event): + if self._palette and self._palette.props.draw_gap: + if self._palette.is_up() or self.child.state == gtk.STATE_PRELIGHT: + invoker = self._palette.props.invoker + invoker.draw_invoker_rect(event, self._palette) + + gtk.RadioToolButton.do_expose_event(self, event) + + def _palette_changed(self, palette): + # Force a redraw to update the invoker rectangle + self.queue_draw() palette = property(get_palette, set_palette) diff --git a/sugar/graphics/style.py b/sugar/graphics/style.py index 55b4a4b..a5186da 100644 --- a/sugar/graphics/style.py +++ b/sugar/graphics/style.py @@ -81,6 +81,12 @@ class Color(object): return (r, g, b) + def get_svg(self): + if self._a == 0.0: + return 'none' + else: + return self.get_html() + _XO_DPI = 200.0 _FOCUS_LINE_WIDTH = 2 @@ -113,6 +119,7 @@ TOOLBOX_TAB_LABEL_WIDTH = zoom(150 - 15 * 2) COLOR_BLACK = Color('#000000') COLOR_WHITE = Color('#FFFFFF') +COLOR_TRANSPARENT = Color('#FFFFFF', alpha=0.0) COLOR_PANEL_GREY = Color('#C0C0C0') COLOR_SELECTION_GREY = Color('#A6A6A6') COLOR_INACTIVE_FILL = Color('#9D9FA1') diff --git a/sugar/graphics/toggletoolbutton.py b/sugar/graphics/toggletoolbutton.py index 3684e9c..41050e2 100644 --- a/sugar/graphics/toggletoolbutton.py +++ b/sugar/graphics/toggletoolbutton.py @@ -21,6 +21,8 @@ from sugar.graphics.icon import Icon from sugar.graphics.palette import Palette, WidgetInvoker class ToggleToolButton(gtk.ToggleToolButton): + __gtype_name__ = "SugarToggleToolButton" + def __init__(self, named_icon=None): gtk.ToggleToolButton.__init__(self) self._palette = None @@ -37,9 +39,25 @@ class ToggleToolButton(gtk.ToggleToolButton): def set_palette(self, palette): self._palette = palette self._palette.props.invoker = WidgetInvoker(self.child) + self._palette.props.draw_gap = True + + self._palette.connect("popup", self._palette_changed) + self._palette.connect("popdown", self._palette_changed) def set_tooltip(self, text): self._palette = Palette(text) self._palette.props.invoker = WidgetInvoker(self.child) + def do_expose_event(self, event): + if self._palette and self._palette.props.draw_gap: + if self._palette.is_up() or self.child.state == gtk.STATE_PRELIGHT: + invoker = self._palette.props.invoker + invoker.draw_invoker_rect(event, self._palette) + + gtk.ToggleToolButton.do_expose_event(self, event) + + def _palette_changed(self, palette): + # Force a redraw to update the invoker rectangle + self.queue_draw() + palette = property(get_palette, set_palette) diff --git a/sugar/graphics/toolbutton.py b/sugar/graphics/toolbutton.py index e5d90ab..52a5d62 100644 --- a/sugar/graphics/toolbutton.py +++ b/sugar/graphics/toolbutton.py @@ -23,6 +23,7 @@ from sugar.graphics.icon import Icon from sugar.graphics.palette import Palette, WidgetInvoker class ToolButton(gtk.ToolButton): + __gtype_name__ = "SugarToolButton" def __init__(self, icon_name=None): gtk.ToolButton.__init__(self) @@ -41,12 +42,28 @@ class ToolButton(gtk.ToolButton): def set_palette(self, palette): self._palette = palette self._palette.props.invoker = WidgetInvoker(self.child) + self._palette.props.draw_gap = True + + self._palette.connect("popup", self._palette_changed) + self._palette.connect("popdown", self._palette_changed) def set_tooltip(self, text): self.set_palette(Palette(text)) + def do_expose_event(self, event): + if self._palette and self._palette.props.draw_gap: + if self._palette.is_up() or self.child.state == gtk.STATE_PRELIGHT: + invoker = self._palette.props.invoker + invoker.draw_invoker_rect(event, self._palette) + + gtk.ToolButton.do_expose_event(self, event) + def _button_clicked_cb(self, widget): if self._palette: self._palette.popdown(True) + def _palette_changed(self, palette): + # Force a redraw to update the invoker rectangle + self.queue_draw() + palette = property(get_palette, set_palette) |