diff --git a/.gitignore b/.gitignore index fb168b10..6e3a9e50 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ services/clipboard/org.laptop.Clipboard.service services/console/org.laptop.sugar.Console.service services/presence/org.laptop.Sugar.Presence.service bin/sugar +shell/extensions/_extensions.c diff --git a/bin/Makefile.am b/bin/Makefile.am index db5e2196..ffbc7bba 100644 --- a/bin/Makefile.am +++ b/bin/Makefile.am @@ -1,3 +1,6 @@ +sugardir = $(pkgdatadir)/bin +sugar_SCRIPTS = sugar-activity-factory + bin_SCRIPTS = \ sugar \ sugar-activity \ @@ -6,6 +9,7 @@ bin_SCRIPTS = \ EXTRA_DIST = \ $(bin_SCRIPTS) \ + $(sugar_SCRIPTS) \ sugar.in DISTCLEANFILES = \ diff --git a/bin/sugar.in b/bin/sugar.in index bfdf8f95..de2595da 100644 --- a/bin/sugar.in +++ b/bin/sugar.in @@ -1,2 +1,2 @@ -export GTK2_RC_FILES=@prefix@/share/sugar/gtkrc +export GTK2_RC_FILES=@prefix@/share/sugar/data/gtkrc dbus-launch --exit-with-session sugar-shell diff --git a/browser/GeckoBrowserPersist.cpp b/browser/GeckoBrowserPersist.cpp new file mode 100644 index 00000000..2c019c53 --- /dev/null +++ b/browser/GeckoBrowserPersist.cpp @@ -0,0 +1,190 @@ +#include + +#ifdef HAVE_GECKO_1_9 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "GeckoBrowserPersist.h" + +GeckoBrowserPersist::GeckoBrowserPersist(SugarBrowser *browser) + : mBrowser(browser) +{ +} + +GeckoBrowserPersist::~GeckoBrowserPersist() +{ +} + +static nsresult +NewURI(const char *uri, nsIURI **result) +{ + nsresult rv; + + nsCOMPtr mgr; + NS_GetServiceManager (getter_AddRefs (mgr)); + NS_ENSURE_TRUE(mgr, FALSE); + + nsCOMPtr ioService; + rv = mgr->GetServiceByContractID ("@mozilla.org/network/io-service;1", + NS_GET_IID (nsIIOService), + getter_AddRefs(ioService)); + NS_ENSURE_SUCCESS(rv, FALSE); + + nsCString cSpec(uri); + return ioService->NewURI (cSpec, nsnull, nsnull, result); +} + +static nsresult +GetPostData(SugarBrowser *browser, nsIInputStream **postData) +{ +#ifdef HAVE_NS_WEB_BROWSER + nsCOMPtr webBrowser; + gtk_moz_embed_get_nsIWebBrowser(GTK_MOZ_EMBED(browser), + getter_AddRefs(webBrowser)); + NS_ENSURE_TRUE(webBrowser, NS_ERROR_FAILURE); + + nsCOMPtr webNav(do_QueryInterface(webBrowser)); + NS_ENSURE_TRUE(webNav, NS_ERROR_FAILURE); + + PRInt32 sindex; + + nsCOMPtr sessionHistory; + webNav->GetSessionHistory(getter_AddRefs(sessionHistory)); + NS_ENSURE_TRUE(sessionHistory, NS_ERROR_FAILURE); + + nsCOMPtr entry; + sessionHistory->GetIndex(&sindex); + sessionHistory->GetEntryAtIndex(sindex, PR_FALSE, getter_AddRefs(entry)); + + nsCOMPtr shEntry(do_QueryInterface(entry)); + if (shEntry) { + shEntry->GetPostData(postData); + } + + return NS_OK; +#endif + return NS_ERROR_NOT_IMPLEMENTED; +} + +bool +GeckoBrowserPersist::SaveURI(const char *uri, const char *filename) +{ + nsresult rv; + + nsCOMPtr sourceURI; + rv = NewURI(uri, getter_AddRefs(sourceURI)); + NS_ENSURE_SUCCESS(rv, FALSE); + + nsCOMPtr destFile = do_CreateInstance("@mozilla.org/file/local;1"); + NS_ENSURE_TRUE(destFile, FALSE); + + destFile->InitWithNativePath(nsCString(filename)); + + nsCOMPtr postData; + GetPostData(mBrowser, getter_AddRefs(postData)); + + nsCOMPtr inputChannel; + rv = NS_NewChannel(getter_AddRefs(inputChannel), sourceURI, + nsnull, nsnull, nsnull, nsIRequest::LOAD_NORMAL); + NS_ENSURE_SUCCESS(rv, FALSE); + + nsCOMPtr httpChannel(do_QueryInterface(inputChannel)); + if (httpChannel) { + nsCOMPtr stream(do_QueryInterface(postData)); + if (stream) + { + // Rewind the postdata stream + stream->Seek(nsISeekableStream::NS_SEEK_SET, 0); + nsCOMPtr uploadChannel(do_QueryInterface(httpChannel)); + NS_ASSERTION(uploadChannel, "http must support nsIUploadChannel"); + // Attach the postdata to the http channel + uploadChannel->SetUploadStream(postData, EmptyCString(), -1); + } + } + + nsCOMPtr stream; + rv = inputChannel->Open(getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, FALSE); + + nsCOMPtr fileOutputStream = + do_CreateInstance(NS_LOCALFILEOUTPUTSTREAM_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, FALSE); + + rv = fileOutputStream->Init(destFile, -1, -1, 0); + NS_ENSURE_SUCCESS(rv, FALSE); + + // Read data from the input and write to the output + char buffer[8192]; + PRUint32 bytesRead; + PRUint32 bytesRemaining; + PRBool cancel = PR_FALSE; + PRBool readError; + + rv = stream->Available(&bytesRemaining); + NS_ENSURE_SUCCESS(rv, FALSE); + + while (!cancel && bytesRemaining) + { + readError = PR_TRUE; + rv = stream->Read(buffer, PR_MIN(sizeof(buffer), bytesRemaining), &bytesRead); + if (NS_SUCCEEDED(rv)) + { + readError = PR_FALSE; + // Write out the data until something goes wrong, or, it is + // all written. We loop because for some errors (e.g., disk + // full), we get NS_OK with some bytes written, then an error. + // So, we want to write again in that case to get the actual + // error code. + const char *bufPtr = buffer; // Where to write from. + while (NS_SUCCEEDED(rv) && bytesRead) + { + PRUint32 bytesWritten = 0; + rv = fileOutputStream->Write(bufPtr, bytesRead, &bytesWritten); + if (NS_SUCCEEDED(rv)) + { + bytesRead -= bytesWritten; + bufPtr += bytesWritten; + bytesRemaining -= bytesWritten; + // Force an error if (for some reason) we get NS_OK but + // no bytes written. + if (!bytesWritten) + { + rv = NS_ERROR_FAILURE; + cancel = PR_TRUE; + } + } + else + { + // Disaster - can't write out the bytes - disk full / permission? + cancel = PR_TRUE; + } + } + } + else + { + // Disaster - can't read the bytes - broken link / file error? + cancel = PR_TRUE; + } + } + NS_ENSURE_SUCCESS(rv, FALSE); + + stream->Close(); + + return TRUE; +} + +#endif diff --git a/browser/GeckoBrowserPersist.h b/browser/GeckoBrowserPersist.h new file mode 100644 index 00000000..e9767893 --- /dev/null +++ b/browser/GeckoBrowserPersist.h @@ -0,0 +1,19 @@ +#ifndef __GECKO_BROWSER_PERSIST_H__ +#define __GECKO_BROWSER_PERSIST_H__ + +#include "sugar-browser.h" + +class GeckoBrowserPersist +{ +public: + GeckoBrowserPersist(SugarBrowser *browser); + ~GeckoBrowserPersist(); + + bool SaveURI(const char *uri, const char *filename); +private: + SugarBrowser *mBrowser; +protected: + /* additional members */ +}; + +#endif // __GECKO_BROWSER_PERSIST_H__ diff --git a/browser/GeckoDocumentObject.cpp b/browser/GeckoDocumentObject.cpp new file mode 100644 index 00000000..26eb13dc --- /dev/null +++ b/browser/GeckoDocumentObject.cpp @@ -0,0 +1,241 @@ +#include + +#ifdef HAVE_GECKO_1_9 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "GeckoDocumentObject.h" +#include "GeckoBrowserPersist.h" + +GeckoDocumentObject::GeckoDocumentObject(SugarBrowser *browser, nsIDOMNode *node) + : mBrowser(browser), + mNode(node), + mImage(NULL) +{ +} + +GeckoDocumentObject::~GeckoDocumentObject() +{ +} + +bool GeckoDocumentObject::IsImage() +{ + if(mImage) { + return true; + } + + nsresult rv; + + PRUint16 type; + rv = mNode->GetNodeType(&type); + if(NS_FAILED(rv)) return rv; + + nsCOMPtr element = do_QueryInterface(mNode); + if ((nsIDOMNode::ELEMENT_NODE == type) && element) { + nsString uTag; + rv = element->GetLocalName(uTag); + if(NS_FAILED(rv)) return rv; + + nsCString tag; + NS_UTF16ToCString (uTag, NS_CSTRING_ENCODING_UTF8, tag); + + if (g_ascii_strcasecmp (tag.get(), "img") == 0) { + nsCOMPtr imagePtr; + imagePtr = do_QueryInterface(mNode, &rv); + if(NS_FAILED(rv)) return rv; + + mImage = imagePtr; + + return true; + } + } + + return false; +} + +static nsresult +NewURI(const char *uri, nsIURI **result) +{ + nsresult rv; + + nsCOMPtr mgr; + NS_GetServiceManager (getter_AddRefs (mgr)); + NS_ENSURE_TRUE(mgr, FALSE); + + nsCOMPtr ioService; + rv = mgr->GetServiceByContractID ("@mozilla.org/network/io-service;1", + NS_GET_IID (nsIIOService), + getter_AddRefs(ioService)); + NS_ENSURE_SUCCESS(rv, FALSE); + + nsCString cSpec(uri); + return ioService->NewURI (cSpec, nsnull, nsnull, result); +} + +static nsresult +FilenameFromContentDisposition(nsCString contentDisposition, nsCString &fileName) +{ + nsresult rv; + nsCString fallbackCharset; + + nsCOMPtr mimehdrpar = + do_GetService("@mozilla.org/network/mime-hdrparam;1"); + NS_ENSURE_TRUE(mimehdrpar, NS_ERROR_FAILURE); + + nsString aFileName; + rv = mimehdrpar->GetParameter (contentDisposition, "filename", + fallbackCharset, PR_TRUE, nsnull, + aFileName); + + if (NS_FAILED(rv) || !fileName.Length()) { + rv = mimehdrpar->GetParameter (contentDisposition, "name", + fallbackCharset, PR_TRUE, nsnull, + aFileName); + } + + if (NS_SUCCEEDED(rv) && fileName.Length()) { + NS_UTF16ToCString (aFileName, NS_CSTRING_ENCODING_UTF8, fileName); + } + + return NS_OK; +} + +static nsresult +GetImageProperties(char *imgURIStr, nsIProperties **properties) +{ + nsresult rv; + + nsCOMPtr imageURI; + rv = NewURI(imgURIStr, getter_AddRefs(imageURI)); + if(NS_FAILED(rv)) return rv; + + nsCOMPtr mgr; + NS_GetServiceManager(getter_AddRefs(mgr)); + NS_ENSURE_TRUE(mgr, NS_ERROR_FAILURE); + + nsCOMPtr imgCache; + rv = mgr->GetServiceByContractID("@mozilla.org/image/cache;1", + NS_GET_IID(imgICache), + getter_AddRefs(imgCache)); + if(NS_FAILED(rv)) return rv; + + imgCache->FindEntryProperties(imageURI, properties); + NS_ENSURE_TRUE(mgr, NS_ERROR_FAILURE); + return NS_OK; +} + +char * +GeckoDocumentObject::GetImageName() +{ + if(!IsImage()) { + return NULL; + } + + if(mImageName.Length()) { + return g_strdup(mImageName.get()); + } + + nsresult rv; + char *imgURIStr = GetImageURI(); + + nsCOMPtr imgProperties; + rv = GetImageProperties(imgURIStr, getter_AddRefs(imgProperties)); + NS_ENSURE_SUCCESS(rv, NULL); + if (imgProperties) { + nsCOMPtr dispositionCString; + imgProperties->Get("content-disposition", + NS_GET_IID(nsISupportsCString), + getter_AddRefs(dispositionCString)); + if (dispositionCString) { + nsCString contentDisposition; + dispositionCString->GetData(contentDisposition); + FilenameFromContentDisposition(contentDisposition, mImageName); + } + } + + if (!mImageName.Length()) { + nsCOMPtr imageURI; + rv = NewURI(imgURIStr, getter_AddRefs(imageURI)); + NS_ENSURE_SUCCESS(rv, NULL); + + nsCOMPtr url(do_QueryInterface(imageURI)); + if (url) { + url->GetFileName(mImageName); + } + } + + return mImageName.Length() ? g_strdup(mImageName.get()) : NULL; +} + +char * +GeckoDocumentObject::GetImageMimeType() +{ + if(!IsImage()) { + return NULL; + } + + if(mImageMimeType.Length()) { + return g_strdup(mImageMimeType.get()); + } + + nsresult rv; + char *imgURIStr = GetImageURI(); + + nsCOMPtr imgProperties; + rv = GetImageProperties(imgURIStr, getter_AddRefs(imgProperties)); + NS_ENSURE_SUCCESS(rv, NULL); + if (imgProperties) { + nsCOMPtr typeCString; + imgProperties->Get("type", + NS_GET_IID(nsISupportsCString), + getter_AddRefs(typeCString)); + if (typeCString) { + typeCString->GetData(mImageMimeType); + } + } + + return mImageMimeType.Length() ? g_strdup(mImageMimeType.get()) : NULL; +} + +char * +GeckoDocumentObject::GetImageURI() +{ + if(!IsImage()) { + return NULL; + } + + if(!mImageURI.Length()) { + nsresult rv; + nsString img; + rv = mImage->GetSrc(img); + if (NS_FAILED(rv)) return NULL; + + NS_UTF16ToCString (img, NS_CSTRING_ENCODING_UTF8, mImageURI); + } + return g_strdup(mImageURI.get()); +} + +bool +GeckoDocumentObject::SaveImage(const char *filename) +{ + GeckoBrowserPersist browserPersist(mBrowser); + return browserPersist.SaveURI(mImageURI.get(), filename); +} + +#endif diff --git a/browser/GeckoDocumentObject.h b/browser/GeckoDocumentObject.h new file mode 100644 index 00000000..5f4069fc --- /dev/null +++ b/browser/GeckoDocumentObject.h @@ -0,0 +1,31 @@ +#ifndef __GECKO_DOCUMENT_OBJECT_H__ +#define __GECKO_DOCUMENT_OBJECT_H__ + +#include +#include + +#include "sugar-browser.h" + +class GeckoDocumentObject +{ +public: + GeckoDocumentObject(SugarBrowser *browser, nsIDOMNode *node); + ~GeckoDocumentObject(); + + bool IsImage(); + char *GetImageURI(); + char *GetImageName(); + char *GetImageMimeType(); + bool SaveImage(const char *filename); +private: + SugarBrowser *mBrowser; + nsCOMPtr mNode; + nsCOMPtr mImage; + nsCString mImageURI; + nsCString mImageName; + nsCString mImageMimeType; +protected: + /* additional members */ +}; + +#endif // __GECKO_DOCUMENT_OBJECT_H__ diff --git a/browser/GeckoDragDropHooks.cpp b/browser/GeckoDragDropHooks.cpp new file mode 100644 index 00000000..6439f42c --- /dev/null +++ b/browser/GeckoDragDropHooks.cpp @@ -0,0 +1,218 @@ +#include + +#ifdef HAVE_GECKO_1_9 + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "GeckoDragDropHooks.h" +#include "GeckoDocumentObject.h" + +#define TEXT_URI_LIST "text/uri-list" +#define TEXT_X_MOZ_URL "text/x-moz-url" +#define FILE_LOCALHOST "file://" + +//***************************************************************************** +// UriListDataProvider +//***************************************************************************** + +class UriListDataProvider : public nsIFlavorDataProvider +{ +public: + UriListDataProvider(GeckoDocumentObject *mDocumentObject); + virtual ~UriListDataProvider(); + NS_DECL_ISUPPORTS + NS_DECL_NSIFLAVORDATAPROVIDER +private: + GeckoDocumentObject *mDocumentObject; + nsCString mFilePath; +}; + +//***************************************************************************** + +NS_IMPL_ISUPPORTS1(UriListDataProvider, nsIFlavorDataProvider) + +UriListDataProvider::UriListDataProvider(GeckoDocumentObject *documentObject) + : mDocumentObject(documentObject) +{ +} + +UriListDataProvider::~UriListDataProvider() +{ + if(mFilePath.Length()) { + remove(mFilePath.get()); + } + + delete mDocumentObject; +} + +NS_IMETHODIMP +UriListDataProvider::GetFlavorData(nsITransferable *aTransferable, + const char *aFlavor, nsISupports **aData, + PRUint32 *aDataLen) +{ + NS_ENSURE_ARG_POINTER(aData && aDataLen); + + nsresult rv = NS_ERROR_NOT_IMPLEMENTED; + char *image_name; + char *mime_type; + char *file_ext; + nsCString mime_ext; + timeval timestamp; + + *aData = nsnull; + *aDataLen = 0; + + if(g_ascii_strcasecmp(aFlavor, TEXT_URI_LIST) != 0) { + return rv; + } + + gettimeofday(×tamp, NULL); + + mFilePath.Append(g_get_tmp_dir()); + mFilePath.Append("/"); + mFilePath.AppendInt(timestamp.tv_sec); + mFilePath.AppendInt(timestamp.tv_usec); + + image_name = mDocumentObject->GetImageName(); + file_ext = strrchr(image_name, '.'); + mime_type = mDocumentObject->GetImageMimeType(); + + nsCOMPtr mimeService(do_GetService("@mozilla.org/mime;1", + &rv)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mimeService->GetPrimaryExtension(nsCString(mime_type), + EmptyCString(), + mime_ext); + NS_ENSURE_SUCCESS(rv, rv); + + if(!file_ext) { + mFilePath.Append(image_name); + mFilePath.Append("."); + mFilePath.Append(mime_ext); + } else if(strcmp(file_ext, mime_ext.get())) { + image_name[strlen(file_ext)] = 0; + mFilePath.Append(image_name); + mFilePath.Append("."); + mFilePath.Append(mime_ext); + } else { + mFilePath.Append(image_name); + } + + g_free(mime_type); + g_free(image_name); + + if(!mDocumentObject->SaveImage(mFilePath.get())) { + return NS_ERROR_FAILURE; + } + + nsCString localURI(FILE_LOCALHOST); + localURI.Append(mFilePath); + + nsString localURI16; + NS_CStringToUTF16(localURI, NS_CSTRING_ENCODING_UTF8, localURI16); + + nsCOMPtr localURIData(do_CreateInstance( + "@mozilla.org/supports-string;1", &rv)); + if(NS_FAILED(rv)) return rv; + + rv = localURIData->SetData(localURI16); + if(NS_FAILED(rv)) return rv; + + CallQueryInterface(localURIData, aData); + *aDataLen = sizeof(nsISupportsString*); + + // FIXME: Why do we need this? Is there a leak in mozilla? + this->Release(); + + return rv; +} + +//***************************************************************************** +// GeckoDragDropHooks +//***************************************************************************** + +NS_IMPL_ISUPPORTS1(GeckoDragDropHooks, nsIClipboardDragDropHooks) + +GeckoDragDropHooks::GeckoDragDropHooks(SugarBrowser *browser) + : mBrowser(browser) +{ +} + +GeckoDragDropHooks::~GeckoDragDropHooks() +{ +} + +NS_IMETHODIMP +GeckoDragDropHooks::AllowStartDrag(nsIDOMEvent *event, PRBool *_retval) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +GeckoDragDropHooks::AllowDrop(nsIDOMEvent *event, nsIDragSession *session, + PRBool *_retval) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +GeckoDragDropHooks::OnCopyOrDrag(nsIDOMEvent *aEvent, nsITransferable *trans, + PRBool *_retval) +{ + nsresult rv; + + *_retval = true; + + nsCOMPtr mouseEvent; + mouseEvent = do_QueryInterface(aEvent, &rv); + if(NS_FAILED(rv)) return rv; + + nsCOMPtr eventTarget; + rv = mouseEvent->GetTarget(getter_AddRefs(eventTarget)); + if(NS_FAILED(rv)) return rv; + + nsCOMPtr targetNode; + targetNode = do_QueryInterface(eventTarget, &rv); + if(NS_FAILED(rv)) return rv; + + GeckoDocumentObject *documentObject = new GeckoDocumentObject(mBrowser, + targetNode); + if(documentObject->IsImage()) { + rv = trans->RemoveDataFlavor(TEXT_X_MOZ_URL); + if(NS_FAILED(rv)) return rv; + + rv = trans->AddDataFlavor(TEXT_URI_LIST); + if(NS_FAILED(rv)) return rv; + + UriListDataProvider *rawPtr = new UriListDataProvider(documentObject); + nsCOMPtr dataProvider(do_QueryInterface(rawPtr, &rv)); + if(NS_FAILED(rv)) return rv; + + rv = trans->SetTransferData(TEXT_URI_LIST, dataProvider, 0); + if(NS_FAILED(rv)) return rv; + } + + return rv; +} + +NS_IMETHODIMP +GeckoDragDropHooks::OnPasteOrDrop(nsIDOMEvent *event, nsITransferable *trans, + PRBool *_retval) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +#endif diff --git a/browser/GeckoDragDropHooks.h b/browser/GeckoDragDropHooks.h new file mode 100644 index 00000000..c551c0e5 --- /dev/null +++ b/browser/GeckoDragDropHooks.h @@ -0,0 +1,25 @@ +#ifndef __GECKO_DRAG_DROP_HOOKS_H__ +#define __GECKO_DRAG_DROP_HOOKS_H__ + +#include + +#include "sugar-browser.h" + +class GeckoDragDropHooks : public nsIClipboardDragDropHooks +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICLIPBOARDDRAGDROPHOOKS + + GeckoDragDropHooks(SugarBrowser *browser); + +private: + ~GeckoDragDropHooks(); + + SugarBrowser *mBrowser; + +protected: + /* additional members */ +}; + +#endif // __GECKO_DRAG_DROP_HOOKS_H__ diff --git a/browser/Makefile.am b/browser/Makefile.am index ae013e01..b4185837 100644 --- a/browser/Makefile.am +++ b/browser/Makefile.am @@ -3,6 +3,7 @@ libsugarbrowser_la_CPPFLAGS = \ $(LIB_CFLAGS) \ $(GECKO_CFLAGS) \ $(NSPR_CFLAGS) \ + -I$(MOZILLA_INCLUDE_DIR)/commandhandler \ -I$(MOZILLA_INCLUDE_DIR)/dom \ -I$(MOZILLA_INCLUDE_DIR)/docshell \ -I$(MOZILLA_INCLUDE_DIR)/exthandler \ @@ -15,6 +16,8 @@ libsugarbrowser_la_CPPFLAGS = \ -I$(MOZILLA_INCLUDE_DIR)/uriloader \ -I$(MOZILLA_INCLUDE_DIR)/webbrwsr \ -I$(MOZILLA_INCLUDE_DIR)/webbrowserpersist \ + -I$(MOZILLA_INCLUDE_DIR)/widget \ + -I$(MOZILLA_INCLUDE_DIR)/xpcom \ -DPLUGIN_DIR=\"$(libdir)/mozilla/plugins\" \ -DSHARE_DIR=\"$(pkgdatadir)\" @@ -26,8 +29,14 @@ libsugarbrowser_la_LIBADD = \ libsugarbrowser_la_SOURCES = \ $(BUILT_SOURCES) \ + GeckoBrowserPersist.h \ + GeckoBrowserPersist.cpp \ GeckoContentHandler.h \ GeckoContentHandler.cpp \ + GeckoDocumentObject.h \ + GeckoDocumentObject.cpp \ + GeckoDragDropHooks.h \ + GeckoDragDropHooks.cpp \ GeckoDownload.h \ GeckoDownload.cpp \ sugar-address-entry.c \ diff --git a/browser/sugar-browser.cpp b/browser/sugar-browser.cpp index cd455b6f..df5769b2 100644 --- a/browser/sugar-browser.cpp +++ b/browser/sugar-browser.cpp @@ -24,6 +24,12 @@ #include "GeckoContentHandler.h" #include "GeckoDownload.h" +#ifdef HAVE_GECKO_1_9 +#include "GeckoDragDropHooks.h" +#include "GeckoDocumentObject.h" +#include "GeckoBrowserPersist.h" +#endif + #include #include #include @@ -56,6 +62,10 @@ #include #include #include +#include +#include + +#define SUGAR_PATH "SUGAR_PATH" enum { PROP_0, @@ -75,6 +85,8 @@ enum { static guint signals[N_SIGNALS]; +static GObjectClass *parent_class = NULL; + static const nsModuleComponentInfo sSugarComponents[] = { { "Gecko Content Handler", @@ -151,9 +163,11 @@ sugar_browser_startup(const char *profile_path, const char *profile_name) NS_ENSURE_TRUE(prefService, FALSE); /* Read our predefined default prefs */ + nsCString pathToPrefs(g_getenv(SUGAR_PATH)); + pathToPrefs.Append("/data/gecko-prefs.js"); + nsCOMPtr file; - NS_NewNativeLocalFile(nsCString(SHARE_DIR"/gecko-prefs.js"), - PR_TRUE, getter_AddRefs(file)); + NS_NewNativeLocalFile(pathToPrefs, PR_TRUE, getter_AddRefs(file)); NS_ENSURE_TRUE(file, FALSE); rv = prefService->ReadUserPrefs (file); @@ -166,7 +180,10 @@ sugar_browser_startup(const char *profile_path, const char *profile_name) prefService->GetBranch ("", getter_AddRefs(pref)); NS_ENSURE_TRUE(pref, FALSE); - pref->SetCharPref ("helpers.private_mime_types_file", SHARE_DIR"/mime.types"); + nsCString pathToMimeTypes(g_getenv(SUGAR_PATH)); + pathToMimeTypes.Append("/data/mime.types"); + + pref->SetCharPref ("helpers.private_mime_types_file", pathToMimeTypes.get()); rv = prefService->ReadUserPrefs (nsnull); if (NS_FAILED(rv)) { @@ -210,25 +227,6 @@ sugar_browser_shutdown(void) G_DEFINE_TYPE(SugarBrowser, sugar_browser, GTK_TYPE_MOZ_EMBED) -static nsresult -NewURI(const char *uri, nsIURI **result) -{ - nsresult rv; - - nsCOMPtr mgr; - NS_GetServiceManager (getter_AddRefs (mgr)); - NS_ENSURE_TRUE(mgr, FALSE); - - nsCOMPtr ioService; - rv = mgr->GetServiceByContractID ("@mozilla.org/network/io-service;1", - NS_GET_IID (nsIIOService), - getter_AddRefs(ioService)); - NS_ENSURE_SUCCESS(rv, FALSE); - - nsCString cSpec(uri); - return ioService->NewURI (cSpec, nsnull, nsnull, result); -} - static nsresult FilenameFromContentDisposition(nsCString contentDisposition, nsCString &fileName) { @@ -258,38 +256,6 @@ FilenameFromContentDisposition(nsCString contentDisposition, nsCString &fileName return NS_OK; } -static nsresult -ImageNameFromCache(nsIURI *imgURI, nsCString &imgName) -{ - nsresult rv; - - nsCOMPtr mgr; - NS_GetServiceManager (getter_AddRefs (mgr)); - NS_ENSURE_TRUE(mgr, NS_ERROR_FAILURE); - - nsCOMPtr imgCache; - rv = mgr->GetServiceByContractID("@mozilla.org/image/cache;1", - NS_GET_IID (imgICache), - getter_AddRefs(imgCache)); - NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); - - nsCOMPtr imgProperties; - imgCache->FindEntryProperties(imgURI, getter_AddRefs(imgProperties)); - if (imgProperties) { - nsCOMPtr dispositionCString; - imgProperties->Get("content-disposition", - NS_GET_IID(nsISupportsCString), - getter_AddRefs(dispositionCString)); - if (dispositionCString) { - nsCString contentDisposition; - dispositionCString->GetData(contentDisposition); - FilenameFromContentDisposition(contentDisposition, imgName); - } - } - - return NS_OK; -} - static SugarBrowserMetadata * sugar_browser_get_document_metadata(SugarBrowser *browser) { @@ -382,13 +348,47 @@ sugar_browser_get_property(GObject *object, } } +static void +sugar_browser_realize(GtkWidget *widget) +{ + GTK_WIDGET_CLASS(parent_class)->realize(widget); + +#ifdef HAVE_NS_WEB_BROWSER + GtkMozEmbed *embed = GTK_MOZ_EMBED(widget); + nsCOMPtr webBrowser; + gtk_moz_embed_get_nsIWebBrowser(embed, getter_AddRefs(webBrowser)); + NS_ENSURE_TRUE(webBrowser, ); + + nsCOMPtr commandManager = do_GetInterface(webBrowser); + if (commandManager) { + nsresult rv; + nsIClipboardDragDropHooks *rawPtr = new GeckoDragDropHooks( + SUGAR_BROWSER(widget)); + nsCOMPtr geckoDragDropHooks( + do_QueryInterface(rawPtr, &rv)); + NS_ENSURE_SUCCESS(rv, ); + + nsCOMPtr DOMWindow = do_GetInterface(webBrowser); + nsCOMPtr cmdParamsObj = do_CreateInstance( + NS_COMMAND_PARAMS_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, ); + cmdParamsObj->SetISupportsValue("addhook", geckoDragDropHooks); + commandManager->DoCommand("cmd_clipboardDragDropHook", cmdParamsObj, + DOMWindow); + } +#endif +} static void sugar_browser_class_init(SugarBrowserClass *browser_class) { - GObjectClass *gobject_class = G_OBJECT_CLASS(browser_class); + GObjectClass *gobject_class = G_OBJECT_CLASS(browser_class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(browser_class); - gobject_class->get_property = sugar_browser_get_property; + parent_class = (GObjectClass *) g_type_class_peek_parent(browser_class); + + gobject_class->get_property = sugar_browser_get_property; + widget_class->realize = sugar_browser_realize; signals[MOUSE_CLICK] = g_signal_new ("mouse_click", SUGAR_TYPE_BROWSER, @@ -570,33 +570,11 @@ location_cb(GtkMozEmbed *embed) update_navigation_properties(browser); } -static char * -get_image_name(const char *uri) -{ - nsresult rv; - - nsCString imgName; - - nsCOMPtr imgURI; - rv = NewURI(uri, getter_AddRefs(imgURI)); - NS_ENSURE_SUCCESS(rv, NULL); - - ImageNameFromCache(imgURI, imgName); - - if (!imgName.Length()) { - nsCOMPtr url(do_QueryInterface(imgURI)); - if (url) { - url->GetFileName(imgName); - } - } - - return imgName.Length() ? g_strdup(imgName.get()) : NULL; -} - static gboolean dom_mouse_click_cb(GtkMozEmbed *embed, nsIDOMMouseEvent *mouseEvent) { - SugarBrowser *browser = SUGAR_BROWSER(embed); +#ifdef HAVE_GECKO_1_9 + SugarBrowser *browser = SUGAR_BROWSER(embed); SugarBrowserEvent *event; gint return_value = FALSE; @@ -610,36 +588,10 @@ dom_mouse_click_cb(GtkMozEmbed *embed, nsIDOMMouseEvent *mouseEvent) event = sugar_browser_event_new(); - nsresult rv; - - PRUint16 type; - rv = targetNode->GetNodeType(&type); - if (NS_FAILED(rv)) return NS_ERROR_FAILURE; - - nsCOMPtr element = do_QueryInterface(targetNode); - if ((nsIDOMNode::ELEMENT_NODE == type) && element) { - nsString uTag; - rv = element->GetLocalName(uTag); - if (NS_FAILED(rv)) return NS_ERROR_FAILURE; - - nsCString tag; - NS_UTF16ToCString (uTag, NS_CSTRING_ENCODING_UTF8, tag); - - if (g_ascii_strcasecmp (tag.get(), "img") == 0) { - nsString img; - - nsCOMPtr image; - image = do_QueryInterface(targetNode, &rv); - if (NS_FAILED(rv) || !image) return NS_ERROR_FAILURE; - - rv = image->GetSrc(img); - if (NS_FAILED(rv)) return NS_ERROR_FAILURE; - - nsCString cImg; - NS_UTF16ToCString (img, NS_CSTRING_ENCODING_UTF8, cImg); - event->image_uri = g_strdup(cImg.get()); - event->image_name = get_image_name(event->image_uri); - } + GeckoDocumentObject documentObject(browser, targetNode); + if(documentObject.IsImage()) { + event->image_uri = documentObject.GetImageURI(); + event->image_name = documentObject.GetImageName(); } PRUint16 btn = 0; @@ -651,6 +603,9 @@ dom_mouse_click_cb(GtkMozEmbed *embed, nsIDOMMouseEvent *mouseEvent) sugar_browser_event_free(event); return return_value; +#else + return FALSE; +#endif } static void @@ -712,71 +667,14 @@ sugar_browser_grab_focus(SugarBrowser *browser) } } - -static nsresult -GetPostData(SugarBrowser *browser, nsIInputStream **postData) -{ -#ifdef HAVE_NS_WEB_BROWSER - nsCOMPtr webBrowser; - gtk_moz_embed_get_nsIWebBrowser(GTK_MOZ_EMBED(browser), - getter_AddRefs(webBrowser)); - NS_ENSURE_TRUE(webBrowser, NS_ERROR_FAILURE); - - nsCOMPtr webNav(do_QueryInterface(webBrowser)); - NS_ENSURE_TRUE(webNav, NS_ERROR_FAILURE); - - PRInt32 sindex; - - nsCOMPtr sessionHistory; - webNav->GetSessionHistory(getter_AddRefs(sessionHistory)); - NS_ENSURE_TRUE(sessionHistory, NS_ERROR_FAILURE); - - nsCOMPtr entry; - sessionHistory->GetIndex(&sindex); - sessionHistory->GetEntryAtIndex(sindex, PR_FALSE, getter_AddRefs(entry)); - - nsCOMPtr shEntry(do_QueryInterface(entry)); - if (shEntry) { - shEntry->GetPostData(postData); - } - - return NS_OK; -#endif - return NS_ERROR_NOT_IMPLEMENTED; -} - gboolean sugar_browser_save_uri(SugarBrowser *browser, const char *uri, const char *filename) { -#ifdef HAVE_NS_WEB_BROWSER - nsresult rv; - - nsCOMPtr sourceURI; - rv = NewURI(uri, getter_AddRefs(sourceURI)); - NS_ENSURE_SUCCESS(rv, FALSE); - - nsCOMPtr destFile = do_CreateInstance("@mozilla.org/file/local;1"); - NS_ENSURE_TRUE(destFile, FALSE); - - destFile->InitWithNativePath(nsCString(filename)); - - nsCOMPtr webBrowser; - gtk_moz_embed_get_nsIWebBrowser(GTK_MOZ_EMBED(browser), - getter_AddRefs(webBrowser)); - NS_ENSURE_TRUE(webBrowser, FALSE); - - nsCOMPtr webPersist = do_QueryInterface (webBrowser); - NS_ENSURE_TRUE(webPersist, FALSE); - - nsCOMPtr postData; - GetPostData(browser, getter_AddRefs(postData)); - - rv = webPersist->SaveURI(sourceURI, nsnull, nsnull, postData, nsnull, destFile); - NS_ENSURE_SUCCESS(rv, FALSE); - - return TRUE; +#ifdef HAVE_GECKO_1_9 + GeckoBrowserPersist browserPersist(browser); + return browserPersist.SaveURI(uri, filename); #else return FALSE; #endif diff --git a/data/Makefile.am b/data/Makefile.am index 632c2c9b..8e99b34f 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -1,6 +1,7 @@ -sugardir = $(pkgdatadir) +sugardir = $(pkgdatadir)/data sugar_DATA = \ - gtkrc \ + sugar.gtkrc \ + sugar-xo.gtkrc \ gecko-prefs.js \ mime.types diff --git a/data/gtkrc b/data/gtkrc deleted file mode 100644 index cc0d2caf..00000000 --- a/data/gtkrc +++ /dev/null @@ -1,6 +0,0 @@ -gtk-theme-name = "olpc" -gtk-icon-theme-name = "olpc" -gtk-font-name = "Bitstream Vera Sans 7" -gtk-icon-sizes = "gtk-menu=32,32:gtk-button=32,32" -gtk-cursor-theme-name = "olpc" -gtk-cursor-theme-size = 48 diff --git a/data/mime.types b/data/mime.types index 0f76f396..a6ffcb2b 100644 --- a/data/mime.types +++ b/data/mime.types @@ -1,2 +1,3 @@ application/x-squeak-project pr application/x-abiword abw +application/vnd.olpc-x-sugar xo diff --git a/data/sugar-xo.gtkrc b/data/sugar-xo.gtkrc new file mode 100644 index 00000000..6c6a1a66 --- /dev/null +++ b/data/sugar-xo.gtkrc @@ -0,0 +1,7 @@ +gtk-theme-name = "sugar-xo" +gtk-icon-theme-name = "sugar" +gtk-font-name = "Sans Serif 7" +gtk-icon-sizes = "gtk-menu=32,32:gtk-button=32,32" +gtk-cursor-theme-name = "sugar" +gtk-cursor-theme-size = 48 +gtk-toolbar-style = GTK_TOOLBAR_ICONS diff --git a/data/sugar.gtkrc b/data/sugar.gtkrc new file mode 100644 index 00000000..d7420268 --- /dev/null +++ b/data/sugar.gtkrc @@ -0,0 +1,5 @@ +gtk-theme-name = "sugar" +gtk-icon-theme-name = "sugar" +gtk-font-name = "Sans Serif 11" +gtk-cursor-theme-name = "sugar" +gtk-toolbar-style = GTK_TOOLBAR_ICONS diff --git a/services/clipboard/clipboardobject.py b/services/clipboard/clipboardobject.py index 919acd00..ab00b14d 100644 --- a/services/clipboard/clipboardobject.py +++ b/services/clipboard/clipboardobject.py @@ -1,3 +1,6 @@ +import os +import logging + import typeregistry class ClipboardObject: @@ -8,6 +11,10 @@ class ClipboardObject: self._percent = 0 self._formats = {} + def destroy(self): + for type, format in self._formats.iteritems(): + format.destroy() + def get_id(self): return self._id @@ -49,6 +56,10 @@ class Format: self._data = data self._on_disk = on_disk + def destroy(self): + if self._on_disk: + os.remove(self._data.replace('file://', '')) + def get_type(self): return self._type diff --git a/services/clipboard/clipboardservice.py b/services/clipboard/clipboardservice.py index 0ed423b5..16e37b19 100644 --- a/services/clipboard/clipboardservice.py +++ b/services/clipboard/clipboardservice.py @@ -109,7 +109,8 @@ class ClipboardDBusServiceHelper(dbus.service.Object): @dbus.service.method(_CLIPBOARD_DBUS_INTERFACE, in_signature="o", out_signature="") def delete_object(self, object_path): - del self._objects[str(object_path)] + cb_object = self._objects.pop(str(object_path)) + cb_object.destroy() self.object_deleted(object_path) logging.debug('Deleted object with object_id ' + object_path) diff --git a/services/clipboard/sugar-clipboard b/services/clipboard/sugar-clipboard index 9ee32e70..f9dcfb3d 100755 --- a/services/clipboard/sugar-clipboard +++ b/services/clipboard/sugar-clipboard @@ -32,7 +32,7 @@ import dbus.glib from sugar import env -sys.path.insert(0, env.get_service_path('clipboard')) +sys.path.append(env.get_service_path('clipboard')) from clipboardservice import ClipboardService diff --git a/services/clipboard/typeregistry.py b/services/clipboard/typeregistry.py index b794cee9..0cab0e9c 100644 --- a/services/clipboard/typeregistry.py +++ b/services/clipboard/typeregistry.py @@ -1,5 +1,7 @@ import logging from gettext import gettext as _ +import urlparse +import posixpath class FileType: def __init__(self, formats): @@ -197,6 +199,64 @@ class OOTextFileType(FileType): return mime_type in cls._types matches_mime_type = classmethod(matches_mime_type) +class UriListFileType(FileType): + + _types = set(['text/uri-list']) + + def _is_image(self): + uris = self._formats['text/uri-list'].get_data().split('\n') + if len(uris) == 1: + uri = urlparse.urlparse(uris[0]) + ext = posixpath.splitext(uri.path)[1] + logging.debug(ext) + # FIXME: Bad hack, the type registry should treat text/uri-list as a special case. + if ext in ['.jpg', '.jpeg', '.gif', '.png', '.svg']: + return True + + return False + + def get_name(self): + if self._is_image(): + return _('Image') + else: + return _('File') + + def get_icon(self): + if self._is_image(): + return 'theme:object-image' + else: + return 'theme:stock-missing' + + def get_preview(self): + return '' + + def get_activity(self): + return '' + + def matches_mime_type(cls, mime_type): + return mime_type in cls._types + matches_mime_type = classmethod(matches_mime_type) + +class XoFileType(FileType): + + _types = set(['application/vnd.olpc-x-sugar']) + + def get_name(self): + return _('Activity package') + + def get_icon(self): + return 'theme:stock-missing' + + def get_preview(self): + return '' + + def get_activity(self): + return '' + + def matches_mime_type(cls, mime_type): + return mime_type in cls._types + matches_mime_type = classmethod(matches_mime_type) + class UnknownFileType(FileType): def get_name(self): return _('Object') @@ -221,11 +281,13 @@ class TypeRegistry: self._types.append(MsWordFileType) self._types.append(RtfFileType) self._types.append(OOTextFileType) + self._types.append(UriListFileType) self._types.append(UriFileType) self._types.append(ImageFileType) self._types.append(AbiwordFileType) self._types.append(TextFileType) self._types.append(SqueakProjectFileType) + self._types.append(XoFileType) def get_type(self, formats): for file_type in self._types: diff --git a/services/console/sugar-console b/services/console/sugar-console index d4d7af02..af709a63 100755 --- a/services/console/sugar-console +++ b/services/console/sugar-console @@ -7,6 +7,6 @@ import os import sys from sugar import env -sys.path.insert(0, env.get_service_path('console')) +sys.path.append(env.get_service_path('console')) import console diff --git a/services/datastore/sugar-data-store b/services/datastore/sugar-data-store index 003e2739..eb325f23 100755 --- a/services/datastore/sugar-data-store +++ b/services/datastore/sugar-data-store @@ -24,7 +24,7 @@ import logging from sugar import logger from sugar import env -sys.path.insert(0, env.get_service_path('datastore')) +sys.path.append(env.get_service_path('datastore')) logger.start('data-store') logging.info('Starting the data store...') diff --git a/services/presence/__init__.py b/services/presence/__init__.py index e69de29b..bd64375a 100644 --- a/services/presence/__init__.py +++ b/services/presence/__init__.py @@ -0,0 +1,36 @@ +"""Service to track buddies and activities on the network + +Model objects: + + activity.Activity -- tracks a (shared/shareable) activity + with many properties and observable events + + buddy.Buddy -- tracks a reference to a particular actor + on the network + + buddy.GenericOwner -- actor who owns a particular + activity on the network + + buddy.ShellOwner -- actor who owns the local machine + connects to the owner module (on the server) + +Controller objects: + + presenceservice.PresenceService -- controller which connects + a networking plugin to a DBUS service. Generates events + for networking events, forwards updates/requests to the + server plugin. + + server_plugin.ServerPlugin -- implementation of networking + plugin using telepathy Python (Jabber) to provide the + underlying communications layer. Generates GObject + events that the PresenceService observes to forward onto + the DBUS clients. + +Utility machinery: + + buddyiconcache.BuddyIconCache -- caches buddy icons on disk + based on the "jid" XXX Jabber ID? of the buddy. + + psutils -- trivial function to decode int-list to characters +""" diff --git a/services/presence/activity.py b/services/presence/activity.py index 7471b46c..e9c3c05c 100644 --- a/services/presence/activity.py +++ b/services/presence/activity.py @@ -30,6 +30,9 @@ class DBusGObject(dbus.service.Object, gobject.GObject): __metaclass__ = DBusGOb class Activity(DBusGObject): + """Represents a potentially shareable activity on the network. + """ + __gtype_name__ = "Activity" __gsignals__ = { @@ -50,6 +53,15 @@ class Activity(DBusGObject): } def __init__(self, bus_name, object_id, tp, **kwargs): + """Initializes the activity and sets its properties to default values. + + bus_name -- DBUS name for lookup on local host + object_id -- The unique worldwide ID for this activity + tp -- The server plugin object (stands for "telepathy plugin") + kwargs -- Keyword arguments for the GObject properties + + """ + if not bus_name: raise ValueError("DBus bus name must be valid") if not object_id or not isinstance(object_id, int): @@ -88,6 +100,13 @@ class Activity(DBusGObject): tp.update_activity_properties(self._id) def do_get_property(self, pspec): + """Gets the value of a property associated with this activity. + + pspec -- Property specifier + + returns The value of the given property. + """ + if pspec.name == "id": return self._id elif pspec.name == "name": @@ -104,6 +123,15 @@ class Activity(DBusGObject): return self._local def do_set_property(self, pspec, value): + """Sets the value of a property associated with this activity. + + pspec -- Property specifier + value -- Desired value + + Note that the "type" property can be set only once; attempting to set it + to something different later will raise a RuntimeError. + + """ if pspec.name == "id": self._id = value elif pspec.name == "name": @@ -122,6 +150,15 @@ class Activity(DBusGObject): self._update_validity() def _update_validity(self): + """Sends a "validity-changed" signal if this activity's validity has changed. + + Determines whether this activity's status has changed from valid to + invalid, or invalid to valid, and emits a "validity-changed" signal + if either is true. "Valid" means that the object's type, ID, name, + colour and type properties have all been set to something valid + (i.e., not "None"). + + """ try: old_valid = self._valid if self._color and self._name and self._id and self._type: @@ -138,42 +175,80 @@ class Activity(DBusGObject): @dbus.service.signal(_ACTIVITY_INTERFACE, signature="o") def BuddyJoined(self, buddy_path): + """Generates DBUS signal when a buddy joins this activity. + + buddy_path -- DBUS path to buddy object + """ pass @dbus.service.signal(_ACTIVITY_INTERFACE, signature="o") def BuddyLeft(self, buddy_path): + """Generates DBUS signal when a buddy leaves this activity. + + buddy_path -- DBUS path to buddy object + """ pass @dbus.service.signal(_ACTIVITY_INTERFACE, signature="o") def NewChannel(self, channel_path): + """Generates DBUS signal when a new channel is created for this activity. + + channel_path -- DBUS path to new channel + + XXX - what is this supposed to do? Who is supposed to call it? + What is the channel path? Right now this is never called. + + """ pass # dbus methods @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="s") def GetId(self): + """DBUS method to get this activity's ID + + returns Activity ID + """ return self.props.id @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="s") def GetColor(self): + """DBUS method to get this activity's colour + + returns Activity colour + """ return self.props.color @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="s") def GetType(self): + """DBUS method to get this activity's type + + returns Activity type + """ return self.props.type @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="", async_callbacks=('async_cb', 'async_err_cb')) def Join(self, async_cb, async_err_cb): + """DBUS method to for the local user to attempt to join the activity + + async_cb -- Callback method to be called if join attempt is successful + async_err_cb -- Callback method to be called if join attempt is unsuccessful + + """ self.join(async_cb, async_err_cb) @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="ao") def GetJoinedBuddies(self): + """DBUS method to return a list of valid buddies who are joined in this activity + + returns A list of buddy object paths + """ ret = [] for buddy in self._buddies: if buddy.props.valid: @@ -183,18 +258,37 @@ class Activity(DBusGObject): @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="soao") def GetChannels(self): + """DBUS method to get the list of channels associated with this activity + + returns XXX - Not sure what this returns as get_channels doesn't actually return + a list of channels! + """ return self.get_channels() @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="s") def GetName(self): + """DBUS method to get this activity's name + + returns Activity name + """ return self.props.name # methods def object_path(self): + """Retrieves our dbus.ObjectPath object + + returns DBUS ObjectPath object + """ return dbus.ObjectPath(self._object_path) def get_joined_buddies(self): + """Local method to return a list of valid buddies who are joined in this activity + + This method is called by the PresenceService on the local machine. + + returns A list of buddy objects + """ ret = [] for buddy in self._buddies: if buddy.props.valid: @@ -202,18 +296,40 @@ class Activity(DBusGObject): return ret def buddy_joined(self, buddy): + """Adds a buddy to this activity and sends a BuddyJoined signal + + buddy -- Buddy object representing the buddy being added + + Adds a buddy to this activity if the buddy is not already in the buddy list. + If this activity is "valid", a BuddyJoined signal is also sent. + This method is called by the PresenceService on the local machine. + + """ if buddy not in self._buddies: self._buddies.append(buddy) if self.props.valid: self.BuddyJoined(buddy.object_path()) def buddy_left(self, buddy): + """Removes a buddy from this activity and sends a BuddyLeft signal. + + buddy -- Buddy object representing the buddy being removed + + Removes a buddy from this activity if the buddy is in the buddy list. + If this activity is "valid", a BuddyLeft signal is also sent. + This method is called by the PresenceService on the local machine. + + """ if buddy in self._buddies: self._buddies.remove(buddy) if self.props.valid: self.BuddyLeft(buddy.object_path()) def _handle_share_join(self, tp, text_channel): + """Called when a join to a network activity was successful. + + Called by the _shared_cb and _joined_cb methods. + """ if not text_channel: logging.debug("Error sharing: text channel was None, shouldn't happen") raise RuntimeError("Plugin returned invalid text channel") @@ -225,6 +341,8 @@ class Activity(DBusGObject): return True def _shared_cb(self, tp, activity_id, text_channel, exc, userdata): + """XXX - not documented yet + """ if activity_id != self.props.id: # Not for us return @@ -243,6 +361,11 @@ class Activity(DBusGObject): logging.debug("Share of activity %s succeeded." % self._id) def _share(self, (async_cb, async_err_cb), owner): + """XXX - not documented yet + + XXX - This method is called externally by the PresenceService despite the fact + that this is supposed to be an internal method! + """ logging.debug("Starting share of activity %s" % self._id) if self._joined: async_err_cb(RuntimeError("Already shared activity %s" % self.props.id)) @@ -252,6 +375,8 @@ class Activity(DBusGObject): logging.debug("done with share attempt %s" % self._id) def _joined_cb(self, tp, activity_id, text_channel, exc, userdata): + """XXX - not documented yet + """ if activity_id != self.props.id: # Not for us return @@ -266,6 +391,16 @@ class Activity(DBusGObject): async_cb() def join(self, async_cb, async_err_cb): + """Local method for the local user to attempt to join the activity. + + async_cb -- Callback method to be called if join attempt is successful + async_err_cb -- Callback method to be called if join attempt is unsuccessful + + The two callbacks are passed to the server_plugin ("tp") object, which in turn + passes them back as parameters in a callback to the _joined_cb method; this + callback is set up within this method. + + """ if self._joined: async_err_cb(RuntimeError("Already joined activity %s" % self.props.id)) return @@ -273,19 +408,35 @@ class Activity(DBusGObject): self._tp.join_activity(self.props.id, (sigid, async_cb, async_err_cb)) def get_channels(self): + """Local method to get the list of channels associated with this activity + + returns XXX - expected a list of channels, instead returning a tuple? ??? + """ conn = self._tp.get_connection() # FIXME add tubes and others channels return str(conn.service_name), conn.object_path, [self._text_channel.object_path] def leave(self): + """Local method called when the user wants to leave the activity. + + (XXX - doesn't appear to be called anywhere!) + + """ if self._joined: self._text_channel[CHANNEL_INTERFACE].Close() def _text_channel_closed_cb(self): + """Callback method called when the text channel is closed. + + This callback is set up in the _handle_share_join method. + """ self._joined = False self._text_channel = None def send_properties(self): + """Tells the Telepathy server what the properties of this activity are. + + """ props = {} props['name'] = self._name props['color'] = self._color @@ -293,6 +444,17 @@ class Activity(DBusGObject): self._tp.set_activity_properties(self.props.id, props) def set_properties(self, properties): + """Sets name, colour and/or type properties for this activity all at once. + + properties - Dictionary object containing properties keyed by property names + + Note that if any of the name, colour and/or type property values is changed from + what it originally was, the update_validity method will be called, resulting in + a "validity-changed" signal being generated. (Also note that unlike with the + do_set_property method, it *is* possible to change an already-set activity type + to something else; this may be a bug.) Called by the PresenceService on the + local machine. + """ changed = False if "name" in properties.keys(): name = properties["name"] diff --git a/services/presence/buddy.py b/services/presence/buddy.py index 35191e6c..ab91cea5 100644 --- a/services/presence/buddy.py +++ b/services/presence/buddy.py @@ -1,3 +1,4 @@ +"""An "actor" on the network, whether remote or local""" # Copyright (C) 2007, Red Hat, Inc. # Copyright (C) 2007, Collabora Ltd. # @@ -29,6 +30,7 @@ _BUDDY_INTERFACE = "org.laptop.Sugar.Presence.Buddy" _OWNER_INTERFACE = "org.laptop.Sugar.Presence.Buddy.Owner" class NotFoundError(dbus.DBusException): + """Raised when a given actor is not found on the network""" def __init__(self): dbus.DBusException.__init__(self) self._dbus_error_name = _PRESENCE_INTERFACE + '.NotFound' @@ -37,9 +39,35 @@ class DBusGObjectMetaclass(dbus.service.InterfaceType, gobject.GObjectMeta): pas class DBusGObject(dbus.service.Object, gobject.GObject): __metaclass__ = DBusGObjectMetaclass +_PROP_NICK = "nick" +_PROP_KEY = "key" +_PROP_ICON = "icon" +_PROP_CURACT = "current-activity" +_PROP_COLOR = "color" +_PROP_OWNER = "owner" +_PROP_VALID = "valid" + class Buddy(DBusGObject): - """Represents another person on the network and keeps track of the - activities and resources they make available for sharing.""" + """Person on the network (tracks properties and shared activites) + + The Buddy is a collection of metadata describing a particular + actor/person on the network. The Buddy object tracks a set of + activities which the actor has shared with the presence service. + + Buddies have a "valid" property which is used to flag Buddies + which are no longer reachable. That is, a Buddy may represent + a no-longer reachable target on the network. + + The Buddy emits GObject events that the PresenceService uses + to track changes in its status. + + Attributes: + + _activities -- dictionary mapping activity ID to + activity.Activity objects + handles -- dictionary mapping telepresence client to + "handle" (XXX what's that) + """ __gsignals__ = { 'validity-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, @@ -51,17 +79,26 @@ class Buddy(DBusGObject): } __gproperties__ = { - 'key' : (str, None, None, None, + _PROP_KEY : (str, None, None, None, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT_ONLY), - 'icon' : (object, None, None, gobject.PARAM_READWRITE), - 'nick' : (str, None, None, None, gobject.PARAM_READWRITE), - 'color' : (str, None, None, None, gobject.PARAM_READWRITE), - 'current-activity' : (str, None, None, None, gobject.PARAM_READWRITE), - 'valid' : (bool, None, None, False, gobject.PARAM_READABLE), - 'owner' : (bool, None, None, False, gobject.PARAM_READABLE) + _PROP_ICON : (object, None, None, gobject.PARAM_READWRITE), + _PROP_NICK : (str, None, None, None, gobject.PARAM_READWRITE), + _PROP_COLOR : (str, None, None, None, gobject.PARAM_READWRITE), + _PROP_CURACT : (str, None, None, None, gobject.PARAM_READWRITE), + _PROP_VALID : (bool, None, None, False, gobject.PARAM_READABLE), + _PROP_OWNER : (bool, None, None, False, gobject.PARAM_READABLE) } def __init__(self, bus_name, object_id, **kwargs): + """Initialize the Buddy object + + bus_name -- DBUS object bus name (identifier) + object_id -- the activity's unique identifier + kwargs -- used to initialize the object's properties + + constructs a DBUS "object path" from the _BUDDY_PATH + and object_id + """ if not bus_name: raise ValueError("DBus bus name must be valid") if not object_id or not isinstance(object_id, int): @@ -83,44 +120,62 @@ class Buddy(DBusGObject): self._nick = None self._color = None - if not kwargs.get("key"): + if not kwargs.get(_PROP_KEY): raise ValueError("key required") + _ALLOWED_INIT_PROPS = [_PROP_NICK, _PROP_KEY, _PROP_ICON, _PROP_CURACT, _PROP_COLOR] + for (key, value) in kwargs.items(): + if key not in _ALLOWED_INIT_PROPS: + logging.debug("Invalid init property '%s'; ignoring..." % key) + del kwargs[key] + gobject.GObject.__init__(self, **kwargs) def do_get_property(self, pspec): - if pspec.name == "key": + """Retrieve current value for the given property specifier + + pspec -- property specifier with a "name" attribute + """ + if pspec.name == _PROP_KEY: return self._key - elif pspec.name == "icon": + elif pspec.name == _PROP_ICON: return self._icon - elif pspec.name == "nick": + elif pspec.name == _PROP_NICK: return self._nick - elif pspec.name == "color": + elif pspec.name == _PROP_COLOR: return self._color - elif pspec.name == "current-activity": + elif pspec.name == _PROP_CURACT: if not self._current_activity: return None if not self._activities.has_key(self._current_activity): return None return self._current_activity - elif pspec.name == "valid": + elif pspec.name == _PROP_VALID: return self._valid - elif pspec.name == "owner": + elif pspec.name == _PROP_OWNER: return self._owner def do_set_property(self, pspec, value): - if pspec.name == "icon": + """Set given property + + pspec -- property specifier with a "name" attribute + value -- value to set + + emits 'icon-changed' signal on icon setting + calls _update_validity on all calls + """ + if pspec.name == _PROP_ICON: if str(value) != self._icon: self._icon = str(value) self.IconChanged(self._icon) self.emit('icon-changed', self._icon) - elif pspec.name == "nick": + elif pspec.name == _PROP_NICK: self._nick = value - elif pspec.name == "color": + elif pspec.name == _PROP_COLOR: self._color = value - elif pspec.name == "current-activity": + elif pspec.name == _PROP_CURACT: self._current_activity = value - elif pspec.name == "key": + elif pspec.name == _PROP_KEY: self._key = value self._update_validity() @@ -129,27 +184,42 @@ class Buddy(DBusGObject): @dbus.service.signal(_BUDDY_INTERFACE, signature="ay") def IconChanged(self, icon_data): - pass + """Generates DBUS signal with icon_data""" @dbus.service.signal(_BUDDY_INTERFACE, signature="o") def JoinedActivity(self, activity_path): - pass + """Generates DBUS signal when buddy joins activity + + activity_path -- DBUS path to the activity object + """ @dbus.service.signal(_BUDDY_INTERFACE, signature="o") def LeftActivity(self, activity_path): - pass + """Generates DBUS signal when buddy leaves activity + + activity_path -- DBUS path to the activity object + """ @dbus.service.signal(_BUDDY_INTERFACE, signature="a{sv}") def PropertyChanged(self, updated): - pass + """Generates DBUS signal when buddy's property changes + + updated -- updated property-set (dictionary) with the + Buddy's property (changed) values. Note: not the + full set of properties, just the changes. + """ # dbus methods @dbus.service.method(_BUDDY_INTERFACE, in_signature="", out_signature="ay") def GetIcon(self): + """Retrieve Buddy's icon data + + returns empty string or dbus.ByteArray + """ if not self.props.icon: return "" return dbus.ByteArray(self.props.icon) @@ -157,6 +227,11 @@ class Buddy(DBusGObject): @dbus.service.method(_BUDDY_INTERFACE, in_signature="", out_signature="ao") def GetJoinedActivities(self): + """Retrieve set of Buddy's joined activities (paths) + + returns list of dbus service paths for the Buddy's joined + activities + """ acts = [] for act in self.get_joined_activities(): acts.append(act.object_path()) @@ -165,22 +240,41 @@ class Buddy(DBusGObject): @dbus.service.method(_BUDDY_INTERFACE, in_signature="", out_signature="a{sv}") def GetProperties(self): + """Retrieve set of Buddy's properties + + returns dictionary of + nick : str(nickname) + owner : bool( whether this Buddy is an owner??? ) + XXX what is the owner flag for? + key : str(public-key) + color: Buddy's icon colour + XXX what type? + current-activity: Buddy's current activity_id, or + "" if no current activity + """ props = {} - props['nick'] = self.props.nick - props['owner'] = self.props.owner - props['key'] = self.props.key - props['color'] = self.props.color + props[_PROP_NICK] = self.props.nick + props[_PROP_OWNER] = self.props.owner + props[_PROP_KEY] = self.props.key + props[_PROP_COLOR] = self.props.color if self.props.current_activity: - props['current-activity'] = self.props.current_activity + props[_PROP_CURACT] = self.props.current_activity else: - props['current-activity'] = "" + props[_PROP_CURACT] = "" return props # methods def object_path(self): + """Retrieve our dbus.ObjectPath object""" return dbus.ObjectPath(self._object_path) def add_activity(self, activity): + """Add an activity to the Buddy's set of activities + + activity -- activity.Activity instance + + calls JoinedActivity + """ actid = activity.props.id if self._activities.has_key(actid): return @@ -189,6 +283,12 @@ class Buddy(DBusGObject): self.JoinedActivity(activity.object_path()) def remove_activity(self, activity): + """Remove the activity from the Buddy's set of activities + + activity -- activity.Activity instance + + calls LeftActivity + """ actid = activity.props.id if not self._activities.has_key(actid): return @@ -197,6 +297,7 @@ class Buddy(DBusGObject): self.LeftActivity(activity.object_path()) def get_joined_activities(self): + """Retrieves list of still-valid activity objects""" acts = [] for act in self._activities.values(): if act.props.valid: @@ -204,36 +305,54 @@ class Buddy(DBusGObject): return acts def set_properties(self, properties): + """Set the given set of properties on the object + + properties -- set of property values to set + + if no change, no events generated + if change, generates property-changed and + calls _update_validity + """ changed = False - if "nick" in properties.keys(): - nick = properties["nick"] + changed_props = {} + if _PROP_NICK in properties.keys(): + nick = properties[_PROP_NICK] if nick != self._nick: self._nick = nick + changed_props[_PROP_NICK] = nick changed = True - if "color" in properties.keys(): - color = properties["color"] + if _PROP_COLOR in properties.keys(): + color = properties[_PROP_COLOR] if color != self._color: self._color = color + changed_props[_PROP_COLOR] = color changed = True - if "current-activity" in properties.keys(): - curact = properties["current-activity"] + if _PROP_CURACT in properties.keys(): + curact = properties[_PROP_CURACT] if curact != self._current_activity: self._current_activity = curact + changed_props[_PROP_CURACT] = curact changed = True - if not changed: + if not changed or not len(changed_props.keys()): return # Try emitting PropertyChanged before updating validity # to avoid leaking a PropertyChanged signal before the buddy is # actually valid the first time after creation if self._valid: - self.PropertyChanged(properties) - self.emit('property-changed', properties) + self.PropertyChanged(changed_props) + self.emit('property-changed', changed_props) self._update_validity() def _update_validity(self): + """Check whether we are now valid + + validity is True if color, nick and key are non-null + + emits validity-changed if we have changed validity + """ try: old_valid = self._valid if self._color and self._nick and self._key: @@ -247,6 +366,13 @@ class Buddy(DBusGObject): self._valid = False class GenericOwner(Buddy): + """Common functionality for Local User-like objects + + The TestOwner wants to produce something *like* a + ShellOwner, but with randomised changes and the like. + This class provides the common features for a real + local owner and a testing one. + """ __gtype_name__ = "GenericOwner" __gproperties__ = { @@ -255,7 +381,17 @@ class GenericOwner(Buddy): 'key-hash' : (str, None, None, None, gobject.PARAM_READABLE | gobject.PARAM_CONSTRUCT) } - def __init__(self, bus_name, object_id, **kwargs): + def __init__(self, ps, bus_name, object_id, **kwargs): + """Initialize the GenericOwner instance + + ps -- presenceservice.PresenceService object + bus_name -- DBUS object bus name (identifier) + object_id -- the activity's unique identifier + kwargs -- used to initialize the object's properties + + calls Buddy.__init__ + """ + self._ps = ps self._server = 'olpc.collabora.co.uk' self._key_hash = None self._registered = False @@ -273,28 +409,47 @@ class GenericOwner(Buddy): self._owner = True def get_registered(self): + """Retrieve whether owner has registered with presence server""" return self._registered def get_server(self): + """Retrieve presence server (XXX url??)""" return self._server def get_key_hash(self): + """Retrieve the user's private-key hash""" return self._key_hash def set_registered(self, registered): + """Customisation point: handle the registration of the owner""" raise RuntimeError("Subclasses must implement") class ShellOwner(GenericOwner): - """Class representing the owner of the machine. This is the client - portion of the Owner, paired with the server portion in Owner.py.""" - + """Representation of the local-machine owner using Sugar's Shell + + The ShellOwner uses the Sugar Shell's dbus services to + register for updates about the user's profile description. + """ __gtype_name__ = "ShellOwner" _SHELL_SERVICE = "org.laptop.Shell" _SHELL_OWNER_INTERFACE = "org.laptop.Shell.Owner" _SHELL_PATH = "/org/laptop/Shell" - def __init__(self, bus_name, object_id, test=False): + def __init__(self, ps, bus_name, object_id, test=False): + """Initialize the ShellOwner instance + + ps -- presenceservice.PresenceService object + bus_name -- DBUS object bus name (identifier) + object_id -- the activity's unique identifier + test -- ignored + + Retrieves initial property values from the profile + module. Loads the buddy icon from file as well. + XXX note: no error handling on that + + calls GenericOwner.__init__ + """ server = profile.get_server() key_hash = profile.get_private_key_hash() registered = profile.get_server_registered() @@ -307,7 +462,7 @@ class ShellOwner(GenericOwner): icon = f.read() f.close() - GenericOwner.__init__(self, bus_name, object_id, key=key, nick=nick, + GenericOwner.__init__(self, ps, bus_name, object_id, key=key, nick=nick, color=color, icon=icon, server=server, key_hash=key_hash, registered=registered) @@ -324,10 +479,17 @@ class ShellOwner(GenericOwner): pass def set_registered(self, value): + """Handle notification that we have been registered""" if value: profile.set_server_registered() def _name_owner_changed_handler(self, name, old, new): + """Handle DBUS notification of a new / renamed service + + Watches for the _SHELL_SERVICE, i.e. the Sugar Shell, + and registers with it if we have not yet registered + with it (using _connect_to_shell). + """ if name != self._SHELL_SERVICE: return if (old and len(old)) and (not new and not len(new)): @@ -338,6 +500,11 @@ class ShellOwner(GenericOwner): self._connect_to_shell() def _connect_to_shell(self): + """Connect to the Sugar Shell service to watch for events + + Connects the various XChanged events on the Sugar Shell + service to our _x_changed_cb methods. + """ obj = self._bus.get_object(self._SHELL_SERVICE, self._SHELL_PATH) self._shell_owner = dbus.Interface(obj, self._SHELL_OWNER_INTERFACE) self._shell_owner.connect_to_signal('IconChanged', self._icon_changed_cb) @@ -347,21 +514,30 @@ class ShellOwner(GenericOwner): self._cur_activity_changed_cb) def _icon_changed_cb(self, icon): + """Handle icon change, set property to generate event""" self.props.icon = icon def _color_changed_cb(self, color): - props = {'color': color} + """Handle color change, set property to generate event""" + props = {_PROP_COLOR: color} self.set_properties(props) def _nick_changed_cb(self, nick): - props = {'nick': nick} + """Handle nickname change, set property to generate event""" + props = {_PROP_NICK: nick} self.set_properties(props) def _cur_activity_changed_cb(self, activity_id): + """Handle current-activity change, set property to generate event + + Filters out local activities (those not in self.activites) + because the network users can't join those activities, so + the activity_id shared will be None in those cases... + """ if not self._activities.has_key(activity_id): # This activity is local-only activity_id = None - props = {'current-activity': activity_id} + props = {_PROP_CURACT: activity_id} self.set_properties(props) @@ -371,9 +547,12 @@ class TestOwner(GenericOwner): __gtype_name__ = "TestOwner" - def __init__(self, bus_name, object_id, test_num): + def __init__(self, ps, bus_name, object_id, test_num): self._cp = ConfigParser() self._section = "Info" + self._test_activities = [] + self._test_cur_act = "" + self._change_timeout = 0 self._cfg_file = os.path.join(env.get_profile_path(), 'test-buddy-%d' % test_num) @@ -392,11 +571,46 @@ class TestOwner(GenericOwner): color = xocolor.XoColor().to_string() icon = _get_random_image() - GenericOwner.__init__(self, bus_name, object_id, key=pubkey, nick=nick, + logging.debug("pubkey is %s" % pubkey) + GenericOwner.__init__(self, ps, bus_name, object_id, key=pubkey, nick=nick, color=color, icon=icon, registered=registered, key_hash=privkey_hash) + self._ps.connect('connection-status', self._ps_connection_status_cb) + + def _share_reply_cb(self, actid, object_path): + activity = self._ps.internal_get_activity(actid) + if not activity or not object_path: + logging.debug("Couldn't find activity %s even though it was shared." % actid) + return + logging.debug("Shared activity %s (%s)." % (actid, activity.props.name)) + self._test_activities.append(activity) + + def _share_error_cb(self, actid, err): + logging.debug("Error sharing activity %s: %s" % (actid, str(err))) + + def _ps_connection_status_cb(self, ps, connected): + if not connected: + return + + if not len(self._test_activities): + # Share some activities + actid = util.unique_id("Activity 1") + callbacks = (lambda *args: self._share_reply_cb(actid, *args), + lambda *args: self._share_error_cb(actid, *args)) + atype = "org.laptop.WebActivity" + properties = {"foo": "bar"} + self._ps._share_activity(actid, atype, "Wembley Stadium", properties, callbacks) + + actid2 = util.unique_id("Activity 2") + callbacks = (lambda *args: self._share_reply_cb(actid2, *args), + lambda *args: self._share_error_cb(actid2, *args)) + atype = "org.laptop.WebActivity" + properties = {"baz": "bar"} + self._ps._share_activity(actid2, atype, "Maine Road", properties, callbacks) + # Change a random property ever 10 seconds - gobject.timeout_add(10000, self._update_something) + if self._change_timeout == 0: + self._change_timeout = gobject.timeout_add(10000, self._update_something) def set_registered(self, value): if value: @@ -437,23 +651,26 @@ class TestOwner(GenericOwner): self.props.icon = _get_random_image() elif it == 1: from sugar.graphics import xocolor - props = {'color': xocolor.XoColor().to_string()} + props = {_PROP_COLOR: xocolor.XoColor().to_string()} self.set_properties(props) elif it == 2: - props = {'nick': _get_random_name()} + props = {_PROP_NICK: _get_random_name()} self.set_properties(props) elif it == 3: - bork = random.randint(25, 65) - it = "" - for i in range(0, bork): - it += chr(random.randint(40, 127)) - from sugar import util - props = {'current-activity': util.unique_id(it)} + actid = "" + idx = random.randint(0, len(self._test_activities)) + # if idx == len(self._test_activites), it means no current + # activity + if idx < len(self._test_activities): + activity = self._test_activities[idx] + actid = activity.props.id + props = {_PROP_CURACT: actid} self.set_properties(props) return True def _hash_private_key(self): + """Unused method to has a private key, see profile""" self.privkey_hash = None key_path = os.path.join(env.get_profile_path(), 'owner.key') @@ -504,6 +721,7 @@ def _extract_public_key(keyfile): return key def _extract_private_key(keyfile): + """Get a private key from a private key file""" # Extract the private key try: f = open(keyfile, "r") @@ -527,6 +745,7 @@ def _extract_private_key(keyfile): return key def _get_new_keypair(num): + """Retrieve a public/private key pair for testing""" # Generate keypair privkeyfile = os.path.join("/tmp", "test%d.key" % num) pubkeyfile = os.path.join("/tmp", 'test%d.key.pub' % num) @@ -559,10 +778,12 @@ def _get_new_keypair(num): return (pubkey, privkey) def _get_random_name(): + """Produce random names for testing""" names = ["Liam", "Noel", "Guigsy", "Whitey", "Bonehead"] return names[random.randint(0, len(names) - 1)] def _get_random_image(): + """Produce a random image for display""" import cairo, math, random, gtk def rand(): diff --git a/services/presence/presenceservice.py b/services/presence/presenceservice.py index fd4cbb6b..1312fec0 100644 --- a/services/presence/presenceservice.py +++ b/services/presence/presenceservice.py @@ -40,24 +40,36 @@ class NotFoundError(dbus.DBusException): dbus.DBusException.__init__(self) self._dbus_error_name = _PRESENCE_INTERFACE + '.NotFound' +class DBusGObjectMetaclass(dbus.service.InterfaceType, gobject.GObjectMeta): pass +class DBusGObject(dbus.service.Object, gobject.GObject): __metaclass__ = DBusGObjectMetaclass + +class PresenceService(DBusGObject): + __gtype_name__ = "PresenceService" + + __gsignals__ = { + 'connection-status': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_BOOLEAN])) + } -class PresenceService(dbus.service.Object): def __init__(self, test=0): self._next_object_id = 0 + self._connected = False self._buddies = {} # key -> Buddy self._handles_buddies = {} # tp client -> (handle -> Buddy) self._activities = {} # activity id -> Activity + gobject.GObject.__init__(self) + bus = dbus.SessionBus() self._bus_name = dbus.service.BusName(_PRESENCE_SERVICE, bus=bus) # Create the Owner object objid = self._get_next_object_id() if test > 0: - self._owner = TestOwner(self._bus_name, objid, test) + self._owner = TestOwner(self, self._bus_name, objid, test) else: - self._owner = ShellOwner(self._bus_name, objid) + self._owner = ShellOwner(self, self._bus_name, objid) self._buddies[self._owner.props.key] = self._owner self._registry = ManagerRegistry() @@ -92,8 +104,15 @@ class PresenceService(dbus.service.Object): async_err_cb(exc) def _server_status_cb(self, plugin, status, reason): + # FIXME: figure out connection status when we have a salut plugin too + old_status = self._connected if status == CONNECTION_STATUS_CONNECTED: - pass + self._connected = True + else: + self._connected = False + + if self._connected != old_status: + self.emit('connection-status', self._connected) def _contact_online(self, tp, handle, props): new_buddy = False @@ -120,8 +139,11 @@ class PresenceService(dbus.service.Object): else: self.BuddyDisappeared(buddy.object_path()) logging.debug("Buddy left: %s (%s)" % (buddy.props.nick, buddy.props.color)) - + def _contact_offline(self, tp, handle): + if not self._handles_buddies[tp].has_key(handle): + return + buddy = self._handles_buddies[tp].pop(handle) key = buddy.props.key @@ -144,11 +166,11 @@ class PresenceService(dbus.service.Object): logging.debug("Buddy %s icon updated" % buddy.props.nick) buddy.props.icon = avatar - def _buddy_properties_changed(self, tp, handle, prop): + def _buddy_properties_changed(self, tp, handle, properties): buddy = self._handles_buddies[tp].get(handle) if buddy: - buddy.set_properties(prop) - logging.debug("Buddy %s properties updated" % buddy.props.nick) + buddy.set_properties(properties) + logging.debug("Buddy %s properties updated: %s" % (buddy.props.nick, properties.keys())) def _new_activity(self, activity_id, tp): try: @@ -257,11 +279,10 @@ class PresenceService(dbus.service.Object): @dbus.service.method(_PRESENCE_INTERFACE, in_signature="s", out_signature="o") def GetActivityById(self, actid): - if self._activities.has_key(actid): - act = self._activities[actid] - if act.props.valid: - return act.object_path() - raise NotFoundError("The activity was not found.") + act = self.internal_get_activity(actid) + if not act or not act.props.valid: + raise NotFoundError("The activity was not found.") + return act.object_path() @dbus.service.method(_PRESENCE_INTERFACE, out_signature="ao") def GetBuddies(self): @@ -330,6 +351,11 @@ class PresenceService(dbus.service.Object): if activity: activity.set_properties(props) + def internal_get_activity(self, actid): + if not self._activities.has_key(actid): + return None + return self._activities[actid] + def main(test=False): loop = gobject.MainLoop() diff --git a/services/presence/psutils.py b/services/presence/psutils.py index 995ea3ab..76583d65 100644 --- a/services/presence/psutils.py +++ b/services/presence/psutils.py @@ -16,7 +16,11 @@ def bytes_to_string(bytes): - # Handle both DBus byte arrays and strings + """The function converts a D-BUS byte array provided by dbus to string format. + + bytes -- a D-Bus array of bytes. Handle both DBus byte arrays and strings + + """ try: # DBus Byte array ret = ''.join([chr(item) for item in bytes]) diff --git a/services/presence/server_plugin.py b/services/presence/server_plugin.py index 5180692f..53305678 100644 --- a/services/presence/server_plugin.py +++ b/services/presence/server_plugin.py @@ -1,3 +1,4 @@ +"""Telepathy-python presence server interface/implementation plugin""" # Copyright (C) 2007, Red Hat, Inc. # Copyright (C) 2007, Collabora Ltd. # @@ -41,7 +42,7 @@ CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' _PROTOCOL = "jabber" class InvalidBuddyError(Exception): - pass + """(Unused) exception to indicate an invalid buddy specifier""" def _buddy_icon_save_cb(buf, data): data[0] += buf @@ -76,6 +77,13 @@ def _get_buddy_icon_at_size(icon, maxw, maxh, maxsize): class ServerPlugin(gobject.GObject): + """Telepathy-python-based presence server interface + + The ServerPlugin instance translates network events from + Telepathy Python into GObject events. It provides direct + python calls to perform the required network operations + to implement the PresenceService. + """ __gsignals__ = { 'contact-online': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), @@ -104,6 +112,14 @@ class ServerPlugin(gobject.GObject): } def __init__(self, registry, owner): + """Initialize the ServerPlugin instance + + registry -- telepathy.client.ManagerRegistry from the + PresenceService, used to find the "gabble" connection + manager in this case... + owner -- presence.buddy.GenericOwner instance (normally a + presence.buddy.ShellOwner instance) + """ gobject.GObject.__init__(self) self._icon_cache = BuddyIconCache() @@ -125,6 +141,20 @@ class ServerPlugin(gobject.GObject): self._reconnect_id = 0 def _owner_property_changed_cb(self, owner, properties): + """Local user's configuration properties have changed + + owner -- the Buddy object for the local user + properties -- set of updated properties + + calls: + + _set_self_current_activity current-activity + _set_self_alias nick + _set_self_olpc_properties color + + depending on which properties are present in the + set of properties. + """ logging.debug("Owner properties changed: %s" % properties) if properties.has_key("current-activity"): @@ -137,10 +167,21 @@ class ServerPlugin(gobject.GObject): self._set_self_olpc_properties() def _owner_icon_changed_cb(self, owner, icon): + """Owner has changed their icon, forward to network""" logging.debug("Owner icon changed to size %d" % len(str(icon))) self._set_self_avatar(icon) def _get_account_info(self): + """Retrieve metadata dictionary describing this account + + returns dictionary with: + + server : server url from owner + account : printable-ssh-key-hash@server + password : ssh-key-hash + register : whether to register (i.e. whether not yet + registered) + """ account_info = {} account_info['server'] = self._owner.get_server() @@ -155,6 +196,16 @@ class ServerPlugin(gobject.GObject): return account_info def _find_existing_connection(self): + """Try to find an existing Telepathy connection to this server + + filters the set of connections from + telepathy.client.Connection.get_connections + to find a connection using our protocol with the + "self handle" of that connection being a handle + which matches our account (see _get_account_info) + + returns connection or None + """ our_name = self._account['account'] # Search existing connections, if any, that we might be able to use @@ -173,9 +224,20 @@ class ServerPlugin(gobject.GObject): return None def get_connection(self): + """Retrieve our telepathy.client.Connection object""" return self._conn def _init_connection(self): + """Set up our connection + + if there is no existing connection + (_find_existing_connection returns None) + produce a new connection with our protocol for our + account. + + if there is an existing connection, reuse it by + registering for various of events on it. + """ conn = self._find_existing_connection() if not conn: acct = self._account.copy() @@ -201,6 +263,10 @@ class ServerPlugin(gobject.GObject): return conn def _request_list_channel(self, name): + """Request a contact-list channel from Telepathy + + name -- publish/subscribe, for the type of channel + """ handle = self._conn[CONN_INTERFACE].RequestHandles( CONNECTION_HANDLE_TYPE_LIST, [name])[0] chan_path = self._conn[CONN_INTERFACE].RequestChannel( @@ -212,6 +278,9 @@ class ServerPlugin(gobject.GObject): return channel def _connected_cb(self): + """Callback on successful connection to a server + """ + if self._account['register']: # we successfully register this account self._owner.props.registered = True @@ -257,7 +326,7 @@ class ServerPlugin(gobject.GObject): self._activity_properties_changed_cb) # Set initial buddy properties, avatar, and activities - self._set_self_olpc_properties(True) + self._set_self_olpc_properties() self._set_self_alias() self._set_self_activities() self._set_self_current_activity() @@ -324,34 +393,58 @@ class ServerPlugin(gobject.GObject): def _internal_join_activity(self, activity_id, signal, userdata): handle = self._activities.get(activity_id) if not handle: - self._conn[CONN_INTERFACE].RequestHandles(CONNECTION_HANDLE_TYPE_ROOM, [activity_id], + # FIXME: figure out why the server can't figure this out itself + room_jid = activity_id + "@conference." + self._account["server"] + self._conn[CONN_INTERFACE].RequestHandles(CONNECTION_HANDLE_TYPE_ROOM, [room_jid], reply_handler=lambda *args: self._join_activity_get_channel_cb(activity_id, signal, userdata, *args), error_handler=lambda *args: self._join_error_cb(activity_id, signal, userdata, *args)) else: self._join_activity_get_channel_cb(activity_id, userdata, [handle]) def share_activity(self, activity_id, userdata): + """Share activity with the network + + activity_id -- unique ID for the activity + userdata -- opaque token to be passed in the resulting event + (id, callback, errback) normally + + Asks the Telepathy server to create a "conference" channel + for the activity or return a handle to an already created + conference channel for the activity. + """ self._internal_join_activity(activity_id, "activity-shared", userdata) def join_activity(self, activity_id, userdata): + """Join an activity on the network (or locally) + + activity_id -- unique ID for the activity + userdata -- opaque token to be passed in the resulting event + (id, callback, errback) normally + + Asks the Telepathy server to create a "conference" channel + for the activity or return a handle to an already created + conference channel for the activity. + """ self._internal_join_activity(activity_id, "activity-joined", userdata) def _ignore_success_cb(self): - pass + """Ignore an event (null-operation)""" def _log_error_cb(self, msg, err): + """Log a message (error) at debug level with prefix msg""" logging.debug("Error %s: %s" % (msg, err)) - def _set_self_olpc_properties(self, set_key=False): + def _set_self_olpc_properties(self): + """Set color and key on our Telepathy server identity""" props = {} props['color'] = self._owner.props.color - if set_key: - props['key'] = dbus.ByteArray(self._owner.props.key) + props['key'] = dbus.ByteArray(self._owner.props.key) self._conn[CONN_INTERFACE_BUDDY_INFO].SetProperties(props, reply_handler=self._ignore_success_cb, error_handler=lambda *args: self._log_error_cb("setting properties", *args)) def _set_self_alias(self): + """Forwarded to SetActivities on AliasInfo channel""" alias = self._owner.props.nick self_handle = self._conn[CONN_INTERFACE].GetSelfHandle() self._conn[CONN_INTERFACE_ALIASING].SetAliases({self_handle : alias}, @@ -359,11 +452,19 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._log_error_cb("setting alias", *args)) def _set_self_activities(self): + """Forward set of joined activities to network + + uses SetActivities on BuddyInfo channel + """ self._conn[CONN_INTERFACE_BUDDY_INFO].SetActivities(self._joined_activities, reply_handler=self._ignore_success_cb, error_handler=lambda *args: self._log_error_cb("setting activities", *args)) def _set_self_current_activity(self): + """Forward our current activity (or "") to network + + uses SetCurrentActivity on BuddyInfo channel + """ cur_activity = self._owner.props.current_activity cur_activity_handle = 0 if not cur_activity: @@ -381,12 +482,20 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._log_error_cb("setting current activity", *args)) def _get_handle_for_activity(self, activity_id): + """Retrieve current handle for given activity or None""" for (act, handle) in self._joined_activities: if activity_id == act: return handle return None def _status_changed_cb(self, state, reason): + """Handle notification of connection-status change + + state -- CONNECTION_STATUS_* + reason -- integer code describing the reason... + + returns False XXX what does that mean? + """ if state == CONNECTION_STATUS_CONNECTING: logging.debug("State: connecting...") elif state == CONNECTION_STATUS_CONNECTED: @@ -403,6 +512,16 @@ class ServerPlugin(gobject.GObject): return False def start(self): + """Start up the Telepathy networking connections + + if we are already connected, query for the initial contact + information. + + if we are already connecting, do nothing + + otherwise initiate a connection and transfer control to + _connect_reply_cb or _connect_error_cb + """ logging.debug("Starting up...") # If the connection is already connected query initial contacts conn_status = self._conn[CONN_INTERFACE].GetStatus() @@ -418,25 +537,30 @@ class ServerPlugin(gobject.GObject): error_handler=self._connect_error_cb) def _connect_reply_cb(self): + """Handle connection success""" if self._reconnect_id > 0: gobject.source_remove(self._reconnect_id) def _reconnect(self): + """Reset number-of-attempted connections and re-attempt""" self._reconnect_id = 0 self.start() return False def _connect_error_cb(self, exception): + """Handle connection failure""" logging.debug("Connect error: %s" % exception) if not self._reconnect_id: self._reconnect_id = gobject.timeout_add(10000, self._reconnect) def cleanup(self): + """If we still have a connection, disconnect it""" if not self._conn: return self._conn[CONN_INTERFACE].Disconnect() def _contact_offline(self, handle): + """Handle contact going offline (send message, update set)""" if not self._online_contacts.has_key(handle): return if self._online_contacts[handle]: @@ -444,13 +568,16 @@ class ServerPlugin(gobject.GObject): del self._online_contacts[handle] def _contact_online_activities_cb(self, handle, activities): + """Handle contact's activity list update""" self._buddy_activities_changed_cb(handle, activities) def _contact_online_activities_error_cb(self, handle, err): + """Handle contact's activity list being unavailable""" logging.debug("Handle %s - Error getting activities: %s" % (handle, err)) self._contact_offline(handle) def _contact_online_aliases_cb(self, handle, props, aliases): + """Handle contact's alias being received (do further queries)""" if not aliases or not len(aliases): logging.debug("Handle %s - No aliases" % handle) self._contact_offline(handle) @@ -466,10 +593,12 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._contact_online_activities_error_cb(handle, *args)) def _contact_online_aliases_error_cb(self, handle, err): + """Handle failure to retrieve given user's alias/information""" logging.debug("Handle %s - Error getting nickname: %s" % (handle, err)) self._contact_offline(handle) def _contact_online_properties_cb(self, handle, props): + """Handle failure to retrieve given user's alias/information""" if not props.has_key('key'): logging.debug("Handle %s - invalid key." % handle) self._contact_offline(handle) @@ -487,16 +616,19 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._contact_online_aliases_error_cb(handle, *args)) def _contact_online_properties_error_cb(self, handle, err): + """Handle error retrieving property-set for a user (handle)""" logging.debug("Handle %s - Error getting properties: %s" % (handle, err)) self._contact_offline(handle) def _contact_online(self, handle): + """Handle a contact coming online""" self._online_contacts[handle] = None self._conn[CONN_INTERFACE_BUDDY_INFO].GetProperties(handle, reply_handler=lambda *args: self._contact_online_properties_cb(handle, *args), error_handler=lambda *args: self._contact_online_properties_error_cb(handle, *args)) def _presence_update_cb(self, presence): + """Send update for online/offline status of presence""" for handle in presence: timestamp, statuses = presence[handle] online = handle in self._online_contacts @@ -511,18 +643,19 @@ class ServerPlugin(gobject.GObject): self._contact_offline(handle) def _avatar_updated_cb(self, handle, new_avatar_token): + """Handle update of given user (handle)'s avatar""" if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): # ignore network events for Owner property changes since those # are handled locally return if not self._online_contacts.has_key(handle): - logging.debug("Handle %s not valid yet...") + logging.debug("Handle %s unknown." % handle) return jid = self._online_contacts[handle] if not jid: - logging.debug("Handle %s not valid yet...") + logging.debug("Handle %s not valid yet..." % handle) return icon = self._icon_cache.get_icon(jid, new_avatar_token) @@ -535,6 +668,7 @@ class ServerPlugin(gobject.GObject): self.emit("avatar-updated", handle, icon) def _alias_changed_cb(self, aliases): + """Handle update of aliases for all users""" for handle, alias in aliases: prop = {'nick': alias} #print "Buddy %s alias changed to %s" % (handle, alias) @@ -542,14 +676,16 @@ class ServerPlugin(gobject.GObject): self._buddy_properties_changed_cb(handle, prop) def _buddy_properties_changed_cb(self, handle, properties): + """Handle update of given user (handle)'s properties""" if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): # ignore network events for Owner property changes since those # are handled locally return if self._online_contacts.has_key(handle) and self._online_contacts[handle]: - self.emit("buddy-properties-changed", handle, properties) + self.emit("buddy-properties-changed", handle, properties) def _buddy_activities_changed_cb(self, handle, activities): + """Handle update of given user (handle)'s activities""" if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): # ignore network events for Owner activity changes since those # are handled locally @@ -563,6 +699,8 @@ class ServerPlugin(gobject.GObject): self.emit("buddy-activities-changed", handle, activities_id) def _buddy_current_activity_changed_cb(self, handle, activity, channel): + """Handle update of given user (handle)'s current activity""" + if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): # ignore network events for Owner current activity changes since those # are handled locally @@ -577,6 +715,8 @@ class ServerPlugin(gobject.GObject): self._buddy_properties_changed_cb(handle, prop) def _new_channel_cb(self, object_path, channel_type, handle_type, handle, suppress_handler): + """Handle creation of a new channel + """ if handle_type == CONNECTION_HANDLE_TYPE_ROOM and channel_type == CHANNEL_TYPE_TEXT: channel = Channel(self._conn._dbus_object._named_service, object_path) @@ -595,6 +735,7 @@ class ServerPlugin(gobject.GObject): self.emit("private-invitation", object_path) def update_activity_properties(self, act_id): + """Request update from network on the activity properties of act_id""" handle = self._activities.get(act_id) if not handle: raise RuntimeError("Unknown activity %s: couldn't find handle.") @@ -604,6 +745,7 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._log_error_cb("getting activity properties", *args)) def set_activity_properties(self, act_id, props): + """Send update to network on the activity properties of act_id (props)""" handle = self._activities.get(act_id) if not handle: raise RuntimeError("Unknown activity %s: couldn't find handle.") @@ -613,6 +755,7 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._log_error_cb("setting activity properties", *args)) def _activity_properties_changed_cb(self, room, properties): + """Handle update of properties for a "room" (activity handle)""" for act_id, act_handle in self._activities.items(): if room == act_handle: self.emit("activity-properties-changed", act_id, properties) diff --git a/services/presence/sugar-presence-service b/services/presence/sugar-presence-service index bba32b50..7eec6965 100755 --- a/services/presence/sugar-presence-service +++ b/services/presence/sugar-presence-service @@ -24,7 +24,7 @@ import os from sugar import logger from sugar import env -sys.path.insert(0, env.get_service_path('presence')) +sys.path.append(env.get_service_path('presence')) test=0 if len(sys.argv) > 1: diff --git a/shell/Makefile.am b/shell/Makefile.am index bb874fba..99f55255 100644 --- a/shell/Makefile.am +++ b/shell/Makefile.am @@ -4,7 +4,6 @@ bin_SCRIPTS = sugar-shell sugardir = $(pkgdatadir)/shell sugar_PYTHON = \ - __init__.py \ shellservice.py confdir = $(pkgdatadir)/shell diff --git a/shell/extensions/Makefile.am b/shell/extensions/Makefile.am index 2bfeccd0..c531fbd5 100644 --- a/shell/extensions/Makefile.am +++ b/shell/extensions/Makefile.am @@ -1,27 +1,26 @@ -sugardir = $(pkgdatadir)/shell +sugardir = $(pkgdatadir)/shell/extensions sugar_PYTHON = \ __init__.py -pkgpyexecdir = $(pkgdatadir)/shell +pkgpyexecdir = $(pkgdatadir)/shell/extensions -pkgpyexec_LTLIBRARIES = extensions.la +pkgpyexec_LTLIBRARIES = _extensions.la -extensions_la_LDFLAGS = -module -avoid-version +_extensions_la_LDFLAGS = -module -avoid-version -extensions_la_CFLAGS = \ - $(WARN_CFLAGS) \ - $(PYTHON_INCLUDES) \ - $(PYGTK_CFLAGS) \ - $(SHELL_CFLAGS) \ - $(top_srcdir)/shell/extensions +_extensions_la_CFLAGS = \ + $(WARN_CFLAGS) \ + $(PYTHON_INCLUDES) \ + $(PYGTK_CFLAGS) \ + $(SHELL_CFLAGS) -extensions_la_LIBADD = \ +_extensions_la_LIBADD = \ $(SHELL_LIBS) \ $(PYCAIRO_LIBS) \ -lgstinterfaces-0.10 \ -lgstaudio-0.10 -extensions_la_SOURCES = \ +_extensions_la_SOURCES = \ $(BUILT_SOURCES) \ eggaccelerators.h \ eggaccelerators.c \ @@ -29,10 +28,10 @@ extensions_la_SOURCES = \ sugar-audio-manager.h \ sugar-key-grabber.h \ sugar-key-grabber.c \ - extensionsmodule.c + _extensionsmodule.c BUILT_SOURCES = \ - extensions.c \ + _extensions.c \ sugar-shell-marshal.c \ sugar-shell-marshal.h @@ -58,9 +57,9 @@ CLEANFILES = $(stamp_files) $(BUILT_SOURCES) DISTCLEANFILES = $(stamp_files) $(BUILT_SOURCES) MAINTAINERCLEANFILES = $(stamp_files) $(BUILT_SOURCES) -EXTRA_DIST = sugar-marshal.list extensions.override extensions.defs +EXTRA_DIST = sugar-marshal.list _extensions.override _extensions.defs -extensions.c: extensions.defs extensions.override +extensions.c: _extensions.defs _extensions.override .defs.c: (cd $(srcdir)\ diff --git a/shell/extensions/__init__.py b/shell/extensions/__init__.py index ee59f2ce..8290d419 100644 --- a/shell/extensions/__init__.py +++ b/shell/extensions/__init__.py @@ -14,4 +14,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -from extensions import extensions +try: + from extensions._extensions import * +except ImportError: + from sugar import ltihooks + from extensions._extensions import * diff --git a/shell/extensions/extensions.defs b/shell/extensions/_extensions.defs similarity index 100% rename from shell/extensions/extensions.defs rename to shell/extensions/_extensions.defs diff --git a/shell/extensions/extensions.override b/shell/extensions/_extensions.override similarity index 100% rename from shell/extensions/extensions.override rename to shell/extensions/_extensions.override diff --git a/shell/extensions/extensionsmodule.c b/shell/extensions/_extensionsmodule.c similarity index 59% rename from shell/extensions/extensionsmodule.c rename to shell/extensions/_extensionsmodule.c index 834d5b8d..76d7e53d 100644 --- a/shell/extensions/extensionsmodule.c +++ b/shell/extensions/_extensionsmodule.c @@ -5,21 +5,21 @@ /* include this first, before NO_IMPORT_PYGOBJECT is defined */ #include -void pyextensions_register_classes (PyObject *d); +void py_extensions_register_classes (PyObject *d); -extern PyMethodDef pyextensions_functions[]; +extern PyMethodDef py_extensions_functions[]; DL_EXPORT(void) -initextensions(void) +init_extensions(void) { PyObject *m, *d; init_pygobject (); - m = Py_InitModule ("extensions", pyextensions_functions); + m = Py_InitModule ("_extensions", py_extensions_functions); d = PyModule_GetDict (m); - pyextensions_register_classes (d); + py_extensions_register_classes (d); if (PyErr_Occurred ()) { Py_FatalError ("can't initialise module _sugar"); diff --git a/shell/extensions/extensions.c b/shell/extensions/extensions.c deleted file mode 100644 index 079e5bd1..00000000 --- a/shell/extensions/extensions.c +++ /dev/null @@ -1,260 +0,0 @@ -/* -- THIS FILE IS GENERATED - DO NOT EDIT *//* -*- Mode: C; c-basic-offset: 4 -*- */ - -#include - - - -#line 4 "extensions.override" -#include - -#include "pygobject.h" -#include "sugar-key-grabber.h" -#include "sugar-address-entry.h" -#include "sugar-audio-manager.h" - -#line 16 "extensions.c" - - -/* ---------- types from other modules ---------- */ -static PyTypeObject *_PyGObject_Type; -#define PyGObject_Type (*_PyGObject_Type) -static PyTypeObject *_PyGtkEntry_Type; -#define PyGtkEntry_Type (*_PyGtkEntry_Type) - - -/* ---------- forward type declarations ---------- */ -PyTypeObject G_GNUC_INTERNAL PySugarKeyGrabber_Type; -PyTypeObject G_GNUC_INTERNAL PySugarAudioManager_Type; - -#line 30 "extensions.c" - - - -/* ----------- SugarKeyGrabber ----------- */ - -static PyObject * -_wrap_sugar_key_grabber_grab(PyGObject *self, PyObject *args, PyObject *kwargs) -{ - static char *kwlist[] = { "key", NULL }; - char *key; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs,"s:SugarKeyGrabber.grab", kwlist, &key)) - return NULL; - - sugar_key_grabber_grab(SUGAR_KEY_GRABBER(self->obj), key); - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -_wrap_sugar_key_grabber_get_key(PyGObject *self, PyObject *args, PyObject *kwargs) -{ - static char *kwlist[] = { "keycode", "state", NULL }; - PyObject *py_keycode = NULL, *py_state = NULL; - gchar *ret; - guint keycode = 0, state = 0; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs,"OO:SugarKeyGrabber.get_key", kwlist, &py_keycode, &py_state)) - return NULL; - if (py_keycode) { - if (PyLong_Check(py_keycode)) - keycode = PyLong_AsUnsignedLong(py_keycode); - else if (PyInt_Check(py_keycode)) - keycode = PyInt_AsLong(py_keycode); - else - PyErr_SetString(PyExc_TypeError, "Parameter 'keycode' must be an int or a long"); - if (PyErr_Occurred()) - return NULL; - } - if (py_state) { - if (PyLong_Check(py_state)) - state = PyLong_AsUnsignedLong(py_state); - else if (PyInt_Check(py_state)) - state = PyInt_AsLong(py_state); - else - PyErr_SetString(PyExc_TypeError, "Parameter 'state' must be an int or a long"); - if (PyErr_Occurred()) - return NULL; - } - - ret = sugar_key_grabber_get_key(SUGAR_KEY_GRABBER(self->obj), keycode, state); - - if (ret) { - PyObject *py_ret = PyString_FromString(ret); - g_free(ret); - return py_ret; - } - Py_INCREF(Py_None); - return Py_None; -} - -static const PyMethodDef _PySugarKeyGrabber_methods[] = { - { "grab", (PyCFunction)_wrap_sugar_key_grabber_grab, METH_VARARGS|METH_KEYWORDS, - NULL }, - { "get_key", (PyCFunction)_wrap_sugar_key_grabber_get_key, METH_VARARGS|METH_KEYWORDS, - NULL }, - { NULL, NULL, 0, NULL } -}; - -PyTypeObject G_GNUC_INTERNAL PySugarKeyGrabber_Type = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ - "extensions.KeyGrabber", /* tp_name */ - sizeof(PyGObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - /* methods */ - (destructor)0, /* tp_dealloc */ - (printfunc)0, /* tp_print */ - (getattrfunc)0, /* tp_getattr */ - (setattrfunc)0, /* tp_setattr */ - (cmpfunc)0, /* tp_compare */ - (reprfunc)0, /* tp_repr */ - (PyNumberMethods*)0, /* tp_as_number */ - (PySequenceMethods*)0, /* tp_as_sequence */ - (PyMappingMethods*)0, /* tp_as_mapping */ - (hashfunc)0, /* tp_hash */ - (ternaryfunc)0, /* tp_call */ - (reprfunc)0, /* tp_str */ - (getattrofunc)0, /* tp_getattro */ - (setattrofunc)0, /* tp_setattro */ - (PyBufferProcs*)0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - NULL, /* Documentation string */ - (traverseproc)0, /* tp_traverse */ - (inquiry)0, /* tp_clear */ - (richcmpfunc)0, /* tp_richcompare */ - offsetof(PyGObject, weakreflist), /* tp_weaklistoffset */ - (getiterfunc)0, /* tp_iter */ - (iternextfunc)0, /* tp_iternext */ - (struct PyMethodDef*)_PySugarKeyGrabber_methods, /* tp_methods */ - (struct PyMemberDef*)0, /* tp_members */ - (struct PyGetSetDef*)0, /* tp_getset */ - NULL, /* tp_base */ - NULL, /* tp_dict */ - (descrgetfunc)0, /* tp_descr_get */ - (descrsetfunc)0, /* tp_descr_set */ - offsetof(PyGObject, inst_dict), /* tp_dictoffset */ - (initproc)0, /* tp_init */ - (allocfunc)0, /* tp_alloc */ - (newfunc)0, /* tp_new */ - (freefunc)0, /* tp_free */ - (inquiry)0 /* tp_is_gc */ -}; - - - -/* ----------- SugarAudioManager ----------- */ - -static PyObject * -_wrap_sugar_audio_manager_set_volume(PyGObject *self, PyObject *args, PyObject *kwargs) -{ - static char *kwlist[] = { "level", NULL }; - int level; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs,"i:SugarAudioManager.set_volume", kwlist, &level)) - return NULL; - - sugar_audio_manager_set_volume(SUGAR_AUDIO_MANAGER(self->obj), level); - - Py_INCREF(Py_None); - return Py_None; -} - -static const PyMethodDef _PySugarAudioManager_methods[] = { - { "set_volume", (PyCFunction)_wrap_sugar_audio_manager_set_volume, METH_VARARGS|METH_KEYWORDS, - NULL }, - { NULL, NULL, 0, NULL } -}; - -PyTypeObject G_GNUC_INTERNAL PySugarAudioManager_Type = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ - "extensions.AudioManager", /* tp_name */ - sizeof(PyGObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - /* methods */ - (destructor)0, /* tp_dealloc */ - (printfunc)0, /* tp_print */ - (getattrfunc)0, /* tp_getattr */ - (setattrfunc)0, /* tp_setattr */ - (cmpfunc)0, /* tp_compare */ - (reprfunc)0, /* tp_repr */ - (PyNumberMethods*)0, /* tp_as_number */ - (PySequenceMethods*)0, /* tp_as_sequence */ - (PyMappingMethods*)0, /* tp_as_mapping */ - (hashfunc)0, /* tp_hash */ - (ternaryfunc)0, /* tp_call */ - (reprfunc)0, /* tp_str */ - (getattrofunc)0, /* tp_getattro */ - (setattrofunc)0, /* tp_setattro */ - (PyBufferProcs*)0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - NULL, /* Documentation string */ - (traverseproc)0, /* tp_traverse */ - (inquiry)0, /* tp_clear */ - (richcmpfunc)0, /* tp_richcompare */ - offsetof(PyGObject, weakreflist), /* tp_weaklistoffset */ - (getiterfunc)0, /* tp_iter */ - (iternextfunc)0, /* tp_iternext */ - (struct PyMethodDef*)_PySugarAudioManager_methods, /* tp_methods */ - (struct PyMemberDef*)0, /* tp_members */ - (struct PyGetSetDef*)0, /* tp_getset */ - NULL, /* tp_base */ - NULL, /* tp_dict */ - (descrgetfunc)0, /* tp_descr_get */ - (descrsetfunc)0, /* tp_descr_set */ - offsetof(PyGObject, inst_dict), /* tp_dictoffset */ - (initproc)0, /* tp_init */ - (allocfunc)0, /* tp_alloc */ - (newfunc)0, /* tp_new */ - (freefunc)0, /* tp_free */ - (inquiry)0 /* tp_is_gc */ -}; - - - -/* ----------- functions ----------- */ - -const PyMethodDef pyextensions_functions[] = { - { NULL, NULL, 0, NULL } -}; - -/* initialise stuff extension classes */ -void -pyextensions_register_classes(PyObject *d) -{ - PyObject *module; - - if ((module = PyImport_ImportModule("gobject")) != NULL) { - _PyGObject_Type = (PyTypeObject *)PyObject_GetAttrString(module, "GObject"); - if (_PyGObject_Type == NULL) { - PyErr_SetString(PyExc_ImportError, - "cannot import name GObject from gobject"); - return ; - } - } else { - PyErr_SetString(PyExc_ImportError, - "could not import gobject"); - return ; - } - if ((module = PyImport_ImportModule("gtk")) != NULL) { - _PyGtkEntry_Type = (PyTypeObject *)PyObject_GetAttrString(module, "Entry"); - if (_PyGtkEntry_Type == NULL) { - PyErr_SetString(PyExc_ImportError, - "cannot import name Entry from gtk"); - return ; - } - } else { - PyErr_SetString(PyExc_ImportError, - "could not import gtk"); - return ; - } - - -#line 256 "extensions.c" - pygobject_register_class(d, "SugarKeyGrabber", SUGAR_TYPE_KEY_GRABBER, &PySugarKeyGrabber_Type, Py_BuildValue("(O)", &PyGObject_Type)); - pyg_set_object_has_new_constructor(SUGAR_TYPE_KEY_GRABBER); - pygobject_register_class(d, "SugarAudioManager", SUGAR_TYPE_AUDIO_MANAGER, &PySugarAudioManager_Type, Py_BuildValue("(O)", &PyGObject_Type)); - pyg_set_object_has_new_constructor(SUGAR_TYPE_AUDIO_MANAGER); -} diff --git a/shell/model/BuddyModel.py b/shell/model/BuddyModel.py index a06a728a..a551e1e0 100644 --- a/shell/model/BuddyModel.py +++ b/shell/model/BuddyModel.py @@ -84,6 +84,11 @@ class BuddyModel(gobject.GObject): def get_buddy(self): return self._buddy + def is_owner(self): + if not self._buddy: + return False + return self._buddy.props.owner + def is_present(self): if self._buddy: return True @@ -125,6 +130,8 @@ class BuddyModel(gobject.GObject): if 'color' in keys: self._set_color_from_string(self._buddy.props.color) self.emit('color-changed', self.get_color()) + if 'current-activity' in keys: + self.emit('current-activity-changed', buddy.props.current_activity) def _buddy_disappeared_cb(self, buddy): if buddy != self._buddy: @@ -139,8 +146,3 @@ class BuddyModel(gobject.GObject): def _buddy_icon_changed_cb(self, buddy): self.emit('icon-changed') - - def _buddy_current_activity_changed_cb(self, buddy, activity=None): - if not self._buddy: - return - self.emit('current-activity-changed', activity) diff --git a/shell/model/MeshModel.py b/shell/model/MeshModel.py index dd25d9f4..b037bb71 100644 --- a/shell/model/MeshModel.py +++ b/shell/model/MeshModel.py @@ -160,19 +160,14 @@ class MeshModel(gobject.GObject): def get_buddies(self): return self._buddies.values() - def _buddy_activity_changed_cb(self, buddy, cur_activity): - if not self._buddies.has_key(buddy.props.key): + def _buddy_activity_changed_cb(self, model, cur_activity): + if not self._buddies.has_key(model.get_key()): return - buddy_model = self._buddies[buddy.props.key] - if cur_activity == None: - self.emit('buddy-moved', buddy_model, None) - else: - self._notify_buddy_change(buddy_model, cur_activity) - - def _notify_buddy_change(self, buddy_model, cur_activity): - if self._activities.has_key(cur_activity.get_id()): + if cur_activity and self._activities.has_key(cur_activity.get_id()): activity_model = self._activities[cur_activity.get_id()] - self.emit('buddy-moved', buddy_model, activity_model) + self.emit('buddy-moved', model, activity_model) + else: + self.emit('buddy-moved', model, None) def _buddy_appeared_cb(self, pservice, buddy): if self._buddies.has_key(buddy.props.key): @@ -186,7 +181,7 @@ class MeshModel(gobject.GObject): cur_activity = buddy.props.current_activity if cur_activity: - self._notify_buddy_change(model, cur_activity) + self._buddy_activity_changed_cb(model, cur_activity) def _buddy_disappeared_cb(self, pservice, buddy): if not self._buddies.has_key(buddy.props.key): diff --git a/shell/model/homemodel.py b/shell/model/homemodel.py index 0b1eeb5a..99b0512d 100644 --- a/shell/model/homemodel.py +++ b/shell/model/homemodel.py @@ -166,7 +166,7 @@ class HomeModel(gobject.GObject): else: # activity got lost, took longer to launch than we allow, # or it was launched by something other than the shell - act_type = act_service.get_service_name() + act_type = service.get_service_name() bundle = self._bundle_registry.get_bundle(act_type) if not bundle: raise RuntimeError("No bundle for activity type '%s'." % act_type) diff --git a/shell/view/BuddyMenu.py b/shell/view/BuddyMenu.py index 1a76168a..16e43cff 100644 --- a/shell/view/BuddyMenu.py +++ b/shell/view/BuddyMenu.py @@ -88,7 +88,7 @@ class BuddyMenu(Menu): activity = shell_model.get_home().get_current_activity() if activity != None: - activity_ps = pservice.get_activity(activity.get_id()) + activity_ps = pservice.get_activity(activity.get_activity_id()) # FIXME check that the buddy is not in the activity already diff --git a/shell/view/clipboardicon.py b/shell/view/clipboardicon.py index ef0de29e..143d016e 100644 --- a/shell/view/clipboardicon.py +++ b/shell/view/clipboardicon.py @@ -17,6 +17,7 @@ import logging import os +import urlparse import gobject @@ -74,8 +75,13 @@ class ClipboardIcon(CanvasIcon): self.props.background_color = color.TOOLBAR_BACKGROUND.get_int() def get_popup(self): + cb_service = clipboardservice.get_instance() + obj = cb_service.get_object(self._object_id) + formats = obj['FORMATS'] + self._menu = ClipboardMenu(self._name, self._percent, self._preview, - self._activity) + self._activity, + formats[0] == 'application/vnd.olpc-x-sugar') self._menu.connect('action', self._popup_action_cb) return self._menu @@ -83,15 +89,19 @@ class ClipboardIcon(CanvasIcon): return self._popup_context def set_state(self, name, percent, icon_name, preview, activity): + cb_service = clipboardservice.get_instance() + obj = cb_service.get_object(self._object_id) + installable = (obj['FORMATS'][0] == 'application/vnd.olpc-x-sugar') + self._name = name self._percent = percent self._preview = preview self._activity = activity self.set_property("icon_name", icon_name) if self._menu: - self._menu.set_state(name, percent, preview, activity) + self._menu.set_state(name, percent, preview, activity, installable) - if activity and percent < 100: + if (activity or installable) and percent < 100: self.props.xo_color = XoColor("#000000,#424242") else: self.props.xo_color = XoColor("#000000,#FFFFFF") @@ -107,27 +117,39 @@ class ClipboardIcon(CanvasIcon): self._open_file() def _open_file(self): - if self._percent < 100 or not self._activity: + if self._percent < 100: return # Get the file path cb_service = clipboardservice.get_instance() obj = cb_service.get_object(self._object_id) formats = obj['FORMATS'] - if len(formats) > 0: - path = cb_service.get_object_data(self._object_id, formats[0]) + if len(formats) == 0: + return - # FIXME: would be better to check for format.onDisk - try: - path_exists = os.path.exists(path) - except TypeError: - path_exists = False + if not self._activity and \ + not formats[0] == 'application/vnd.olpc-x-sugar': + return - if path_exists: - uri = 'file://' + path + uri = cb_service.get_object_data(self._object_id, formats[0]) + if not uri.startswith('file://'): + return + + path = urlparse.urlparse(uri).path + + # FIXME: would be better to check for format.onDisk + try: + path_exists = os.path.exists(path) + except TypeError: + path_exists = False + + if path_exists: + if self._activity: activityfactory.create_with_uri(self._activity, uri) else: - logging.debug("Clipboard item file path %s didn't exist" % path) + self._install_xo(path) + else: + logging.debug("Clipboard item file path %s didn't exist" % path) def _popup_action_cb(self, popup, menu_item): action = menu_item.props.action_id @@ -153,3 +175,9 @@ class ClipboardIcon(CanvasIcon): self.props.background_color = color.DESKTOP_BACKGROUND.get_int() else: self.props.background_color = color.TOOLBAR_BACKGROUND.get_int() + + def _install_xo(self, path): + logging.debug('mec') + if os.spawnlp(os.P_WAIT, 'sugar-install-bundle', 'sugar-install-bundle', + path): + raise RuntimeError, 'An error occurred while extracting the .xo contents.' diff --git a/shell/view/clipboardmenu.py b/shell/view/clipboardmenu.py index 3e8239d6..9db69223 100644 --- a/shell/view/clipboardmenu.py +++ b/shell/view/clipboardmenu.py @@ -33,7 +33,7 @@ class ClipboardMenu(Menu): ACTION_OPEN = 1 ACTION_STOP_DOWNLOAD = 2 - def __init__(self, name, percent, preview, activity): + def __init__(self, name, percent, preview, activity, installable): Menu.__init__(self, name) self.props.border = 0 @@ -54,10 +54,10 @@ class ClipboardMenu(Menu): self._preview_text.props.font_desc = font.DEFAULT.get_pango_desc() self.append(self._preview_text) - self._update_icons(percent, activity) + self._update_icons(percent, activity, installable) - def _update_icons(self, percent, activity): - if percent == 100 and activity: + def _update_icons(self, percent, activity, installable): + if percent == 100 and (activity or installable): if not self._remove_item: self._remove_item = MenuItem(ClipboardMenu.ACTION_DELETE, _('Remove'), @@ -73,7 +73,7 @@ class ClipboardMenu(Menu): if self._stop_item: self.remove_item(self._stop_item) self._stop_item = None - elif percent == 100 and not activity: + elif percent == 100 and (not activity and not installable): if not self._remove_item: self._remove_item = MenuItem(ClipboardMenu.ACTION_DELETE, _('Remove'), @@ -102,8 +102,8 @@ class ClipboardMenu(Menu): self.remove_item(self._open_item) self._open_item = None - def set_state(self, name, percent, preview, activity): + def set_state(self, name, percent, preview, activity, installable): self.set_title(name) if self._progress_bar: self._progress_bar.set_property('percent', percent) - self._update_icons(percent, activity) + self._update_icons(percent, activity, installable) diff --git a/shell/view/frame/clipboardbox.py b/shell/view/frame/clipboardbox.py index 2dcad10e..4a01610d 100644 --- a/shell/view/frame/clipboardbox.py +++ b/shell/view/frame/clipboardbox.py @@ -1,4 +1,8 @@ +import shutil +import os import logging +import urlparse + import hippo import gtk @@ -72,13 +76,29 @@ class ClipboardBox(hippo.CanvasBox): if not selection.data: return - logging.debug('ClipboardBox: adding type ' + selection.type + '.') - + logging.debug('ClipboardBox: adding type ' + selection.type + ' ' + selection.data) + cb_service = clipboardservice.get_instance() - cb_service.add_object_format(object_id, - selection.type, - selection.data, - on_disk = False) + if selection.type == 'text/uri-list': + uris = selection.data.split('\n') + if len(uris) > 1: + raise NotImplementedError('Multiple uris in text/uri-list still not supported.') + uri = urlparse.urlparse(uris[0]) + path, file_name = os.path.split(uri.path) + + # Copy the file, as it will be deleted when the dnd operation finishes. + new_file_path = os.path.join(path, 'cb' + file_name) + shutil.copyfile(uri.path, new_file_path) + + cb_service.add_object_format(object_id, + selection.type, + uri.scheme + "://" + new_file_path, + on_disk=True) + else: + cb_service.add_object_format(object_id, + selection.type, + selection.data, + on_disk=False) def _object_added_cb(self, cb_service, object_id, name): icon = ClipboardIcon(self._popup_context, object_id, name) @@ -165,15 +185,16 @@ class ClipboardBox(hippo.CanvasBox): def drag_data_received_cb(self, widget, context, x, y, selection, targetType, time): logging.debug('ClipboardBox: got data for target ' + selection.target) - if selection: - object_id = self._context_map.get_object_id(context) - self._add_selection(object_id, selection) - else: - logging.warn('ClipboardBox: empty selection for target ' + selection.target) - - # If it's the last target to be processed, finish the dnd transaction - if not self._context_map.has_context(context): - context.finish(True, False, time) + try: + if selection: + object_id = self._context_map.get_object_id(context) + self._add_selection(object_id, selection) + else: + logging.warn('ClipboardBox: empty selection for target ' + selection.target) + finally: + # If it's the last target to be processed, finish the dnd transaction + if not self._context_map.has_context(context): + context.drop_finish(True, gtk.get_current_event_time()) def drag_data_get_cb(self, widget, context, selection, targetType, eventTime): logging.debug("drag_data_get_cb: requested target " + selection.target) diff --git a/shell/view/frame/frame.py b/shell/view/frame/frame.py index bd598aa2..472cb5fb 100644 --- a/shell/view/frame/frame.py +++ b/shell/view/frame/frame.py @@ -169,7 +169,7 @@ class Frame(object): if self._animator: self._animator.stop() - self._animator = animator.Animator(0.5, 30, animator.EASE_OUT_EXPO) + self._animator = animator.Animator(0.5) self._animator.add(_Animation(self, 0.0)) self._animator.start() @@ -188,7 +188,7 @@ class Frame(object): if self._animator: self._animator.stop() - self._animator = animator.Animator(0.5, 30, animator.EASE_OUT_EXPO) + self._animator = animator.Animator(0.5) self._animator.add(_Animation(self, 1.0)) self._animator.start() diff --git a/shell/view/home/FriendsBox.py b/shell/view/home/FriendsBox.py index 6f1cdb7d..71b90263 100644 --- a/shell/view/home/FriendsBox.py +++ b/shell/view/home/FriendsBox.py @@ -24,7 +24,7 @@ from sugar.graphics import units from view.home.MyIcon import MyIcon from view.home.FriendView import FriendView -class FriendsBox(SpreadBox, hippo.CanvasItem): +class FriendsBox(SpreadBox): __gtype_name__ = 'SugarFriendsBox' def __init__(self, shell, menu_shell): SpreadBox.__init__(self, background_color=0xe2e2e2ff) @@ -34,7 +34,7 @@ class FriendsBox(SpreadBox, hippo.CanvasItem): self._friends = {} self._my_icon = MyIcon(units.LARGE_ICON_SCALE) - self.append(self._my_icon, hippo.PACK_FIXED) + self.set_center_item(self._my_icon) friends = self._shell.get_model().get_friends() @@ -56,10 +56,3 @@ class FriendsBox(SpreadBox, hippo.CanvasItem): def _friend_removed_cb(self, data_model, key): self.remove_item(self._friends[key]) del self._friends[key] - - def do_allocate(self, width, height, origin_changed): - SpreadBox.do_allocate(self, width, height, origin_changed) - - [icon_width, icon_height] = self._my_icon.get_allocation() - self.set_position(self._my_icon, (width - icon_width) / 2, - (height - icon_height) / 2) diff --git a/shell/view/home/HomeWindow.py b/shell/view/home/HomeWindow.py index a37aeaea..3f339190 100644 --- a/shell/view/home/HomeWindow.py +++ b/shell/view/home/HomeWindow.py @@ -19,20 +19,26 @@ import hippo import cairo from sugar.graphics.menushell import MenuShell +from sugar.graphics.window import Window +from sugar.graphics import units import sugar + from view.home.MeshBox import MeshBox from view.home.HomeBox import HomeBox from view.home.FriendsBox import FriendsBox +from view.home.transitionbox import TransitionBox -_HOME_PAGE = 0 -_FRIENDS_PAGE = 1 -_MESH_PAGE = 2 +_HOME_PAGE = 0 +_FRIENDS_PAGE = 1 +_MESH_PAGE = 2 +_TRANSITION_PAGE = 3 -class HomeWindow(gtk.Window): +class HomeWindow(Window): def __init__(self, shell): - gtk.Window.__init__(self) + Window.__init__(self) self._shell = shell self._active = False + self._level = sugar.ZOOM_HOME self.set_default_size(gtk.gdk.screen_width(), gtk.gdk.screen_height()) @@ -43,30 +49,15 @@ class HomeWindow(gtk.Window): self.connect('focus-in-event', self._focus_in_cb) self.connect('focus-out-event', self._focus_out_cb) - self._nb = gtk.Notebook() - self._nb.set_show_border(False) - self._nb.set_show_tabs(False) - - self.add(self._nb) - self._nb.show() - - canvas = hippo.Canvas() self._home_box = HomeBox(shell) - canvas.set_root(self._home_box) - self._nb.append_page(canvas) - canvas.show() + self._friends_box = FriendsBox(shell, MenuShell(self)) + self._mesh_box = MeshBox(shell, MenuShell(self)) + self._transition_box = TransitionBox() - canvas = hippo.Canvas() - box = FriendsBox(shell, MenuShell(canvas)) - canvas.set_root(box) - self._nb.append_page(canvas) - canvas.show() - - canvas = hippo.Canvas() - self._mesh_box = MeshBox(shell, MenuShell(canvas)) - canvas.set_root(self._mesh_box) - self._nb.append_page(canvas) - canvas.show() + self.set_root(self._home_box) + + self._transition_box.connect('completed', + self._transition_completed_cb) def _key_release_cb(self, widget, event): keyname = gtk.gdk.keyval_name(event.keyval) @@ -74,7 +65,7 @@ class HomeWindow(gtk.Window): self._home_box.release() def _update_mesh_state(self): - if self._active and self._nb.get_current_page() == _MESH_PAGE: + if self._active and self._level == sugar.ZOOM_MESH: self._mesh_box.resume() else: self._mesh_box.suspend() @@ -88,12 +79,26 @@ class HomeWindow(gtk.Window): self._update_mesh_state() def set_zoom_level(self, level): + self._level = level + + self.set_root(self._transition_box) + if level == sugar.ZOOM_HOME: - self._nb.set_current_page(_HOME_PAGE) + scale = units.XLARGE_ICON_SCALE elif level == sugar.ZOOM_FRIENDS: - self._nb.set_current_page(_FRIENDS_PAGE) + scale = units.LARGE_ICON_SCALE elif level == sugar.ZOOM_MESH: - self._nb.set_current_page(_MESH_PAGE) + scale = units.STANDARD_ICON_SCALE + + self._transition_box.set_scale(scale) + + def _transition_completed_cb(self, transition_box): + if self._level == sugar.ZOOM_HOME: + self.set_root(self._home_box) + elif self._level == sugar.ZOOM_FRIENDS: + self.set_root(self._friends_box) + elif self._level == sugar.ZOOM_MESH: + self.set_root(self._mesh_box) self._update_mesh_state() diff --git a/shell/view/home/Makefile.am b/shell/view/home/Makefile.am index 11e221c3..466187be 100644 --- a/shell/view/home/Makefile.am +++ b/shell/view/home/Makefile.am @@ -7,4 +7,5 @@ sugar_PYTHON = \ HomeBox.py \ HomeWindow.py \ MeshBox.py \ - MyIcon.py + MyIcon.py \ + transitionbox.py diff --git a/shell/view/home/MeshBox.py b/shell/view/home/MeshBox.py index 218a44d8..fa85b033 100644 --- a/shell/view/home/MeshBox.py +++ b/shell/view/home/MeshBox.py @@ -278,7 +278,10 @@ class MeshBox(SpreadBox): def _add_alone_buddy(self, buddy_model): icon = BuddyIcon(self._shell, self._menu_shell, buddy_model) - self.add_item(icon) + if buddy_model.is_owner(): + self.set_center_item(icon) + else: + self.add_item(icon) self._buddies[buddy_model.get_key()] = icon diff --git a/shell/view/home/transitionbox.py b/shell/view/home/transitionbox.py new file mode 100644 index 00000000..3d83347e --- /dev/null +++ b/shell/view/home/transitionbox.py @@ -0,0 +1,66 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import hippo +import gobject + +from sugar.graphics import units +from sugar.graphics import animator +from sugar.graphics.spreadbox import SpreadBox + +from view.home.MyIcon import MyIcon + +class _Animation(animator.Animation): + def __init__(self, icon, start_scale, end_scale): + animator.Animation.__init__(self, 0.0, 1.0) + + self._icon = icon + self.start_scale = start_scale + self.end_scale = end_scale + + def next_frame(self, current): + d = (self.end_scale - self.start_scale) * current + self._icon.props.scale = self.start_scale + d + +class TransitionBox(SpreadBox): + __gtype_name__ = 'SugarTransitionBox' + + __gsignals__ = { + 'completed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self): + SpreadBox.__init__(self, background_color=0xe2e2e2ff) + + self._scale = units.XLARGE_ICON_SCALE + + self._my_icon = MyIcon(self._scale) + self.set_center_item(self._my_icon) + + self._animator = animator.Animator(0.3) + self._animator.connect('completed', self._animation_completed_cb) + + def _animation_completed_cb(self, anim): + self.emit('completed') + + def set_scale(self, scale): + self._animator.remove_all() + self._animator.add(_Animation(self._my_icon, self._scale, scale)) + self._animator.start() + + self._scale = scale + diff --git a/sugar-emulator b/sugar-emulator index d4005d52..29b02603 100755 --- a/sugar-emulator +++ b/sugar-emulator @@ -25,8 +25,6 @@ pygtk.require('2.0') import gtk import gobject -from sugar import env - def _get_display_number(): """Find a free display number trying to connect to 6000+ ports""" retries = 20 @@ -50,22 +48,23 @@ def _get_display_number(): logging.error('Cannot find a free display.') sys.exit(0) -def _start_xephyr(width, height, dpi): +def _start_xephyr(): display = _get_display_number() cmd = [ 'Xephyr' ] cmd.append(':%d' % display) cmd.append('-ac') - if width > 0 and height > 0: - cmd.append('-screen') - cmd.append('%dx%d' % (width, height)) - else: + if gtk.gdk.screen_width() < 1200 or gtk.gdk.screen_height() < 900: cmd.append('-fullscreen') + else: + cmd.append('-screen') + cmd.append('%dx%d' % (1200, 900)) + dpi = gtk.settings_get_default().get_property('gtk-xft-dpi') if dpi > 0: cmd.append('-dpi') - cmd.append(str(dpi)) + cmd.append('%d' % int(dpi/1024)) result = gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH) pid = result[0] @@ -73,23 +72,40 @@ def _start_xephyr(width, height, dpi): os.environ['DISPLAY'] = ":%d" % (display) os.environ['SUGAR_EMULATOR_PID'] = str(pid) -os.environ['SUGAR_EMULATOR'] = 'yes' +def _start_matchbox(): + cmd = ['matchbox-window-manager'] + + cmd.extend(['-use_titlebar', 'no']) + cmd.extend(['-theme', 'olpc']) + + gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH) + +def _setup_env(): + os.environ['SUGAR_EMULATOR'] = 'yes' + + source_dir = os.path.dirname(os.path.abspath(__file__)) + if os.path.isfile(os.path.join(source_dir, 'autogen.sh')): + os.environ['SUGAR_PATH'] = source_dir + if os.environ.has_key('PYTHONPATH'): + path = os.environ['PYTHONPATH'] + os.environ['PYTHONPATH'] = source_dir + ':' + path + +_setup_env() +_start_xephyr() + +from sugar import env + +if env.is_emulator(): + gtkrc_filename = 'sugar.gtkrc' +else: + gtkrc_filename = 'sugar-xo.gtkrc' + +os.environ['GTK2_RC_FILES'] = env.get_data_path(gtkrc_filename) if len(sys.argv) == 1: program = 'sugar-shell' else: + _start_matchbox() program = sys.argv[1] -if gtk.gdk.screen_width() < 1200 or gtk.gdk.screen_height() < 900: - width = -1 - height = -1 -else: - width = 1200 - height = 900 - -_gtk_xft_dpi = float(gtk.settings_get_default().get_property('gtk-xft-dpi')) -_start_xephyr(width, height, _gtk_xft_dpi / 1024) - -os.environ['GTK2_RC_FILES'] = env.get_data_path('gtkrc') - os.execlp('dbus-launch', 'dbus-launch', '--exit-with-session', program) diff --git a/sugar/Makefile.am b/sugar/Makefile.am index 7ea659a9..9e8db383 100644 --- a/sugar/Makefile.am +++ b/sugar/Makefile.am @@ -1,10 +1,11 @@ SUBDIRS = activity browser clipboard graphics p2p presence datastore sugardir = $(pythondir)/sugar -sugar_PYTHON = \ - __init__.py \ - date.py \ - env.py \ - logger.py \ - profile.py \ +sugar_PYTHON = \ + __init__.py \ + date.py \ + env.py \ + logger.py \ + ltihooks.py \ + profile.py \ util.py diff --git a/sugar/activity/activityfactoryservice.py b/sugar/activity/activityfactoryservice.py index 5dcedb37..ee98f511 100644 --- a/sugar/activity/activityfactoryservice.py +++ b/sugar/activity/activityfactoryservice.py @@ -140,7 +140,7 @@ def run_with_args(args): run(options.bundle_path) def run(bundle_path): - sys.path.insert(0, bundle_path) + sys.path.append(bundle_path) bundle = Bundle(bundle_path) diff --git a/sugar/browser/__init__.py b/sugar/browser/__init__.py index a79048b1..7de5339d 100644 --- a/sugar/browser/__init__.py +++ b/sugar/browser/__init__.py @@ -3,4 +3,9 @@ XUL Runner and gtkmozembed and is produced by the PyGTK .defs system. """ -from sugar.browser._sugarbrowser import * + +try: + from sugar.browser._sugarbrowser import * +except ImportError: + from sugar import ltihooks + from sugar.browser._sugarbrowser import * diff --git a/sugar/env.py b/sugar/env.py index f25f4606..3555e938 100644 --- a/sugar/env.py +++ b/sugar/env.py @@ -29,6 +29,17 @@ def _get_prefix_path(base, path=None): else: return os.path.join(prefix, base) +def _get_sugar_path(base, path=None): + if os.environ.has_key('SUGAR_PATH'): + sugar_path = os.environ['SUGAR_PATH'] + else: + sugar_path = _get_prefix_path('share/sugar') + + if path: + return os.path.join(sugar_path, base, path) + else: + return os.path.join(sugar_path, base) + def is_emulator(): if os.environ.has_key('SUGAR_EMULATOR'): if os.environ['SUGAR_EMULATOR'] == 'yes': @@ -56,17 +67,17 @@ def get_profile_path(path=None): def get_user_activities_path(): return os.path.expanduser('~/Activities') -def get_bin_path(path=None): - return _get_prefix_path('bin', path) - def get_locale_path(path=None): return _get_prefix_path('share/locale', path) +def get_bin_path(path=None): + return _get_sugar_path('bin', path) + def get_service_path(name): - return _get_prefix_path('share/sugar/services', name) + return _get_sugar_path('services', name) def get_shell_path(path=None): - return _get_prefix_path('share/sugar/shell', path) + return _get_sugar_path('shell', path) def get_data_path(path=None): - return _get_prefix_path('share/sugar', path) + return _get_sugar_path('data', path) diff --git a/sugar/graphics/animator.py b/sugar/graphics/animator.py index 60ff9f39..459851b7 100644 --- a/sugar/graphics/animator.py +++ b/sugar/graphics/animator.py @@ -28,7 +28,7 @@ class Animator(gobject.GObject): gobject.TYPE_NONE, ([])), } - def __init__(self, time, fps, easing=EASE_OUT_EXPO): + def __init__(self, time, fps=20, easing=EASE_OUT_EXPO): gobject.GObject.__init__(self) self._animations = [] self._time = time @@ -39,6 +39,10 @@ class Animator(gobject.GObject): def add(self, animation): self._animations.append(animation) + def remove_all(self): + self.stop() + self._animations = [] + def start(self): if self._timeout_sid: self.stop() @@ -51,7 +55,7 @@ class Animator(gobject.GObject): if self._timeout_sid: gobject.source_remove(self._timeout_sid) self._timeout_sid = 0 - self.emit('completed') + self.emit('completed') def _next_frame_cb(self): current_time = min(self._time, time.time() - self._start_time) diff --git a/sugar/graphics/canvasicon.py b/sugar/graphics/canvasicon.py index d50e9f90..14f83519 100644 --- a/sugar/graphics/canvasicon.py +++ b/sugar/graphics/canvasicon.py @@ -369,6 +369,11 @@ class CanvasIcon(hippo.CanvasBox, hippo.CanvasItem): if not self._popup: return + if not self.get_context(): + # If we have been detached from our parent, don't show up the popup + # in this case. + return + popup_context = self.get_popup_context() [x, y] = [None, None] diff --git a/sugar/graphics/snowflakebox.py b/sugar/graphics/snowflakebox.py index dfef45b0..14f4559b 100644 --- a/sugar/graphics/snowflakebox.py +++ b/sugar/graphics/snowflakebox.py @@ -20,9 +20,11 @@ import math import cairo import hippo -_BASE_RADIUS = 65 +from sugar.graphics import units + +_BASE_RADIUS = units.points_to_pixels(20) _CHILDREN_FACTOR = 1 -_FLAKE_DISTANCE = 6 +_FLAKE_DISTANCE = units.points_to_pixels(4) class SnowflakeBox(hippo.CanvasBox, hippo.CanvasItem): __gtype_name__ = 'SugarSnowflakeBox' @@ -67,6 +69,13 @@ class SnowflakeBox(hippo.CanvasBox, hippo.CanvasItem): self.set_position(child, int(x), int(y)) + def do_get_height_request(self, for_width): + hippo.CanvasBox.do_get_height_request(self, for_width) + + height = for_width + + return (height, height) + def do_get_width_request(self): hippo.CanvasBox.do_get_width_request(self) diff --git a/sugar/graphics/spreadbox.py b/sugar/graphics/spreadbox.py index 24e12f39..ff50f71e 100644 --- a/sugar/graphics/spreadbox.py +++ b/sugar/graphics/spreadbox.py @@ -22,20 +22,31 @@ import gtk from sugar.graphics import units +_WIDTH = gtk.gdk.screen_width() +_HEIGHT = gtk.gdk.screen_height() _CELL_WIDTH = units.grid_to_pixels(1) _CELL_HEIGHT = units.grid_to_pixels(1) -_GRID_WIDTH = gtk.gdk.screen_width() / _CELL_WIDTH -_GRID_HEIGHT = gtk.gdk.screen_height() / _CELL_HEIGHT +_GRID_WIDTH = _WIDTH / _CELL_WIDTH +_GRID_HEIGHT = _HEIGHT / _CELL_HEIGHT -class SpreadBox(hippo.CanvasBox): +class SpreadBox(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = 'SugarSpreadBox' def __init__(self, **kwargs): hippo.CanvasBox.__init__(self, **kwargs) self._grid = [] + self._center = None for i in range(0, _GRID_WIDTH * _GRID_HEIGHT): self._grid.append(None) + def set_center_item(self, item): + if self._center: + self.remove(self._center) + + self._center = item + self.append(item, hippo.PACK_FIXED) + def add_item(self, item): start_pos = int(random.random() * len(self._grid)) @@ -60,3 +71,11 @@ class SpreadBox(hippo.CanvasBox): if self._grid[i] == item: self._grid[i] = None self.remove(item) + + def do_allocate(self, width, height, origin_changed): + hippo.CanvasBox.do_allocate(self, width, height, origin_changed) + + if self._center: + [icon_width, icon_height] = self._center.get_allocation() + self.set_position(self._center, (width - icon_width) / 2, + (height - icon_height) / 2) diff --git a/sugar/graphics2/Makefile.am b/sugar/graphics2/Makefile.am new file mode 100644 index 00000000..f5bde18a --- /dev/null +++ b/sugar/graphics2/Makefile.am @@ -0,0 +1,5 @@ +sugardir = $(pythondir)/sugar/graphics2 +sugar_PYTHON = \ + __init__.py \ + window.py \ + toolbox.py diff --git a/sugar/graphics2/__init__.py b/sugar/graphics2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sugar/graphics2/toolbox.py b/sugar/graphics2/toolbox.py new file mode 100644 index 00000000..be7e5e09 --- /dev/null +++ b/sugar/graphics2/toolbox.py @@ -0,0 +1,32 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk + +class Toolbox(gtk.VBox): + __gtype_name__ = 'SugarToolbox' + def __init__(self): + gtk.VBox.__init__(self) + + self._notebook = gtk.Notebook() + self._notebook.set_tab_pos(gtk.POS_BOTTOM) + self._notebook.set_show_border(False) + self.pack_start(self._notebook) + self._notebook.show() + + def add_toolbar(self, name, toolbar): + self._notebook.append_page(toolbar, gtk.Label(name)) diff --git a/sugar/graphics2/window.py b/sugar/graphics2/window.py new file mode 100644 index 00000000..d03788b0 --- /dev/null +++ b/sugar/graphics2/window.py @@ -0,0 +1,49 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk +import hippo + +from sugar.graphics2.toolbox import Toolbox + +class Window(gtk.Window): + def __init__(self): + gtk.Window.__init__(self) + + vbox = gtk.VBox() + self.add(vbox) + + self.toolbox = Toolbox() + vbox.pack_start(self.toolbox, False) + self.toolbox.show() + + self._canvas_box = gtk.VBox() + vbox.pack_start(self._canvas_box) + self._canvas_box.show() + + self.canvas = hippo.Canvas() + self._canvas_box.pack_start(self.canvas) + self.canvas.show() + + vbox.show() + + def set_canvas(self, canvas): + if self.canvas: + self._canvas_box.remove(self.canvas) + + self._canvas_box.add(canvas) + self.canvas = canvas diff --git a/sugar/ltihooks.py b/sugar/ltihooks.py new file mode 100644 index 00000000..d68f2ebc --- /dev/null +++ b/sugar/ltihooks.py @@ -0,0 +1,72 @@ +# -*- Mode: Python -*- +# vi:si:et:sw=4:sts=4:ts=4 + +# ltihooks.py: python import hooks that understand libtool libraries. +# Copyright (C) 2000 James Henstridge. +# renamed to gstltihooks.py so it does not accidentally get imported by +# an installed copy of gtk +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os, ihooks + +class LibtoolHooks(ihooks.Hooks): + def get_suffixes(self): + """Like normal get_suffixes, but adds .la suffixes to list""" + ret = ihooks.Hooks.get_suffixes(self) + ret.insert(0, ('module.la', 'rb', 3)) + ret.insert(0, ('.la', 'rb', 3)) + return ret + + def load_dynamic(self, name, filename, file=None): + """Like normal load_dynamic, but treat .la files specially""" + if len(filename) > 3 and filename[-3:] == '.la': + fp = open(filename, 'r') + dlname = '' + installed = 1 + line = fp.readline() + while line: + # dlname: the name that we can dlopen + if len(line) > 7 and line[:7] == 'dlname=': + dlname = line[8:-2] + # installed: whether it's already installed + elif len(line) > 10 and line[:10] == 'installed=': + installed = line[10:-1] == 'yes' + line = fp.readline() + fp.close() + if dlname: + if installed: + filename = os.path.join(os.path.dirname(filename), + dlname) + else: + # if .libs already there, don't need to add it again + if os.path.dirname(filename).endswith('.libs'): + filename = os.path.join(os.path.dirname(filename), + dlname) + else: + filename = os.path.join(os.path.dirname(filename), + '.libs', dlname) + return ihooks.Hooks.load_dynamic(self, name, filename, file) + +importer = ihooks.ModuleImporter() +importer.set_hooks(LibtoolHooks()) + +def install(): + print 'Installed ltihooks.' + importer.install() +def uninstall(): + importer.uninstall() + +install() diff --git a/sugar/presence/presenceservice.py b/sugar/presence/presenceservice.py index 78f51e6b..e61c8d74 100644 --- a/sugar/presence/presenceservice.py +++ b/sugar/presence/presenceservice.py @@ -337,6 +337,19 @@ class PresenceService(gobject.GObject): reply_handler=lambda *args: self._share_activity_cb(activity, *args), error_handler=lambda *args: self._share_activity_error_cb(activity, *args)) + def get_preferred_connection(self): + """Gets the preferred telepathy connection object that an activity + should use when talking directly to telepathy + + returns the bus name and the object path of the Telepathy connection""" + + try: + bus_name, object_path = self._ps.GetPreferredConnection() + except dbus.exceptions.DBusException: + return None + + return bus_name, object_path + class _MockPresenceService(gobject.GObject): """Test fixture allowing testing of items that use PresenceService diff --git a/tests/test-button.py b/tests/test-button.py deleted file mode 100755 index 9cceb92d..00000000 --- a/tests/test-button.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python - -# 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 sys -sys.path.insert(0, '/home/tomeu/sugar-jhbuild/source/sugar') - -import gtk -import hippo - -from sugar.graphics.toolbar import Toolbar -from sugar.graphics.iconbutton import IconButton -from sugar.graphics.toggleiconbutton import ToggleIconButton -from sugar.graphics.button import Button -from sugar.graphics.entry import Entry - -def _button_activated_cb(button): - print "_button_activated_cb" - -def _toggled_changed_cb(button, pspec): - print "Toggle state: %d" % button.props.toggled - -window = gtk.Window() -window.connect("destroy", lambda w: gtk.main_quit()) -window.show() - -canvas = hippo.Canvas() -window.add(canvas) -canvas.show() - -vbox = hippo.CanvasBox() -canvas.set_root(vbox) - -for i in [1, 2]: - toolbar = Toolbar() - toolbar.props.box_width = 400 - vbox.append(toolbar) - - icon_button = IconButton(icon_name='theme:stock-close') - toolbar.append(icon_button) - - toggle = ToggleIconButton(icon_name='theme:stock-back') - toggle.connect('notify::toggled', _toggled_changed_cb) - toolbar.append(toggle) - - button = Button(text='Click me!', icon_name='theme:stock-close') - button.connect('activated', _button_activated_cb) - toolbar.append(button) - - entry = Entry(text='mec') - toolbar.append(entry) - -gtk.main() diff --git a/tests/test-entry.py b/tests/test-entry.py deleted file mode 100755 index 80f0e234..00000000 --- a/tests/test-entry.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python - -# 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 gtk -import hippo - -from sugar.graphics.toolbar import Toolbar -from sugar.graphics.frame import Frame -from sugar.graphics.iconbutton import IconButton -from sugar.graphics.entry import Entry - -def _entry_activated_cb(entry): - print "_entry_activated_cb" - -def _entry_button_activated_cb(entry, action_id): - print "_entry_button_activated_cb: " + str(action_id) - entry.props.text = '' - -window = gtk.Window() -window.connect("destroy", lambda w: gtk.main_quit()) -window.show() - -canvas = hippo.Canvas() -window.add(canvas) -canvas.show() - -vbox = hippo.CanvasBox() -canvas.set_root(vbox) - -for i in [1, 2]: - toolbar = Toolbar() - vbox.append(toolbar) - - button = IconButton('theme:stock-close') - toolbar.append(button) - - BUTTON_DELETE = 1 - entry = Entry() - entry.props.text = 'mec mac' - entry.add_button('theme:stock-close', BUTTON_DELETE) - entry.connect('activated', _entry_activated_cb) - entry.connect('button-activated', _entry_button_activated_cb) - toolbar.append(entry, hippo.PACK_EXPAND) - - entry = Entry() - entry.props.text = 'moc muc' - toolbar.append(entry, hippo.PACK_EXPAND) - - gtk_entry = gtk.Entry() - gtk_entry.props.has_frame = False - #gtk_entry.connect("activate", self._entry_activate_cb) - - gtk_entry_widget = hippo.CanvasWidget() - gtk_entry_widget.props.widget = gtk_entry - toolbar.append(gtk_entry_widget, hippo.PACK_EXPAND) - -gtk.main() diff --git a/tests/test-frame.py b/tests/test-frame.py deleted file mode 100755 index 4a22fd1f..00000000 --- a/tests/test-frame.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python - -# 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 gtk -import hippo - -from sugar.graphics.frame import Frame - -window = gtk.Window() -window.connect("destroy", lambda w: gtk.main_quit()) -window.show() - -canvas = hippo.Canvas() - -frame = Frame() -canvas.set_root(frame) - -window.add(canvas) -canvas.show() - -gtk.main() diff --git a/tests/test-label.py b/tests/test-label.py deleted file mode 100755 index 6e7b6b28..00000000 --- a/tests/test-label.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python - -# 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 gtk -import hippo - -from sugar.graphics.toolbar import Toolbar -from sugar.graphics.label import Label -from sugar.graphics.iconbutton import IconButton - -BUTTON_DELETE = 1 - -window = gtk.Window() -window.connect("destroy", lambda w: gtk.main_quit()) -window.show() - -canvas = hippo.Canvas() -window.add(canvas) -canvas.show() - -vbox = hippo.CanvasBox() -canvas.set_root(vbox) - -toolbar = Toolbar() -vbox.append(toolbar) - -button = IconButton('theme:stock-close') -toolbar.append(button) - -label = Label('mec moc') -toolbar.append(label) - -label = Label() -label.props.text = 'mac mic' -toolbar.append(label) - -gtk.main() diff --git a/tests/test-option-menu.py b/tests/test-option-menu.py deleted file mode 100755 index a9202da8..00000000 --- a/tests/test-option-menu.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python - -# 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 sys -sys.path.insert(0, '/home/tomeu/sugar-jhbuild/source/sugar') -from gettext import gettext as _ - -import gtk -import hippo - -from sugar.graphics.toolbar import Toolbar -from sugar.graphics.optionmenu import OptionMenu -from sugar.graphics.menu import MenuItem -from sugar.graphics.iconbutton import IconButton - -def _option_menu_changed_cb(option_menu): - print '_option_menu_activated_cb: %i' % option_menu.props.value - -window = gtk.Window() -window.connect("destroy", lambda w: gtk.main_quit()) -window.show() - -canvas = hippo.Canvas() -window.add(canvas) -canvas.show() - -vbox = hippo.CanvasBox() -canvas.set_root(vbox) - -toolbar = Toolbar() -vbox.append(toolbar) - -button = IconButton(icon_name='theme:stock-close') -toolbar.append(button) - -OPTION_ANYTHING = 1 -OPTION_DRAW = 2 -OPTION_WRITE = 3 -OPTION_CHAT = 4 - -option_menu = OptionMenu() -option_menu.add_item(MenuItem(OPTION_ANYTHING, 'Anything')) -option_menu.add_separator() -option_menu.add_item(MenuItem(OPTION_DRAW, 'Draw', 'theme:stock-close')) -option_menu.add_item(MenuItem(OPTION_WRITE, 'Write')) -option_menu.add_item(MenuItem(OPTION_CHAT, 'Chat')) -option_menu.connect('changed', _option_menu_changed_cb) -toolbar.append(option_menu) - -gtk.main() diff --git a/tests/test-snowflake-box.py b/tests/test-snowflake-box.py index a0c35bb7..4164af4f 100755 --- a/tests/test-snowflake-box.py +++ b/tests/test-snowflake-box.py @@ -33,14 +33,14 @@ from sugar.graphics.canvasicon import CanvasIcon def _create_snowflake(parent, children): color = XoColor() - icon = CanvasIcon(scale=0.5, xo_color=color, + icon = CanvasIcon(scale=1.0, xo_color=color, icon_name='theme:object-link') parent.append(icon, hippo.PACK_FIXED) parent.set_root(icon) for i in range(0, children): color = XoColor() - icon = CanvasIcon(scale=1.0, xo_color=color, + icon = CanvasIcon(scale=0.5, xo_color=color, icon_name='theme:stock-buddy') parent.append(icon, hippo.PACK_FIXED) diff --git a/tests/test-theme.py b/tests/test-theme.py deleted file mode 100755 index eae835fc..00000000 --- a/tests/test-theme.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python - -# 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 pygtk -pygtk.require('2.0') - -import gtk - -# Main window -window = gtk.Window() -window.connect("destroy", lambda w: gtk.main_quit()) -#window.set_border_width(10) - -# Main VBox - -main_vbox = gtk.VBox(homogeneous=False, spacing=0) -window.add(main_vbox) - -############################### ############################## -############################### Menus ############################## -############################### ############################## - -menu = gtk.Menu() -file_menu = gtk.Menu() # Don't need to show menus -edit_menu = gtk.Menu() - -# Create the menu items -dummy_item_1 = gtk.MenuItem("Dummy Item 1") -dummy_item_2 = gtk.MenuItem("Dummy Item 2") -quit_item = gtk.MenuItem("Quit") -dummy_item_3 = gtk.MenuItem("Dummy Item 3") -dummy_item_4 = gtk.MenuItem("Dummy Item 4") -dummy_item_5 = gtk.MenuItem("Dummy Item 5") - -# Add them to the menu -file_menu.append(dummy_item_1) -file_menu.append(dummy_item_2) -file_menu.append(quit_item) - -edit_menu.append(dummy_item_3) -edit_menu.append(dummy_item_4) -edit_menu.append(dummy_item_5) - -# We can attach the Quit menu item to our exit function -quit_item.connect_object ("activate", lambda w: gtk.main_quit (), "file.quit") - -# We do need to show menu items -dummy_item_1.show() -dummy_item_2.show() -quit_item.show() -dummy_item_3.show() -dummy_item_4.show() -dummy_item_5.show() - -# Pack the menu into the menubar -menu_bar = gtk.MenuBar() -main_vbox.pack_start(menu_bar, False, False, 0) -menu_bar.show() - -file_item = gtk.MenuItem("File") -file_item.show() -menu_bar.append(file_item) -file_item.set_submenu(file_menu) - -edit_item = gtk.MenuItem("Edit") -edit_item.show() -menu_bar.append(edit_item) -edit_item.set_submenu(edit_menu) - - -# Scrolled window -scrolled_window = gtk.ScrolledWindow(hadjustment=None, vadjustment=None) -#scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) -scrolled_window.set_border_width(10) -main_vbox.pack_start(scrolled_window, True, True, 0) - -# Vbox inside the scrolled window -vbox = gtk.VBox(homogeneous=False, spacing=10) -scrolled_window.add_with_viewport(vbox) -vbox.set_border_width (10) - -# Label -label = gtk.Label("This is a label") -vbox.pack_start(label, False, False, 0) - -# Entry -entry = gtk.Entry () -entry.set_text("Type some text here") -vbox.pack_start(entry, False, False, 0) - -# Buttons -buttons_hbox = gtk.HBox(homogeneous=False, spacing=5) -vbox.pack_start(buttons_hbox, False, False, 0) - -button_1 = gtk.Button ("Button 1") -buttons_hbox.pack_start(button_1, False, False, 0) - -button_2 = gtk.Button ("Button 2") -buttons_hbox.pack_start(button_2, False, False, 0) - -button_3 = gtk.Button ("Button 3") -buttons_hbox.pack_start(button_3, False, False, 0) - -window.show_all() - -gtk.main() diff --git a/tests/test-ui.py b/tests/test-ui.py new file mode 100755 index 00000000..26f3d127 --- /dev/null +++ b/tests/test-ui.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python + +# 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 gtk + +from sugar.graphics2.window import Window + +class ActivityToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + +class EditToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + +class TextToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + + button = gtk.ToolButton() + button.set_icon_name('text-format-bold') + self.insert(button, -1) + button.show() + +class ImageToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + +class TableToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + +class FormatToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + +class ViewToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + +window = Window() +window.connect("destroy", lambda w: gtk.main_quit()) + +activity_toolbar = ActivityToolbar() +window.toolbox.add_toolbar('Activity', activity_toolbar) +activity_toolbar.show() + +edit_toolbar = EditToolbar() +window.toolbox.add_toolbar('Edit', edit_toolbar) +edit_toolbar.show() + +text_toolbar = TextToolbar() +window.toolbox.add_toolbar('Text', text_toolbar) +text_toolbar.show() + +image_toolbar = ImageToolbar() +window.toolbox.add_toolbar('Image', image_toolbar) +image_toolbar.show() + +table_toolbar = TableToolbar() +window.toolbox.add_toolbar('Table', table_toolbar) +table_toolbar.show() + +format_toolbar = FormatToolbar() +window.toolbox.add_toolbar('Format', format_toolbar) +format_toolbar.show() + +view_toolbar = ViewToolbar() +window.toolbox.add_toolbar('View', view_toolbar) +view_toolbar.show() + +scrolled_window = gtk.ScrolledWindow() +scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) +window.set_canvas(scrolled_window) +scrolled_window.show() + +text_view = gtk.TextView() +scrolled_window.add(text_view) +text_view.show() + +window.show() + +gtk.main() diff --git a/tests/test-window-manager.py b/tests/test-window-manager.py deleted file mode 100644 index 539979de..00000000 --- a/tests/test-window-manager.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python - -# 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 pygtk -pygtk.require('2.0') - -from sugar.session.UITestSession import UITestSession - -session = UITestSession() -session.start() - -import gtk - -def _show_dialog(window): - dialog = gtk.Dialog(title='No Unviewed Media', - parent=window, flags=gtk.DIALOG_MODAL, - buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) - label = gtk.Label('There is no unviewed media to download.') - dialog.vbox.pack_start(label, True, True, 0) - label.show() - response = dialog.run() - dialog.hide() - del dialog - -window = gtk.Window() -window.show() - -_show_dialog(window) - -gtk.main()