/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: NPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Netscape Public License * Version 1.1 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.mozilla.org/NPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is * Netscape Communications Corporation. * Portions created by the Initial Developer are Copyright (C) 2002 * the Initial Developer. All Rights Reserved. * * Contributor(s): * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the NPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the NPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #import "NSBrowserView.h" #include "BookmarksService.h" #include "nsIDocument.h" #include "nsIContent.h" #include "nsIAtom.h" #include "nsITextContent.h" #include "nsIDOMWindow.h" #include "nsIDOMHTMLDocument.h" #include "nsIDOMElement.h" #include "nsString.h" #include "nsIFile.h" #include "nsAppDirectoryServiceDefs.h" #include "nsIXMLHttpRequest.h" #include "nsIDOMSerializer.h" #include "nsNetUtil.h" #include "nsINamespaceManager.h" #include "nsIXBLService.h" #include "nsIWebBrowser.h" @implementation BookmarksDataSource -(id) init { [super init]; mBookmarks = nsnull; mCachedParent = nsnull; mCachedHref = nil; return self; } -(void) dealloc { [super dealloc]; } -(void) windowClosing { if (mBookmarks) { mBookmarks->RemoveObserver(); delete mBookmarks; } } -(void) ensureBookmarks { if (mBookmarks) return; mBookmarks = new BookmarksService(self); mBookmarks->AddObserver(); [mOutlineView setTarget: self]; [mOutlineView setDoubleAction: @selector(openBookmark:)]; [mOutlineView reloadData]; } -(IBAction)addBookmark:(id)aSender { [self addBookmark: aSender useSelection: YES]; } -(void)addBookmark:(id)aSender useSelection:(BOOL)aUseSel { if (!mBookmarks) return; nsCOMPtr content; if (aUseSel) { int index = [mOutlineView selectedRow]; if (index >= 0) { BookmarkItem* item = [mOutlineView itemAtRow: index]; if ([mOutlineView isExpandable: item]) content = [item contentNode]; } } if (!content) mBookmarks->GetRootContent(getter_AddRefs(content)); nsCOMPtr domDoc(do_QueryInterface(mBookmarks->gBookmarks)); // Fetch the title of the current page and the URL. nsCOMPtr webBrowser = getter_AddRefs([[[mBrowserWindowController getMyBrowserView] getBrowserView] getWebBrowser]); nsCOMPtr window; webBrowser->GetContentDOMWindow(getter_AddRefs(window)); nsCOMPtr htmlDoc; window->GetDocument(getter_AddRefs(htmlDoc)); nsCOMPtr pageDoc(do_QueryInterface(htmlDoc)); nsAutoString href; if (pageDoc) { nsCOMPtr url; pageDoc->GetDocumentURL(getter_AddRefs(url)); nsCAutoString spec; url->GetSpec(spec); href.AssignWithConversion(spec.get()); } mCachedHref = [NSString stringWithCharacters: href.get() length: nsCRT::strlen(href.get())]; [mCachedHref retain]; mCachedParent = content; nsAutoString title; nsCOMPtr htmlDocument(do_QueryInterface(htmlDoc)); if (htmlDocument) htmlDocument->GetTitle(title); if (title.IsEmpty()) title = href; NSTextField* textField = [mBrowserWindowController getAddBookmarkTitle]; [textField setStringValue: [NSString stringWithCharacters: title.get() length: nsCRT::strlen(title.get())]]; [mBrowserWindowController cacheBookmarkDS: self]; [NSApp beginSheet: [mBrowserWindowController getAddBookmarkSheetWindow] modalForWindow: [mBrowserWindowController window] modalDelegate: nil //self didEndSelector: nil //@selector(sheetDidEnd:) contextInfo: nil]; } -(void)endAddBookmark: (int)aCode { if (aCode == 0) return; const char* titleC = [[[mBrowserWindowController getAddBookmarkTitle] stringValue] cString]; nsAutoString title; title.AssignWithConversion(titleC); nsCOMPtr domDoc(do_QueryInterface(mBookmarks->gBookmarks)); nsCOMPtr elt; domDoc->CreateElementNS(NS_LITERAL_STRING("http://chimera.mozdev.org/bookmarks"), NS_LITERAL_STRING("bookmark"), getter_AddRefs(elt)); elt->SetAttribute(NS_LITERAL_STRING("name"), title); nsAutoString href; href.AssignWithConversion([mCachedHref cString]); [mCachedHref release]; elt->SetAttribute(NS_LITERAL_STRING("href"), href); nsCOMPtr parent(do_QueryInterface(mCachedParent)); nsCOMPtr dummy; parent->AppendChild(elt, getter_AddRefs(dummy)); nsCOMPtr childContent(do_QueryInterface(elt)); mBookmarks->BookmarkAdded(mCachedParent, childContent); } -(IBAction)deleteBookmark: (id)aSender { if (!mBookmarks) return; int index = [mOutlineView selectedRow]; if (index == -1) return; BookmarkItem* item = [mOutlineView itemAtRow: index]; nsCOMPtr content = [item contentNode]; nsCOMPtr child(do_QueryInterface(content)); nsCOMPtr parent; child->GetParentNode(getter_AddRefs(parent)); nsCOMPtr parentContent(do_QueryInterface(parent)); nsCOMPtr dummy; parent->RemoveChild(child, getter_AddRefs(dummy)); mBookmarks->BookmarkRemoved(parentContent, content); int total = [mOutlineView numberOfRows]; if (index == total) index--; [mOutlineView selectRow: index byExtendingSelection: NO]; } -(IBAction)openBookmark: (id)aSender { int index = [mOutlineView selectedRow]; if (index == -1) return; id item = [mOutlineView itemAtRow: index]; if (!item) return; if ([mOutlineView isExpandable: item]) { if ([mOutlineView isItemExpanded: item]) [mOutlineView collapseItem: item]; else [mOutlineView expandItem: item]; } else { nsIContent* content = [item contentNode]; nsAutoString href; content->GetAttr(kNameSpaceID_None, BookmarksService::gHrefAtom, href); if (!href.IsEmpty()) { nsCAutoString cstr; cstr.AssignWithConversion(href); NSString* url = [NSString stringWithCString: cstr.get()]; [[[mBrowserWindowController getMyBrowserView] getBrowserView] loadURI:[NSURL URLWithString: url] flags:NSLoadFlagsNone]; // Focus and activate our content area. [[[mBrowserWindowController getMyBrowserView] getBrowserView] setActive: YES]; } } } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item { return NO; } - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item { if (!mBookmarks) return nil; nsCOMPtr content; if (!item) mBookmarks->GetRootContent(getter_AddRefs(content)); else content = [item contentNode]; nsCOMPtr child; content->ChildAt(index, *getter_AddRefs(child)); return mBookmarks->GetWrapperFor(child); } - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { if (!mBookmarks) return NO; if (!item) return YES; // The root node is always open. nsCOMPtr tagName; nsIContent* content = [item contentNode]; content->GetTag(*getter_AddRefs(tagName)); return (tagName == BookmarksService::gFolderAtom); } - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { if (!mBookmarks) return 0; nsCOMPtr content; if (!item) mBookmarks->GetRootContent(getter_AddRefs(content)); else content = [item contentNode]; PRInt32 childCount; content->ChildCount(childCount); return childCount; } - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { NSString *columnName = [tableColumn identifier]; NSMutableAttributedString *cellValue = [[NSMutableAttributedString alloc] init]; NSFileWrapper *fileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:nil]; NSTextAttachment *textAttachment = [[NSTextAttachment alloc] initWithFileWrapper:fileWrapper]; NSMutableAttributedString *attachmentAttrString = nil; NSCell *attachmentAttrStringCell; if ([columnName isEqualToString: @"name"]) { nsIContent* content = [item contentNode]; nsAutoString nameAttr; content->GetAttr(kNameSpaceID_None, BookmarksService::gNameAtom, nameAttr); nsCAutoString cStr; cStr.AssignWithConversion(nameAttr); //Set cell's textual contents [cellValue replaceCharactersInRange:NSMakeRange(0, [cellValue length]) withString:[NSString stringWithCString: cStr.get()]]; //Create an attributed string to hold the empty attachment, then release the components. attachmentAttrString = [[NSMutableAttributedString attributedStringWithAttachment:textAttachment] retain]; [textAttachment release]; [fileWrapper release]; //Get the cell of the text attachment. attachmentAttrStringCell = (NSCell *)[(NSTextAttachment *)[attachmentAttrString attribute:NSAttachmentAttributeName atIndex:0 effectiveRange:nil] attachmentCell]; //Figure out which image to add, and set the cell's image. if ( [self outlineView:outlineView isItemExpandable:item] ) { [attachmentAttrStringCell setImage:[NSImage imageNamed:@"folder"]]; } else { [attachmentAttrStringCell setImage:[NSImage imageNamed:@"smallbookmark"]]; } //Insert the image [cellValue replaceCharactersInRange:NSMakeRange(0, 0) withAttributedString:attachmentAttrString]; //Tweak the baseline to vertically center the text. [cellValue addAttribute:NSBaselineOffsetAttributeName value:[NSNumber numberWithFloat:-3.0] range:NSMakeRange(0, 1)]; } return cellValue; } - (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { } - (void)reloadDataForItem:(id)item reloadChildren: (BOOL)aReloadChildren { printf("Reloading?\n"); if (!item) [mOutlineView reloadData]; else if ([mOutlineView isItemExpanded: item]) [mOutlineView reloadItem: item reloadChildren: aReloadChildren]; } @end @implementation BookmarkItem -(nsIContent*)contentNode { return mContentNode; } -(void)setContentNode: (nsIContent*)aContentNode { mContentNode = aContentNode; } - (id)copyWithZone:(NSZone *)aZone { BookmarkItem* copy = [[[self class] allocWithZone: aZone] init]; [copy setContentNode: mContentNode]; return copy; } @end // Helper for stripping whitespace static void StripWhitespaceNodes(nsIContent* aElement) { PRInt32 childCount; aElement->ChildCount(childCount); for (PRInt32 i = 0; i < childCount; i++) { nsCOMPtr child; aElement->ChildAt(i, *getter_AddRefs(child)); nsCOMPtr text = do_QueryInterface(child); if (text) { PRBool isEmpty; text->IsOnlyWhitespace(&isEmpty); if (isEmpty) { // This node contained nothing but whitespace. // Remove it from the content model. aElement->RemoveChildAt(i, PR_TRUE); i--; // Decrement our count, since we just removed this child. childCount--; // Also decrement our total count. } } else StripWhitespaceNodes(child); } } PRUint32 BookmarksService::gRefCnt = 0; nsIDocument* BookmarksService::gBookmarks = nsnull; NSMutableDictionary* BookmarksService::gDictionary = nil; MainController* BookmarksService::gMainController = nil; NSMenu* BookmarksService::gBookmarksMenu = nil; nsIDOMElement* BookmarksService::gToolbarRoot = nsnull; nsIAtom* BookmarksService::gFolderAtom = nsnull; nsIAtom* BookmarksService::gBookmarkAtom = nsnull; nsIAtom* BookmarksService::gHrefAtom = nsnull; nsIAtom* BookmarksService::gNameAtom = nsnull; nsVoidArray* BookmarksService::gInstances = nsnull; BookmarksService::BookmarksService(BookmarksDataSource* aDataSource) { mDataSource = aDataSource; mToolbar = nil; } BookmarksService::BookmarksService(BookmarksToolbar* aToolbar) { mDataSource = nil; mToolbar = aToolbar; } BookmarksService::~BookmarksService() { } void BookmarksService::GetRootContent(nsIContent** aResult) { *aResult = nsnull; if (gBookmarks) { nsCOMPtr elt; nsCOMPtr domDoc(do_QueryInterface(gBookmarks)); domDoc->GetDocumentElement(getter_AddRefs(elt)); elt->QueryInterface(NS_GET_IID(nsIContent), (void**)aResult); // Addref happens here. } } BookmarkItem* BookmarksService::GetWrapperFor(nsIContent* aContent) { if (!gDictionary) gDictionary = [[NSMutableDictionary alloc] initWithCapacity: 30]; PRUint32 contentID; aContent->GetContentID(&contentID); BookmarkItem* item = [gDictionary objectForKey: [NSNumber numberWithInt: contentID]]; if (item) return item; else { // Create an item. item = [[[BookmarkItem alloc] init] autorelease]; // The dictionary retains us. [item setContentNode: aContent]; [gDictionary setObject: item forKey: [NSNumber numberWithInt: contentID]]; } return item; } NSMenu* BookmarksService::LocateMenu(nsIContent* aContent) { nsCOMPtr parent; aContent->GetParent(*getter_AddRefs(parent)); if (!parent) { return BookmarksService::gBookmarksMenu; } NSMenu* parentMenu = LocateMenu(parent); PRUint32 contentID; aContent->GetContentID(&contentID); NSMenuItem* childMenu = [parentMenu itemWithTag: contentID]; return [childMenu submenu]; } void BookmarksService::BookmarkAdded(nsIContent* aContainer, nsIContent* aChild) { if (!gInstances || !gDictionary) return; PRInt32 count = gInstances->Count(); for (PRInt32 i = 0; i < count; i++) { BookmarksService* instance = (BookmarksService*)gInstances->ElementAt(i); if (instance->mDataSource) { // We're a tree view. nsCOMPtr parent; aContainer->GetParent(*getter_AddRefs(parent)); BookmarkItem* item = nil; if (parent) // We're not the root. item = GetWrapperFor(aContainer); [(instance->mDataSource) reloadDataForItem: item reloadChildren: YES]; } else if (instance->mToolbar) { // We're a personal toolbar. nsCOMPtr parentElt(do_QueryInterface(aContainer)); if (parentElt == gToolbarRoot) { // We only care about changes that occur to the personal toolbar's immediate // children. PRInt32 index = -1; aContainer->IndexOf(aChild, index); nsCOMPtr elt(do_QueryInterface(aChild)); [(instance->mToolbar) addButton: elt atIndex: index]; } } else { // We're the menu. PRInt32 index = -1; aContainer->IndexOf(aChild, index); NSMenu* menu = LocateMenu(aContainer); AddMenuBookmark(menu, aContainer, aChild, index); } } FlushBookmarks(); } void BookmarksService::BookmarkChanged(nsIContent* aItem) { if (!gInstances || !gDictionary) return; PRInt32 count = gInstances->Count(); for (PRInt32 i = 0; i < count; i++) { BookmarksService* instance = (BookmarksService*)gInstances->ElementAt(i); if (instance->mDataSource) { BookmarkItem* item = GetWrapperFor(aItem); [(instance->mDataSource) reloadDataForItem: item reloadChildren: NO]; } } FlushBookmarks(); } void BookmarksService::BookmarkRemoved(nsIContent* aContainer, nsIContent* aChild) { if (!gInstances) return; PRInt32 count = gInstances->Count(); for (PRInt32 i = 0; i < count; i++) { BookmarksService* instance = (BookmarksService*)gInstances->ElementAt(i); if (instance->mDataSource) { // We're a tree view. nsCOMPtr parent; aContainer->GetParent(*getter_AddRefs(parent)); BookmarkItem* item = nil; if (parent) // We're not the root. item = GetWrapperFor(aContainer); [(instance->mDataSource) reloadDataForItem: item reloadChildren: YES]; } else if (instance->mToolbar) { // We're a personal toolbar. nsCOMPtr parentElt(do_QueryInterface(aContainer)); if (parentElt == gToolbarRoot) { // We only care about changes that occur to the personal toolbar's immediate // children. nsCOMPtr childElt(do_QueryInterface(aChild)); [(instance->mToolbar) removeButton: childElt]; } } else { // We're the menu. NSMenu* menu = LocateMenu(aContainer); PRUint32 contentID; aChild->GetContentID(&contentID); NSMenuItem* childItem = [menu itemWithTag: contentID]; [menu removeItem: childItem]; } } FlushBookmarks(); } void BookmarksService::AddObserver() { gRefCnt++; if (gRefCnt == 1) { gBookmarkAtom = NS_NewAtom("bookmark"); gFolderAtom = NS_NewAtom("folder"); gNameAtom = NS_NewAtom("name"); gHrefAtom = NS_NewAtom("href"); gInstances = new nsVoidArray(); nsCOMPtr profileDir; NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(profileDir)); profileDir->Append("bookmarks.xml"); nsCAutoString bookmarksFileURL; NS_GetURLSpecFromFile(profileDir, bookmarksFileURL); nsCOMPtr uri; NS_NewURI(getter_AddRefs(uri), bookmarksFileURL.get()); nsCOMPtr xblService(do_GetService("@mozilla.org/xbl;1")); xblService->FetchSyncXMLDocument(uri, &gBookmarks); // The addref is here. nsCOMPtr rootNode; GetRootContent(getter_AddRefs(rootNode)); StripWhitespaceNodes(rootNode); } gInstances->AppendElement(this); } void BookmarksService::RemoveObserver() { if (gRefCnt == 0) return; gInstances->RemoveElement(this); gRefCnt--; if (gRefCnt == 0) { NS_IF_RELEASE(gBookmarks); NS_RELEASE(gBookmarkAtom); NS_RELEASE(gFolderAtom); NS_RELEASE(gNameAtom); NS_RELEASE(gHrefAtom); [gDictionary release]; } } void BookmarksService::FlushBookmarks() { nsCOMPtr bookmarksFile; NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(bookmarksFile)); bookmarksFile->Append("bookmarks.xml"); nsCOMPtr outputStream; NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), bookmarksFile); nsCOMPtr domDoc(do_QueryInterface(gBookmarks)); nsCOMPtr domSerializer(do_CreateInstance(NS_XMLSERIALIZER_CONTRACTID)); domSerializer->SerializeToStream(domDoc, outputStream, nsnull); } void BookmarksService::EnsureToolbarRoot() { if (gToolbarRoot) return; nsCOMPtr domDoc(do_QueryInterface(gBookmarks)); nsCOMPtr rootElt; domDoc->GetDocumentElement(getter_AddRefs(rootElt)); nsCOMPtr child; rootElt->GetFirstChild(getter_AddRefs(child)); nsAutoString typeValue; while (child) { nsCOMPtr childElt(do_QueryInterface(child)); if (childElt) { childElt->GetAttribute(NS_LITERAL_STRING("type"), typeValue); if (typeValue.Equals(NS_LITERAL_STRING("toolbar"))) gToolbarRoot = childElt; } nsCOMPtr temp; child->GetNextSibling(getter_AddRefs(temp)); child = temp; } if (!gToolbarRoot) { printf("Repairing personal toolbar.\n"); nsCOMPtr elt; domDoc->CreateElementNS(NS_LITERAL_STRING("http://chimera.mozdev.org/bookmarks/"), NS_LITERAL_STRING("folder"), getter_AddRefs(elt)); elt->SetAttribute(NS_LITERAL_STRING("name"), NS_LITERAL_STRING("Toolbar Bookmarks")); elt->SetAttribute(NS_LITERAL_STRING("type"), NS_LITERAL_STRING("toolbar")); nsCOMPtr dummy; rootElt->AppendChild(elt, getter_AddRefs(dummy)); gToolbarRoot = elt; } } void BookmarksService::ConstructBookmarksMenu(NSMenu* aMenu, nsIContent* aContent) { nsCOMPtr content = aContent; if (!content) { GetRootContent(getter_AddRefs(content)); GetWrapperFor(content); gBookmarksMenu = aMenu; } // Now walk our children, and for folders also recur into them. PRInt32 childCount; content->ChildCount(childCount); for (PRInt32 i = 0; i < childCount; i++) { nsCOMPtr child; content->ChildAt(i, *getter_AddRefs(child)); AddMenuBookmark(aMenu, content, child, -1); } } void BookmarksService::AddMenuBookmark(NSMenu* aMenu, nsIContent* aParent, nsIContent* aChild, PRInt32 aIndex) { nsAutoString name; aChild->GetAttr(kNameSpaceID_None, gNameAtom, name); nsCAutoString nameCStr; nameCStr.AssignWithConversion(name); NSString* title = [NSString stringWithCString: nameCStr.get()]; // Create a menu or menu item for the child. NSMenuItem* menuItem = [[[NSMenuItem alloc] initWithTitle: title action: NULL keyEquivalent: @""] autorelease]; GetWrapperFor(aChild); if (aIndex == -1) [aMenu addItem: menuItem]; else [aMenu insertItem: menuItem atIndex: aIndex]; nsCOMPtr tagName; aChild->GetTag(*getter_AddRefs(tagName)); if (tagName == gFolderAtom) { NSMenu* menu = [[[NSMenu alloc] initWithTitle: title] autorelease]; [aMenu setSubmenu: menu forItem: menuItem]; [menu setAutoenablesItems: NO]; ConstructBookmarksMenu(menu, aChild); } else { [menuItem setTarget: gMainController]; [menuItem setAction: @selector(openMenuBookmark:)]; } PRUint32 contentID; aChild->GetContentID(&contentID); [menuItem setTag: contentID]; } void BookmarksService::OpenMenuBookmark(BrowserWindowController* aController, id aMenuItem) { // Get the corresponding bookmark item. BookmarkItem* item = [gDictionary objectForKey: [NSNumber numberWithInt: [aMenuItem tag]]]; // Get the content node. nsIContent* content = [item contentNode]; // Get the href attribute. This is the URL we want to load. nsAutoString href; content->GetAttr(kNameSpaceID_None, gHrefAtom, href); nsCAutoString cref; cref.AssignWithConversion(href); if (cref.IsEmpty()) return; NSString* url = [NSString stringWithCString: cref.get()]; // Now load the URL in the window. [aController loadURL:[NSURL URLWithString: url]]; // Focus and activate our content area. [[[aController getMyBrowserView] getBrowserView] setActive: YES]; }