First version of the ClipboardService. Added support for showing the progress of a pdf download in the clipboard.
This commit is contained in:
		
							parent
							
								
									cf508c1d22
								
							
						
					
					
						commit
						23565cfd48
					
				@ -18,6 +18,7 @@ from gettext import gettext as _
 | 
			
		||||
import gtk
 | 
			
		||||
import gtkmozembed
 | 
			
		||||
import logging
 | 
			
		||||
import dbus
 | 
			
		||||
 | 
			
		||||
import _sugar
 | 
			
		||||
from sugar.activity import ActivityFactory
 | 
			
		||||
@ -105,11 +106,34 @@ def start():
 | 
			
		||||
	style.load_stylesheet(web.stylesheet)
 | 
			
		||||
	
 | 
			
		||||
	chandler = _sugar.get_browser_chandler()
 | 
			
		||||
	chandler.connect('handle-content', handle_content_cb)
 | 
			
		||||
	chandler.connect('download-started', download_started_cb)
 | 
			
		||||
	chandler.connect('download-completed', download_completed_cb)
 | 
			
		||||
	chandler.connect('download-cancelled', download_started_cb)
 | 
			
		||||
	chandler.connect('download-progress', download_progress_cb)
 | 
			
		||||
 | 
			
		||||
def stop():
 | 
			
		||||
	gtkmozembed.pop_startup()
 | 
			
		||||
 | 
			
		||||
def handle_content_cb(chandler, url, mimeType, tmpFileName):
 | 
			
		||||
	activity = ActivityFactory.create("org.laptop.sugar.Xbook")
 | 
			
		||||
	activity.execute("open_document", [tmpFileName])
 | 
			
		||||
def download_started_cb(chandler, url, mimeType, tmpFileName):	
 | 
			
		||||
	bus = dbus.SessionBus()
 | 
			
		||||
	proxy_obj = bus.get_object('org.laptop.Clipboard', '/org/laptop/Clipboard')
 | 
			
		||||
	iface = dbus.Interface(proxy_obj, 'org.laptop.Clipboard')
 | 
			
		||||
	iface.add_object(mimeType, tmpFileName)
 | 
			
		||||
 | 
			
		||||
def download_completed_cb(chandler, tmpFileName):
 | 
			
		||||
	bus = dbus.SessionBus()
 | 
			
		||||
	proxy_obj = bus.get_object('org.laptop.Clipboard', '/org/laptop/Clipboard')
 | 
			
		||||
	iface = dbus.Interface(proxy_obj, 'org.laptop.Clipboard')
 | 
			
		||||
	iface.update_object_state(tmpFileName, 100)
 | 
			
		||||
 | 
			
		||||
def download_cancelled_cb(chandler, tmpFileName):
 | 
			
		||||
	bus = dbus.SessionBus()
 | 
			
		||||
	proxy_obj = bus.get_object('org.laptop.Clipboard', '/org/laptop/Clipboard')
 | 
			
		||||
	iface = dbus.Interface(proxy_obj, 'org.laptop.Clipboard')
 | 
			
		||||
	iface.delete_object(tmpFileName, 100)
 | 
			
		||||
 | 
			
		||||
def download_progress_cb(chandler, tmpFileName, progress):
 | 
			
		||||
	bus = dbus.SessionBus()
 | 
			
		||||
	proxy_obj = bus.get_object('org.laptop.Clipboard', '/org/laptop/Clipboard')
 | 
			
		||||
	iface = dbus.Interface(proxy_obj, 'org.laptop.Clipboard')
 | 
			
		||||
	iface.update_object_state(tmpFileName, progress)
 | 
			
		||||
 | 
			
		||||
@ -64,6 +64,7 @@ lib/threadframe/Makefile
 | 
			
		||||
services/Makefile
 | 
			
		||||
services/presence/Makefile
 | 
			
		||||
services/nm/Makefile
 | 
			
		||||
services/clipboard/Makefile
 | 
			
		||||
shell/Makefile
 | 
			
		||||
shell/conf/Makefile
 | 
			
		||||
shell/data/Makefile
 | 
			
		||||
 | 
			
		||||
@ -35,27 +35,39 @@ GSugarDownload::Init (nsIURI *aSource,
 | 
			
		||||
NS_IMETHODIMP 
 | 
			
		||||
GSugarDownload::OnStateChange (nsIWebProgress *aWebProgress, nsIRequest *aRequest,
 | 
			
		||||
			    PRUint32 aStateFlags, nsresult aStatus)
 | 
			
		||||
{
 | 
			
		||||
	nsCString url;
 | 
			
		||||
	nsCString mimeType;
 | 
			
		||||
	nsCString targetURI;
 | 
			
		||||
	
 | 
			
		||||
	if ((((aStateFlags & STATE_IS_REQUEST) &&
 | 
			
		||||
{	
 | 
			
		||||
	SugarBrowserChandler *browser_chandler = sugar_get_browser_chandler();
 | 
			
		||||
		
 | 
			
		||||
	if (((aStateFlags & STATE_IS_REQUEST) &&
 | 
			
		||||
	     (aStateFlags & STATE_IS_NETWORK) &&
 | 
			
		||||
	     (aStateFlags & STATE_STOP)) ||
 | 
			
		||||
	    aStateFlags == STATE_STOP) &&
 | 
			
		||||
	    NS_SUCCEEDED (aStatus)) {
 | 
			
		||||
	     (aStateFlags & STATE_START)) ||
 | 
			
		||||
	    aStateFlags == STATE_START) {
 | 
			
		||||
	
 | 
			
		||||
		nsCString url;
 | 
			
		||||
		nsCString mimeType;
 | 
			
		||||
	
 | 
			
		||||
		mMIMEInfo->GetMIMEType(mimeType);
 | 
			
		||||
		mSource->GetSpec(url);
 | 
			
		||||
 | 
			
		||||
		SugarBrowserChandler *browser_chandler = sugar_get_browser_chandler();
 | 
			
		||||
		sugar_browser_chandler_handle_content(browser_chandler,
 | 
			
		||||
											  url.get(),
 | 
			
		||||
											  mimeType.get(),
 | 
			
		||||
											  mTargetFileName.get());
 | 
			
		||||
		sugar_browser_chandler_download_started(browser_chandler,
 | 
			
		||||
												url.get(),
 | 
			
		||||
												mimeType.get(),
 | 
			
		||||
												mTargetFileName.get());
 | 
			
		||||
 | 
			
		||||
	} else if (((aStateFlags & STATE_IS_REQUEST) &&
 | 
			
		||||
	     (aStateFlags & STATE_IS_NETWORK) &&
 | 
			
		||||
	     (aStateFlags & STATE_STOP)) ||
 | 
			
		||||
	    aStateFlags == STATE_STOP) {
 | 
			
		||||
		
 | 
			
		||||
		if (NS_SUCCEEDED (aStatus)) {
 | 
			
		||||
			sugar_browser_chandler_download_completed(browser_chandler,
 | 
			
		||||
													  mTargetFileName.get());
 | 
			
		||||
		} else {
 | 
			
		||||
			sugar_browser_chandler_download_cancelled(browser_chandler,
 | 
			
		||||
													  mTargetFileName.get());
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
 | 
			
		||||
	return NS_OK; 
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -79,7 +91,15 @@ GSugarDownload::OnProgressChange64 (nsIWebProgress *aWebProgress,
 | 
			
		||||
				 PRInt64 aMaxSelfProgress,
 | 
			
		||||
				 PRInt64 aCurTotalProgress,
 | 
			
		||||
				 PRInt64 aMaxTotalProgress)
 | 
			
		||||
{
 | 
			
		||||
{	
 | 
			
		||||
	SugarBrowserChandler *browser_chandler = sugar_get_browser_chandler();
 | 
			
		||||
	PRInt32 percentComplete =
 | 
			
		||||
		(PRInt32)(((float)aCurSelfProgress / (float)aMaxSelfProgress) * 100.0);
 | 
			
		||||
 | 
			
		||||
	sugar_browser_chandler_update_progress(browser_chandler,
 | 
			
		||||
										   mTargetFileName.get(),
 | 
			
		||||
										   percentComplete);
 | 
			
		||||
 | 
			
		||||
	return NS_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,10 @@
 | 
			
		||||
#include "sugar-browser-chandler.h"
 | 
			
		||||
 | 
			
		||||
enum {
 | 
			
		||||
  HANDLE_CONTENT,
 | 
			
		||||
  DOWNLOAD_STARTED,
 | 
			
		||||
  DOWNLOAD_COMPLETED,
 | 
			
		||||
  DOWNLOAD_CANCELLED,
 | 
			
		||||
  DOWNLOAD_PROGRESS,
 | 
			
		||||
  LAST_SIGNAL
 | 
			
		||||
};
 | 
			
		||||
static guint signals[LAST_SIGNAL] = { 0 };
 | 
			
		||||
@ -19,8 +22,8 @@ sugar_browser_chandler_init(SugarBrowserChandler *browserChandler)
 | 
			
		||||
static void
 | 
			
		||||
sugar_browser_chandler_class_init(SugarBrowserChandlerClass *browser_chandler_class)
 | 
			
		||||
{
 | 
			
		||||
	signals[HANDLE_CONTENT] =
 | 
			
		||||
		g_signal_new ("handle-content",
 | 
			
		||||
	signals[DOWNLOAD_STARTED] =
 | 
			
		||||
		g_signal_new ("download-started",
 | 
			
		||||
					  G_OBJECT_CLASS_TYPE (browser_chandler_class),
 | 
			
		||||
		  			  G_SIGNAL_RUN_LAST,
 | 
			
		||||
		  			  G_STRUCT_OFFSET (SugarBrowserChandlerClass, handle_content),
 | 
			
		||||
@ -30,6 +33,37 @@ sugar_browser_chandler_class_init(SugarBrowserChandlerClass *browser_chandler_cl
 | 
			
		||||
					  G_TYPE_STRING,
 | 
			
		||||
					  G_TYPE_STRING,
 | 
			
		||||
					  G_TYPE_STRING);
 | 
			
		||||
					  
 | 
			
		||||
	signals[DOWNLOAD_COMPLETED] =
 | 
			
		||||
		g_signal_new ("download-completed",
 | 
			
		||||
					  G_OBJECT_CLASS_TYPE (browser_chandler_class),
 | 
			
		||||
		  			  G_SIGNAL_RUN_LAST,
 | 
			
		||||
		  			  G_STRUCT_OFFSET (SugarBrowserChandlerClass, handle_content),
 | 
			
		||||
					  NULL, NULL,
 | 
			
		||||
					  sugar_marshal_VOID__STRING,
 | 
			
		||||
					  G_TYPE_NONE, 1,
 | 
			
		||||
					  G_TYPE_STRING);
 | 
			
		||||
					  
 | 
			
		||||
	signals[DOWNLOAD_CANCELLED] =
 | 
			
		||||
		g_signal_new ("download-cancelled",
 | 
			
		||||
					  G_OBJECT_CLASS_TYPE (browser_chandler_class),
 | 
			
		||||
		  			  G_SIGNAL_RUN_LAST,
 | 
			
		||||
		  			  G_STRUCT_OFFSET (SugarBrowserChandlerClass, handle_content),
 | 
			
		||||
					  NULL, NULL,
 | 
			
		||||
					  sugar_marshal_VOID__STRING,
 | 
			
		||||
					  G_TYPE_NONE, 1,
 | 
			
		||||
					  G_TYPE_STRING);
 | 
			
		||||
					  
 | 
			
		||||
	signals[DOWNLOAD_PROGRESS] =
 | 
			
		||||
		g_signal_new ("download-progress",
 | 
			
		||||
					  G_OBJECT_CLASS_TYPE (browser_chandler_class),
 | 
			
		||||
		  			  G_SIGNAL_RUN_LAST,
 | 
			
		||||
		  			  G_STRUCT_OFFSET (SugarBrowserChandlerClass, handle_content),
 | 
			
		||||
					  NULL, NULL,
 | 
			
		||||
					  sugar_marshal_VOID__STRING_INT,
 | 
			
		||||
					  G_TYPE_NONE, 2,
 | 
			
		||||
					  G_TYPE_STRING,
 | 
			
		||||
					  G_TYPE_INT);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SugarBrowserChandler *
 | 
			
		||||
@ -42,15 +76,46 @@ sugar_get_browser_chandler()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
sugar_browser_chandler_handle_content (SugarBrowserChandler *browser_chandler, 
 | 
			
		||||
									   const char *url, 
 | 
			
		||||
									   const char *mime_type, 
 | 
			
		||||
									   const char *tmp_file_name)
 | 
			
		||||
{	
 | 
			
		||||
sugar_browser_chandler_download_started (SugarBrowserChandler *browser_chandler,
 | 
			
		||||
										 const char *url,
 | 
			
		||||
										 const char *mime_type,
 | 
			
		||||
										 const char *tmp_file_name)
 | 
			
		||||
{
 | 
			
		||||
	g_signal_emit(browser_chandler, 
 | 
			
		||||
				  signals[HANDLE_CONTENT],
 | 
			
		||||
				  signals[DOWNLOAD_STARTED],
 | 
			
		||||
                  0 /* details */, 
 | 
			
		||||
                  url,
 | 
			
		||||
                  mime_type,
 | 
			
		||||
                  tmp_file_name);          
 | 
			
		||||
                  tmp_file_name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
sugar_browser_chandler_download_completed (SugarBrowserChandler *browser_chandler,
 | 
			
		||||
										   const char *tmp_file_name)
 | 
			
		||||
{
 | 
			
		||||
	g_signal_emit(browser_chandler, 
 | 
			
		||||
				  signals[DOWNLOAD_COMPLETED],
 | 
			
		||||
                  0 /* details */, 
 | 
			
		||||
                  tmp_file_name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void sugar_browser_chandler_download_cancelled (SugarBrowserChandler *browser_chandler,
 | 
			
		||||
												const char *tmp_file_name)
 | 
			
		||||
{
 | 
			
		||||
	g_signal_emit(browser_chandler, 
 | 
			
		||||
				  signals[DOWNLOAD_CANCELLED],
 | 
			
		||||
                  0 /* details */, 
 | 
			
		||||
                  tmp_file_name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
sugar_browser_chandler_update_progress (SugarBrowserChandler *browser_chandler,
 | 
			
		||||
										const char *tmp_file_name,
 | 
			
		||||
										const int percent)
 | 
			
		||||
{
 | 
			
		||||
	g_signal_emit(browser_chandler, 
 | 
			
		||||
				  signals[DOWNLOAD_PROGRESS],
 | 
			
		||||
                  0 /* details */, 
 | 
			
		||||
                  tmp_file_name,
 | 
			
		||||
                  percent);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,10 +29,17 @@ struct _SugarBrowserChandlerClass {
 | 
			
		||||
 | 
			
		||||
GType				  sugar_browser_chandler_get_type	    (void);
 | 
			
		||||
SugarBrowserChandler *sugar_get_browser_chandler		    (void);
 | 
			
		||||
void            	  sugar_browser_chandler_handle_content (SugarBrowserChandler *chandler,
 | 
			
		||||
															 const char *url, 
 | 
			
		||||
															 const char *mime_type,
 | 
			
		||||
															 const char *tmp_file_name);
 | 
			
		||||
void            	  sugar_browser_chandler_download_started (SugarBrowserChandler *chandler,
 | 
			
		||||
															   const char *url,
 | 
			
		||||
															   const char *mime_type,
 | 
			
		||||
															   const char *tmp_file_name);
 | 
			
		||||
void            	  sugar_browser_chandler_download_completed (SugarBrowserChandler *chandler,
 | 
			
		||||
															   const char *tmp_file_name);
 | 
			
		||||
void            	  sugar_browser_chandler_download_cancelled (SugarBrowserChandler *chandler,
 | 
			
		||||
																 const char *tmp_file_name);
 | 
			
		||||
void            	  sugar_browser_chandler_update_progress (SugarBrowserChandler *chandler,
 | 
			
		||||
															  const char *tmp_file_name,
 | 
			
		||||
															  const int percent);
 | 
			
		||||
 | 
			
		||||
G_END_DECLS
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -78,7 +78,7 @@ sugar_browser_startup(void)
 | 
			
		||||
						  PR_TRUE, getter_AddRefs(file));
 | 
			
		||||
	NS_ENSURE_TRUE(file, FALSE);
 | 
			
		||||
 | 
			
		||||
	rv = prefService->ReadUserPrefs (file);                                                                              
 | 
			
		||||
	rv = prefService->ReadUserPrefs (file);
 | 
			
		||||
	if (NS_FAILED(rv)) {
 | 
			
		||||
		g_warning ("failed to read default preferences, error: %x", rv);
 | 
			
		||||
		return FALSE;
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
VOID:OBJECT,STRING,LONG,LONG
 | 
			
		||||
VOID:OBJECT,LONG
 | 
			
		||||
VOID:STRING,STRING,STRING
 | 
			
		||||
VOID:STRING,INT
 | 
			
		||||
VOID:STRING
 | 
			
		||||
 | 
			
		||||
@ -1 +1 @@
 | 
			
		||||
SUBDIRS = presence nm
 | 
			
		||||
SUBDIRS = presence nm clipboard
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										76
									
								
								services/clipboard/ClipboardService.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								services/clipboard/ClipboardService.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
			
		||||
# vi: ts=4 ai noet
 | 
			
		||||
#
 | 
			
		||||
# Copyright (C) 2006, Red Hat, Inc.
 | 
			
		||||
#
 | 
			
		||||
# This program is free software; you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU General Public License as published by
 | 
			
		||||
# the Free Software Foundation; either version 2 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with this program; if not, write to the Free Software
 | 
			
		||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
import gobject
 | 
			
		||||
import dbus
 | 
			
		||||
import dbus.service
 | 
			
		||||
from sugar import env
 | 
			
		||||
 | 
			
		||||
_CLIPBOARD_SERVICE = "org.laptop.Clipboard"
 | 
			
		||||
_CLIPBOARD_DBUS_INTERFACE = "org.laptop.Clipboard"
 | 
			
		||||
_CLIPBOARD_OBJECT_PATH = "/org/laptop/Clipboard"
 | 
			
		||||
 | 
			
		||||
class ClipboardDBusServiceHelper(dbus.service.Object):
 | 
			
		||||
	def __init__(self, parent):
 | 
			
		||||
		self._parent = parent
 | 
			
		||||
 | 
			
		||||
		bus = dbus.SessionBus()
 | 
			
		||||
		bus_name = dbus.service.BusName(_CLIPBOARD_DBUS_INTERFACE, bus=bus)
 | 
			
		||||
		dbus.service.Object.__init__(self, bus_name, _CLIPBOARD_OBJECT_PATH)
 | 
			
		||||
		
 | 
			
		||||
	@dbus.service.method(_CLIPBOARD_DBUS_INTERFACE,
 | 
			
		||||
						 in_signature="ss", out_signature="")
 | 
			
		||||
	def add_object(self, mimeType, fileName):
 | 
			
		||||
		logging.debug('Added object of type ' + mimeType + ' with path at ' + fileName)
 | 
			
		||||
		self.object_added(mimeType, fileName)
 | 
			
		||||
 | 
			
		||||
	@dbus.service.method(_CLIPBOARD_DBUS_INTERFACE,
 | 
			
		||||
						 in_signature="s", out_signature="")
 | 
			
		||||
	def delete_object(self, fileName):
 | 
			
		||||
		logging.debug('Deleted object with path at ' + fileName)
 | 
			
		||||
		self.object_deleted(fileName)
 | 
			
		||||
 | 
			
		||||
	@dbus.service.method(_CLIPBOARD_DBUS_INTERFACE,
 | 
			
		||||
						 in_signature="si", out_signature="")
 | 
			
		||||
	def update_object_state(self, fileName, percent):
 | 
			
		||||
		logging.debug('Updated object with path at ' + fileName + ' with percent ' + str(percent))
 | 
			
		||||
		self.object_state_updated(fileName, percent)
 | 
			
		||||
 | 
			
		||||
	@dbus.service.signal(_CLIPBOARD_DBUS_INTERFACE, signature="ss")
 | 
			
		||||
	def object_added(self, mimeType, fileName):
 | 
			
		||||
		pass
 | 
			
		||||
 | 
			
		||||
	@dbus.service.signal(_CLIPBOARD_DBUS_INTERFACE, signature="s")
 | 
			
		||||
	def object_deleted(self, fileName):
 | 
			
		||||
		pass
 | 
			
		||||
 | 
			
		||||
	@dbus.service.signal(_CLIPBOARD_DBUS_INTERFACE, signature="si")
 | 
			
		||||
	def object_state_updated(self, fileName, percent):
 | 
			
		||||
		pass
 | 
			
		||||
 | 
			
		||||
class ClipboardService(object):
 | 
			
		||||
	def __init__(self):
 | 
			
		||||
		self._dbus_helper = ClipboardDBusServiceHelper(self)
 | 
			
		||||
 | 
			
		||||
	def run(self):
 | 
			
		||||
		loop = gobject.MainLoop()
 | 
			
		||||
		try:
 | 
			
		||||
			loop.run()
 | 
			
		||||
		except KeyboardInterrupt:
 | 
			
		||||
			print 'Ctrl+C pressed, exiting...'
 | 
			
		||||
							
								
								
									
										15
									
								
								services/clipboard/Makefile.am
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								services/clipboard/Makefile.am
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
servicedir = $(datadir)/sugar/services
 | 
			
		||||
service_in_files = org.laptop.Clipboard.service.in
 | 
			
		||||
service_DATA = $(service_in_files:.service.in=.service)
 | 
			
		||||
 | 
			
		||||
$(service_DATA): $(service_in_files) Makefile
 | 
			
		||||
	@sed -e "s|\@bindir\@|$(bindir)|" $< > $@
 | 
			
		||||
 | 
			
		||||
sugardir = $(pkgdatadir)/services/clipboard
 | 
			
		||||
sugar_PYTHON =			\
 | 
			
		||||
	__init__.py			\
 | 
			
		||||
	ClipboardService.py
 | 
			
		||||
 | 
			
		||||
bin_SCRIPTS = sugar-clipboard
 | 
			
		||||
 | 
			
		||||
EXTRA_DIST = $(service_in_files) $(bin_SCRIPTS)
 | 
			
		||||
							
								
								
									
										0
									
								
								services/clipboard/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								services/clipboard/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										4
									
								
								services/clipboard/org.laptop.Clipboard.service.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								services/clipboard/org.laptop.Clipboard.service.in
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
[D-BUS Service]
 | 
			
		||||
Name = org.laptop.Clipboard
 | 
			
		||||
Exec = @bindir@/sugar-clipboard
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										44
									
								
								services/clipboard/sugar-clipboard
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										44
									
								
								services/clipboard/sugar-clipboard
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
#!/usr/bin/python
 | 
			
		||||
# vi: ts=4 ai noet 
 | 
			
		||||
#
 | 
			
		||||
# Copyright (C) 2006, Red Hat, Inc.
 | 
			
		||||
#
 | 
			
		||||
# This program is free software; you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU General Public License as published by
 | 
			
		||||
# the Free Software Foundation; either version 2 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with this program; if not, write to the Free Software
 | 
			
		||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from sugar import logger
 | 
			
		||||
logger.start('clipboard')
 | 
			
		||||
 | 
			
		||||
import gobject
 | 
			
		||||
import pygtk
 | 
			
		||||
pygtk.require('2.0')
 | 
			
		||||
 | 
			
		||||
import dbus.glib
 | 
			
		||||
 
 | 
			
		||||
from sugar import env
 | 
			
		||||
 | 
			
		||||
sys.path.insert(0, env.get_services_dir())
 | 
			
		||||
 | 
			
		||||
from clipboard.ClipboardService import ClipboardService
 | 
			
		||||
 | 
			
		||||
logging.info('Starting clipboard service.')
 | 
			
		||||
 | 
			
		||||
gobject.threads_init()
 | 
			
		||||
dbus.glib.threads_init()
 | 
			
		||||
 | 
			
		||||
app = ClipboardService()
 | 
			
		||||
app.run()
 | 
			
		||||
							
								
								
									
										33
									
								
								shell/view/ClipboardIcon.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								shell/view/ClipboardIcon.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
from sugar.graphics.menuicon import MenuIcon
 | 
			
		||||
from view.ClipboardMenu import ClipboardMenu
 | 
			
		||||
from sugar.activity import ActivityFactory
 | 
			
		||||
 | 
			
		||||
class ClipboardIcon(MenuIcon):
 | 
			
		||||
	def __init__(self, menu_shell, file_name):
 | 
			
		||||
		MenuIcon.__init__(self, menu_shell, icon_name='stock-written-doc')
 | 
			
		||||
		self._file_name = file_name
 | 
			
		||||
		self._percent = 0
 | 
			
		||||
		self.connect('activated', self._icon_activated_cb)
 | 
			
		||||
		self._menu = None
 | 
			
		||||
		
 | 
			
		||||
	def create_menu(self):
 | 
			
		||||
		self._menu = ClipboardMenu(self._file_name, self._percent)
 | 
			
		||||
		self._menu.connect('action', self._popup_action_cb)
 | 
			
		||||
		return self._menu
 | 
			
		||||
 | 
			
		||||
	def set_percent(self, percent):
 | 
			
		||||
		self._percent = percent
 | 
			
		||||
		if self._menu:
 | 
			
		||||
			self._menu.set_percent(percent)
 | 
			
		||||
 | 
			
		||||
	def _icon_activated_cb(self, icon):
 | 
			
		||||
		activity = ActivityFactory.create("org.laptop.sugar.Xbook")
 | 
			
		||||
		activity.execute("open_document", [self._file_name])
 | 
			
		||||
 | 
			
		||||
	def _popup_action_cb(self, popup, action):
 | 
			
		||||
#		self.popdown()
 | 
			
		||||
#		
 | 
			
		||||
#		if action == ClipboardMenu.ACTION_DELETE:
 | 
			
		||||
#			activity = self._shell.get_current_activity()
 | 
			
		||||
#			activity.invite(ps_buddy)
 | 
			
		||||
		pass
 | 
			
		||||
							
								
								
									
										58
									
								
								shell/view/ClipboardMenu.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								shell/view/ClipboardMenu.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
			
		||||
import gtk
 | 
			
		||||
import gobject
 | 
			
		||||
import hippo
 | 
			
		||||
 | 
			
		||||
from sugar.graphics.menu import Menu
 | 
			
		||||
from sugar.graphics.canvasicon import CanvasIcon
 | 
			
		||||
from sugar.graphics.ClipboardBubble import ClipboardBubble
 | 
			
		||||
from sugar.graphics import style
 | 
			
		||||
 | 
			
		||||
clipboard_bubble = {
 | 
			
		||||
	'fill-color'	: 0x646464FF,
 | 
			
		||||
	'stroke-color'	: 0x646464FF,
 | 
			
		||||
	'progress-color': 0x333333FF,
 | 
			
		||||
	'spacing'		: style.space_unit,
 | 
			
		||||
	'padding'		: style.space_unit * 1.5
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
clipboard_menu_item_title = {
 | 
			
		||||
	'xalign': hippo.ALIGNMENT_START,
 | 
			
		||||
	'padding-left': 5,
 | 
			
		||||
	'color'	 : 0xFFFFFFFF,
 | 
			
		||||
	'font'	 : style.get_font_description('Bold', 1.2)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
style.register_stylesheet("clipboard.Bubble", clipboard_bubble)
 | 
			
		||||
style.register_stylesheet("clipboard.MenuItem.Title", clipboard_menu_item_title)
 | 
			
		||||
 | 
			
		||||
class ClipboardMenuItem(ClipboardBubble):
 | 
			
		||||
 | 
			
		||||
	def __init__(self, percent = 0, stylesheet="clipboard.Bubble"):
 | 
			
		||||
		ClipboardBubble.__init__(self, percent = percent)
 | 
			
		||||
		style.apply_stylesheet(self, stylesheet)
 | 
			
		||||
 | 
			
		||||
class ClipboardMenu(Menu):
 | 
			
		||||
 | 
			
		||||
	ACTION_DELETE = 0
 | 
			
		||||
	ACTION_SHARE = 1
 | 
			
		||||
	ACTION_STOP_DOWNLOAD = 2
 | 
			
		||||
	
 | 
			
		||||
	def __init__(self, file_name, percent):
 | 
			
		||||
		Menu.__init__(self, file_name)
 | 
			
		||||
		
 | 
			
		||||
		self._progress_bar = ClipboardMenuItem(percent)
 | 
			
		||||
		self._root.append(self._progress_bar)
 | 
			
		||||
				
 | 
			
		||||
		icon = CanvasIcon(icon_name='stock-share-mesh')
 | 
			
		||||
		self.add_action(icon, ClipboardMenu.ACTION_SHARE)
 | 
			
		||||
		
 | 
			
		||||
		if percent == 100:
 | 
			
		||||
			icon = CanvasIcon(icon_name='stock-remove')
 | 
			
		||||
			self.add_action(icon, ClipboardMenu.ACTION_DELETE)
 | 
			
		||||
		else:
 | 
			
		||||
			icon = CanvasIcon(icon_name='stock-close')
 | 
			
		||||
			self.add_action(icon, ClipboardMenu.ACTION_STOP_DOWNLOAD)
 | 
			
		||||
 | 
			
		||||
	def set_percent(self, percent):
 | 
			
		||||
		self._progress_bar.set_property('percent', percent)
 | 
			
		||||
		
 | 
			
		||||
@ -7,6 +7,8 @@ sugar_PYTHON =				\
 | 
			
		||||
	FirstTimeDialog.py		\
 | 
			
		||||
	BuddyIcon.py			\
 | 
			
		||||
	BuddyMenu.py			\
 | 
			
		||||
	ClipboardIcon.py		\
 | 
			
		||||
	ClipboardMenu.py		\
 | 
			
		||||
	OverlayWindow.py		\
 | 
			
		||||
	Shell.py			\
 | 
			
		||||
	stylesheet.py
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										61
									
								
								shell/view/frame/ClipboardBox.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								shell/view/frame/ClipboardBox.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
			
		||||
import logging
 | 
			
		||||
import dbus
 | 
			
		||||
import hippo
 | 
			
		||||
 | 
			
		||||
from sugar.graphics import style
 | 
			
		||||
from view.ClipboardIcon import ClipboardIcon
 | 
			
		||||
 | 
			
		||||
class ClipboardBox(hippo.CanvasBox):
 | 
			
		||||
 | 
			
		||||
	_CLIPBOARD_SERVICE = "org.laptop.Clipboard"
 | 
			
		||||
	_CLIPBOARD_OBJECT_PATH = "/org/laptop/Clipboard"
 | 
			
		||||
	
 | 
			
		||||
	def __init__(self, shell, menu_shell):
 | 
			
		||||
		hippo.CanvasBox.__init__(self)
 | 
			
		||||
		self._shell = shell
 | 
			
		||||
		self._menu_shell = menu_shell
 | 
			
		||||
		self._icons = {}
 | 
			
		||||
 | 
			
		||||
		bus = dbus.SessionBus()
 | 
			
		||||
		bus.add_signal_receiver(self.name_owner_changed_cb,
 | 
			
		||||
									signal_name="NameOwnerChanged",
 | 
			
		||||
									dbus_interface="org.freedesktop.DBus")
 | 
			
		||||
		# Try to register to ClipboardService, if we fail, we'll try later.
 | 
			
		||||
		try:
 | 
			
		||||
			self._connect_clipboard_signals()
 | 
			
		||||
		except dbus.DBusException, exception:
 | 
			
		||||
			pass
 | 
			
		||||
		
 | 
			
		||||
	def _connect_clipboard_signals(self):
 | 
			
		||||
		bus = dbus.SessionBus()
 | 
			
		||||
		proxy_obj = bus.get_object(self._CLIPBOARD_SERVICE, self._CLIPBOARD_OBJECT_PATH)
 | 
			
		||||
		iface = dbus.Interface(proxy_obj, self._CLIPBOARD_SERVICE)
 | 
			
		||||
		iface.connect_to_signal('object_added', self.object_added_callback)	
 | 
			
		||||
		iface.connect_to_signal('object_deleted', self.object_deleted_callback)
 | 
			
		||||
		iface.connect_to_signal('object_state_updated', self.object_state_updated_callback)	
 | 
			
		||||
 | 
			
		||||
	def name_owner_changed_cb(self, name, old, new):
 | 
			
		||||
		if name != self._CLIPBOARD_SERVICE:
 | 
			
		||||
			return
 | 
			
		||||
		if (not old and not len(old)) and (new and len(new)):
 | 
			
		||||
			# ClipboardService started up
 | 
			
		||||
			self._connect_clipboard_signals()
 | 
			
		||||
 | 
			
		||||
	def object_added_callback(self, mimeType, fileName):
 | 
			
		||||
		icon = ClipboardIcon(self._menu_shell, fileName)
 | 
			
		||||
		style.apply_stylesheet(icon, 'frame.BuddyIcon')
 | 
			
		||||
		self.append(icon)
 | 
			
		||||
		self._icons[fileName] = icon
 | 
			
		||||
		
 | 
			
		||||
		logging.debug('ClipboardBox: ' + fileName + ' was added.')
 | 
			
		||||
 | 
			
		||||
	def object_deleted_callback(self, fileName):
 | 
			
		||||
		icon = self._icons[fileName]
 | 
			
		||||
		self.remove(icon)
 | 
			
		||||
		self._icons.remove(icon)		
 | 
			
		||||
		logging.debug('ClipboardBox: ' + fileName + ' was deleted.')
 | 
			
		||||
 | 
			
		||||
	def object_state_updated_callback(self, fileName, percent):
 | 
			
		||||
		icon = self._icons[fileName]
 | 
			
		||||
		icon.set_percent(percent)
 | 
			
		||||
		logging.debug('ClipboardBox: ' + fileName + ' state was updated.')
 | 
			
		||||
@ -23,6 +23,7 @@ from view.frame.ActivitiesBox import ActivitiesBox
 | 
			
		||||
from view.frame.ZoomBox import ZoomBox
 | 
			
		||||
from view.frame.overlaybox import OverlayBox
 | 
			
		||||
from view.frame.FriendsBox import FriendsBox
 | 
			
		||||
from view.frame.ClipboardBox import ClipboardBox
 | 
			
		||||
from view.frame.PanelWindow import PanelWindow
 | 
			
		||||
from view.frame.notificationtray import NotificationTray
 | 
			
		||||
from sugar.graphics.timeline import Timeline
 | 
			
		||||
@ -198,7 +199,10 @@ class Frame:
 | 
			
		||||
		root.append(box)
 | 
			
		||||
 | 
			
		||||
		# Left panel
 | 
			
		||||
		self._create_panel(grid, 0, 1, 1, 10)
 | 
			
		||||
		[menu_shell, root] = self._create_panel(grid, 0, 1, 1, 10)
 | 
			
		||||
		
 | 
			
		||||
		box = ClipboardBox(self._shell, menu_shell)
 | 
			
		||||
		root.append(box)
 | 
			
		||||
 | 
			
		||||
	def _create_panel(self, grid, x, y, width, height):
 | 
			
		||||
		panel = PanelWindow()
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ sugardir = $(pkgdatadir)/shell/view/frame
 | 
			
		||||
sugar_PYTHON =				\
 | 
			
		||||
	__init__.py			\
 | 
			
		||||
	ActivitiesBox.py		\
 | 
			
		||||
	ClipboardBox.py			\
 | 
			
		||||
	FriendsBox.py			\
 | 
			
		||||
	PanelWindow.py			\
 | 
			
		||||
	Frame.py			\
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,7 @@ if sourcedir:
 | 
			
		||||
	bin_path = sourcedir
 | 
			
		||||
	bin_path += ':' + os.path.join(sourcedir, 'shell')
 | 
			
		||||
	bin_path += ':' + os.path.join(sourcedir, 'services/presence')
 | 
			
		||||
	bin_path += ':' + os.path.join(sourcedir, 'services/clipboard')
 | 
			
		||||
 | 
			
		||||
	if os.environ.has_key('PATH'):
 | 
			
		||||
		old_path = os.environ['PATH']
 | 
			
		||||
@ -54,6 +55,11 @@ if sourcedir:
 | 
			
		||||
	bin = os.path.join(sourcedir,
 | 
			
		||||
					  'services/presence/sugar-presence-service')
 | 
			
		||||
	setup.write_service('org.laptop.Presence', bin,
 | 
			
		||||
						env.get_activity_info_dir())	
 | 
			
		||||
							
 | 
			
		||||
	bin = os.path.join(sourcedir,
 | 
			
		||||
					  'services/clipboard/sugar-clipboard')
 | 
			
		||||
	setup.write_service('org.laptop.Clipboard', bin,
 | 
			
		||||
						env.get_activity_info_dir())
 | 
			
		||||
 | 
			
		||||
from sugar.emulator import Emulator
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										133
									
								
								sugar/graphics/ClipboardBubble.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								sugar/graphics/ClipboardBubble.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,133 @@
 | 
			
		||||
# Copyright (C) 2006, Red Hat, Inc.
 | 
			
		||||
#
 | 
			
		||||
# This library is free software; you can redistribute it and/or
 | 
			
		||||
# modify it under the terms of the GNU Lesser General Public
 | 
			
		||||
# License as published by the Free Software Foundation; either
 | 
			
		||||
# version 2 of the License, or (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This library is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | 
			
		||||
# Lesser General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU Lesser General Public
 | 
			
		||||
# License along with this library; if not, write to the
 | 
			
		||||
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 | 
			
		||||
# Boston, MA 02111-1307, USA.
 | 
			
		||||
 | 
			
		||||
#TODO: has to be merged with all the existing bubbles in a generic progress bar widget
 | 
			
		||||
 | 
			
		||||
import math
 | 
			
		||||
 | 
			
		||||
import gobject
 | 
			
		||||
import gtk
 | 
			
		||||
import hippo
 | 
			
		||||
 | 
			
		||||
class ClipboardBubble(hippo.CanvasBox, hippo.CanvasItem):
 | 
			
		||||
	__gtype_name__ = 'ClipboardBubble'
 | 
			
		||||
 | 
			
		||||
	__gproperties__ = {
 | 
			
		||||
		'fill-color': (object, None, None,
 | 
			
		||||
					  gobject.PARAM_READWRITE),
 | 
			
		||||
		'stroke-color': (object, None, None,
 | 
			
		||||
					  gobject.PARAM_READWRITE),
 | 
			
		||||
		'progress-color': (object, None, None,
 | 
			
		||||
					  gobject.PARAM_READWRITE),
 | 
			
		||||
		'percent'   : (object, None, None,
 | 
			
		||||
					  gobject.PARAM_READWRITE),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	def __init__(self, **kwargs):
 | 
			
		||||
		self._stroke_color = 0xFFFFFFFF
 | 
			
		||||
		self._fill_color = 0xFFFFFFFF
 | 
			
		||||
		self._progress_color = 0x000000FF
 | 
			
		||||
		self._percent = 0
 | 
			
		||||
		self._radius = 8
 | 
			
		||||
 | 
			
		||||
		hippo.CanvasBox.__init__(self, **kwargs)
 | 
			
		||||
 | 
			
		||||
	def do_set_property(self, pspec, value):
 | 
			
		||||
		if pspec.name == 'fill-color':
 | 
			
		||||
			self._fill_color = value
 | 
			
		||||
			self.emit_paint_needed(0, 0, -1, -1)
 | 
			
		||||
		elif pspec.name == 'stroke-color':
 | 
			
		||||
			self._stroke_color = value
 | 
			
		||||
			self.emit_paint_needed(0, 0, -1, -1)
 | 
			
		||||
		elif pspec.name == 'progress-color':
 | 
			
		||||
			self._progress_color = value
 | 
			
		||||
			self.emit_paint_needed(0, 0, -1, -1)
 | 
			
		||||
		elif pspec.name == 'percent':
 | 
			
		||||
			self._percent = value
 | 
			
		||||
			self.emit_paint_needed(0, 0, -1, -1)
 | 
			
		||||
 | 
			
		||||
	def do_get_property(self, pspec):
 | 
			
		||||
		if pspec.name == 'fill-color':
 | 
			
		||||
			return self._fill_color
 | 
			
		||||
		elif pspec.name == 'stroke-color':
 | 
			
		||||
			return self._stroke_color
 | 
			
		||||
		elif pspec.name == 'progress-color':
 | 
			
		||||
			return self._progress_color
 | 
			
		||||
		elif pspec.name == 'percent':
 | 
			
		||||
			return self._percent
 | 
			
		||||
 | 
			
		||||
	def _int_to_rgb(self, int_color):
 | 
			
		||||
		red = (int_color >> 24) & 0x000000FF
 | 
			
		||||
		green = (int_color >> 16) & 0x000000FF
 | 
			
		||||
		blue = (int_color >> 8) & 0x000000FF
 | 
			
		||||
		alpha = int_color & 0x000000FF
 | 
			
		||||
		return (red / 255.0, green / 255.0, blue / 255.0)
 | 
			
		||||
 | 
			
		||||
	def do_paint_below_children(self, cr, damaged_box):
 | 
			
		||||
		[width, height] = self.get_allocation()
 | 
			
		||||
 | 
			
		||||
		line_width = 3.0
 | 
			
		||||
		x = line_width
 | 
			
		||||
		y = line_width
 | 
			
		||||
		width -= line_width * 2
 | 
			
		||||
		height -= line_width * 2
 | 
			
		||||
 | 
			
		||||
		cr.move_to(x + self._radius, y);
 | 
			
		||||
		cr.arc(x + width - self._radius, y + self._radius,
 | 
			
		||||
			   self._radius, math.pi * 1.5, math.pi * 2);
 | 
			
		||||
		cr.arc(x + width - self._radius, x + height - self._radius,
 | 
			
		||||
			   self._radius, 0, math.pi * 0.5);
 | 
			
		||||
		cr.arc(x + self._radius, y + height - self._radius,
 | 
			
		||||
			   self._radius, math.pi * 0.5, math.pi);
 | 
			
		||||
		cr.arc(x + self._radius, y + self._radius, self._radius,
 | 
			
		||||
			   math.pi, math.pi * 1.5);
 | 
			
		||||
 | 
			
		||||
		color = self._int_to_rgb(self._fill_color)
 | 
			
		||||
		cr.set_source_rgb(*color)
 | 
			
		||||
		cr.fill_preserve();
 | 
			
		||||
 | 
			
		||||
		color = self._int_to_rgb(self._stroke_color)
 | 
			
		||||
		cr.set_source_rgb(*color)
 | 
			
		||||
		cr.set_line_width(line_width)
 | 
			
		||||
		cr.stroke();
 | 
			
		||||
 | 
			
		||||
		self._paint_progress_bar(cr, x, y, width, height, line_width)
 | 
			
		||||
 | 
			
		||||
	def _paint_progress_bar(self, cr, x, y, width, height, line_width):
 | 
			
		||||
		prog_x = x + line_width
 | 
			
		||||
		prog_y = y + line_width
 | 
			
		||||
		prog_width = (width - (line_width * 2)) * (self._percent / 100.0)
 | 
			
		||||
		prog_height = (height - (line_width * 2))
 | 
			
		||||
 | 
			
		||||
		x = prog_x
 | 
			
		||||
		y = prog_y
 | 
			
		||||
		width = prog_width
 | 
			
		||||
		height = prog_height
 | 
			
		||||
 | 
			
		||||
		cr.move_to(x + self._radius, y);
 | 
			
		||||
		cr.arc(x + width - self._radius, y + self._radius,
 | 
			
		||||
			   self._radius, math.pi * 1.5, math.pi * 2);
 | 
			
		||||
		cr.arc(x + width - self._radius, x + height - self._radius,
 | 
			
		||||
			   self._radius, 0, math.pi * 0.5);
 | 
			
		||||
		cr.arc(x + self._radius, y + height - self._radius,
 | 
			
		||||
			   self._radius, math.pi * 0.5, math.pi);
 | 
			
		||||
		cr.arc(x + self._radius, y + self._radius, self._radius,
 | 
			
		||||
			   math.pi, math.pi * 1.5);
 | 
			
		||||
 | 
			
		||||
		color = self._int_to_rgb(self._progress_color)
 | 
			
		||||
		cr.set_source_rgb(*color)
 | 
			
		||||
		cr.fill_preserve();
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user