1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/gtk/bookmarks/bookmark_editor_gtk.h"
11 #include "base/basictypes.h"
12 #include "base/logging.h"
13 #include "base/prefs/pref_service.h"
14 #include "base/strings/string_util.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "chrome/browser/bookmarks/bookmark_expanded_state_tracker.h"
17 #include "chrome/browser/bookmarks/bookmark_model.h"
18 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
19 #include "chrome/browser/bookmarks/bookmark_utils.h"
20 #include "chrome/browser/history/history_service.h"
21 #include "chrome/browser/profiles/profile.h"
22 #include "chrome/browser/ui/bookmarks/bookmark_utils.h"
23 #include "chrome/browser/ui/gtk/bookmarks/bookmark_tree_model.h"
24 #include "chrome/browser/ui/gtk/bookmarks/bookmark_utils_gtk.h"
25 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
26 #include "chrome/browser/ui/gtk/gtk_util.h"
27 #include "chrome/browser/ui/gtk/menu_gtk.h"
28 #include "chrome/common/net/url_fixer_upper.h"
29 #include "components/user_prefs/user_prefs.h"
30 #include "grit/chromium_strings.h"
31 #include "grit/generated_resources.h"
32 #include "grit/locale_settings.h"
33 #include "ui/base/gtk/gtk_hig_constants.h"
34 #include "ui/base/l10n/l10n_util.h"
35 #include "ui/base/models/simple_menu_model.h"
36 #include "ui/gfx/gtk_util.h"
37 #include "ui/gfx/image/image.h"
38 #include "ui/gfx/point.h"
43 // Background color of text field when URL is invalid.
44 const GdkColor kErrorColor
= GDK_COLOR_RGB(0xFF, 0xBC, 0xBC);
46 // Preferred initial dimensions, in pixels, of the folder tree.
47 const int kTreeWidth
= 300;
48 const int kTreeHeight
= 150;
50 typedef std::set
<int64
> ExpandedNodeIDs
;
52 // Used by ExpandNodes.
53 struct ExpandNodesData
{
54 const ExpandedNodeIDs
* ids
;
58 // Expands all the nodes in |pointer_data| (which is a ExpandNodesData). This is
59 // intended for use by gtk_tree_model_foreach to expand a particular set of
61 gboolean
ExpandNodes(GtkTreeModel
* model
,
64 gpointer pointer_data
) {
65 ExpandNodesData
* data
= reinterpret_cast<ExpandNodesData
*>(pointer_data
);
66 int64 node_id
= GetIdFromTreeIter(model
, iter
);
67 if (data
->ids
->find(node_id
) != data
->ids
->end())
68 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(data
->tree_view
), path
);
69 return FALSE
; // Indicates we want to continue iterating.
72 // Used by SaveExpandedNodes.
73 struct SaveExpandedNodesData
{
74 // Filled in by SaveExpandedNodes.
75 BookmarkExpandedStateTracker::Nodes nodes
;
76 BookmarkModel
* bookmark_model
;
79 // Adds the node at |path| to |pointer_data| (which is a SaveExpandedNodesData).
80 // This is intended for use with gtk_tree_view_map_expanded_rows to save all
81 // the expanded paths.
82 void SaveExpandedNodes(GtkTreeView
* tree_view
,
84 gpointer pointer_data
) {
85 SaveExpandedNodesData
* data
=
86 reinterpret_cast<SaveExpandedNodesData
*>(pointer_data
);
88 gtk_tree_model_get_iter(gtk_tree_view_get_model(tree_view
), &iter
, path
);
89 const BookmarkNode
* node
= data
->bookmark_model
->GetNodeByID(
90 GetIdFromTreeIter(gtk_tree_view_get_model(tree_view
), &iter
));
92 data
->nodes
.insert(node
);
97 class BookmarkEditorGtk::ContextMenuController
98 : public ui::SimpleMenuModel::Delegate
{
100 explicit ContextMenuController(BookmarkEditorGtk
* editor
)
102 running_menu_for_root_(false) {
103 menu_model_
.reset(new ui::SimpleMenuModel(this));
104 menu_model_
->AddItemWithStringId(COMMAND_EDIT
, IDS_EDIT
);
105 menu_model_
->AddItemWithStringId(COMMAND_DELETE
, IDS_DELETE
);
106 menu_model_
->AddItemWithStringId(
108 IDS_BOOKMARK_EDITOR_NEW_FOLDER_MENU_ITEM
);
109 menu_
.reset(new MenuGtk(NULL
, menu_model_
.get()));
111 virtual ~ContextMenuController() {}
113 void RunMenu(const gfx::Point
& point
, guint32 event_time
) {
114 const BookmarkNode
* selected_node
= GetSelectedNode();
116 running_menu_for_root_
= selected_node
->parent()->is_root();
117 menu_
->PopupAsContext(point
, event_time
);
126 enum ContextMenuCommand
{
132 // Overridden from ui::SimpleMenuModel::Delegate:
133 virtual bool IsCommandIdEnabled(int command_id
) const OVERRIDE
{
137 switch (command_id
) {
140 return !running_menu_for_root_
;
141 case COMMAND_NEW_FOLDER
:
147 virtual bool IsCommandIdChecked(int command_id
) const OVERRIDE
{
151 virtual bool GetAcceleratorForCommandId(
153 ui::Accelerator
* accelerator
) OVERRIDE
{
157 virtual void ExecuteCommand(int command_id
, int event_flags
) OVERRIDE
{
161 switch (command_id
) {
162 case COMMAND_DELETE
: {
164 GtkTreeModel
* model
= NULL
;
165 if (!gtk_tree_selection_get_selected(editor_
->tree_selection_
,
170 const BookmarkNode
* selected_node
= GetNodeAt(model
, &iter
);
172 DCHECK(selected_node
->is_folder());
173 // Deleting an existing bookmark folder. Confirm if it has other
175 if (!selected_node
->empty()) {
176 if (!chrome::ConfirmDeleteBookmarkNode(selected_node
,
177 GTK_WINDOW(editor_
->dialog_
)))
180 editor_
->deletes_
.push_back(selected_node
->id());
182 gtk_tree_store_remove(editor_
->tree_store_
, &iter
);
187 if (!gtk_tree_selection_get_selected(editor_
->tree_selection_
,
193 GtkTreePath
* path
= gtk_tree_model_get_path(
194 GTK_TREE_MODEL(editor_
->tree_store_
), &iter
);
195 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(editor_
->tree_view_
), path
);
197 // Make the folder name editable.
198 gtk_tree_view_set_cursor(GTK_TREE_VIEW(editor_
->tree_view_
), path
,
199 gtk_tree_view_get_column(GTK_TREE_VIEW(editor_
->tree_view_
), 0),
202 gtk_tree_path_free(path
);
205 case COMMAND_NEW_FOLDER
:
206 editor_
->NewFolder();
214 const BookmarkNode
* GetNodeAt(GtkTreeModel
* model
, GtkTreeIter
* iter
) const {
215 int64 id
= GetIdFromTreeIter(model
, iter
);
216 return (id
> 0) ? editor_
->bb_model_
->GetNodeByID(id
) : NULL
;
219 const BookmarkNode
* GetSelectedNode() const {
222 if (!gtk_tree_selection_get_selected(editor_
->tree_selection_
,
228 return GetNodeAt(model
, &iter
);
231 // The model and view for the right click context menu.
232 scoped_ptr
<ui::SimpleMenuModel
> menu_model_
;
233 scoped_ptr
<MenuGtk
> menu_
;
235 // The context menu was brought up for. Set to NULL when the menu is canceled.
236 BookmarkEditorGtk
* editor_
;
238 // If true, we're running the menu for the bookmark bar or other bookmarks
240 bool running_menu_for_root_
;
242 DISALLOW_COPY_AND_ASSIGN(ContextMenuController
);
246 void BookmarkEditor::Show(gfx::NativeWindow parent_hwnd
,
248 const EditDetails
& details
,
249 Configuration configuration
) {
251 BookmarkEditorGtk
* editor
=
252 new BookmarkEditorGtk(parent_hwnd
,
260 BookmarkEditorGtk::BookmarkEditorGtk(
263 const BookmarkNode
* parent
,
264 const EditDetails
& details
,
265 BookmarkEditor::Configuration configuration
)
270 running_menu_for_root_(false),
271 show_tree_(configuration
== SHOW_TREE
) {
276 BookmarkEditorGtk::~BookmarkEditorGtk() {
277 // The tree model is deleted before the view. Reset the model otherwise the
278 // tree will reference a deleted model.
280 bb_model_
->RemoveObserver(this);
283 void BookmarkEditorGtk::Init(GtkWindow
* parent_window
) {
284 bb_model_
= BookmarkModelFactory::GetForProfile(profile_
);
286 bb_model_
->AddObserver(this);
288 dialog_
= gtk_dialog_new_with_buttons(
289 l10n_util::GetStringUTF8(details_
.GetWindowTitleId()).c_str(),
292 GTK_STOCK_CANCEL
, GTK_RESPONSE_REJECT
,
293 GTK_STOCK_SAVE
, GTK_RESPONSE_ACCEPT
,
295 #if !GTK_CHECK_VERSION(2, 22, 0)
296 gtk_dialog_set_has_separator(GTK_DIALOG(dialog_
), FALSE
);
300 GtkWidget
* action_area
= gtk_dialog_get_action_area(GTK_DIALOG(dialog_
));
301 new_folder_button_
= gtk_button_new_with_label(
302 l10n_util::GetStringUTF8(
303 IDS_BOOKMARK_EDITOR_NEW_FOLDER_BUTTON
).c_str());
304 g_signal_connect(new_folder_button_
, "clicked",
305 G_CALLBACK(OnNewFolderClickedThunk
), this);
306 gtk_container_add(GTK_CONTAINER(action_area
), new_folder_button_
);
307 gtk_button_box_set_child_secondary(GTK_BUTTON_BOX(action_area
),
308 new_folder_button_
, TRUE
);
311 gtk_dialog_set_default_response(GTK_DIALOG(dialog_
), GTK_RESPONSE_ACCEPT
);
313 // The GTK dialog content area layout (overview)
315 // +- GtkVBox |vbox| ----------------------------------------------+
316 // |+- GtkTable |table| ------------------------------------------+|
317 // ||+- GtkLabel ------+ +- GtkEntry |name_entry_| --------------+||
319 // ||+-----------------+ +---------------------------------------+||
320 // ||+- GtkLabel ------+ +- GtkEntry |url_entry_| ---------------+|| *
322 // ||+-----------------+ +---------------------------------------+||
323 // |+-------------------------------------------------------------+|
324 // |+- GtkScrollWindow |scroll_window| ---------------------------+|
325 // ||+- GtkTreeView |tree_view_| --------------------------------+||
326 // |||+- GtkTreeViewColumn |name_column| -----------------------+|||
331 // |||+---------------------------------------------------------+|||
332 // ||+-----------------------------------------------------------+||
333 // |+-------------------------------------------------------------+|
334 // +---------------------------------------------------------------+
336 // * The url and corresponding label are not shown if creating a new folder.
337 GtkWidget
* content_area
= gtk_dialog_get_content_area(GTK_DIALOG(dialog_
));
338 gtk_box_set_spacing(GTK_BOX(content_area
), ui::kContentAreaSpacing
);
340 GtkWidget
* vbox
= gtk_vbox_new(FALSE
, 12);
342 name_entry_
= gtk_entry_new();
345 if (details_
.type
== EditDetails::EXISTING_NODE
) {
346 title
= base::UTF16ToUTF8(details_
.existing_node
->GetTitle());
347 url
= details_
.existing_node
->url();
348 } else if (details_
.type
== EditDetails::NEW_FOLDER
) {
349 title
= l10n_util::GetStringUTF8(IDS_BOOKMARK_EDITOR_NEW_FOLDER_NAME
);
350 } else if (details_
.type
== EditDetails::NEW_URL
) {
352 title
= base::UTF16ToUTF8(details_
.title
);
354 gtk_entry_set_text(GTK_ENTRY(name_entry_
), title
.c_str());
355 g_signal_connect(name_entry_
, "changed",
356 G_CALLBACK(OnEntryChangedThunk
), this);
357 gtk_entry_set_activates_default(GTK_ENTRY(name_entry_
), TRUE
);
360 if (details_
.GetNodeType() != BookmarkNode::FOLDER
) {
361 url_entry_
= gtk_entry_new();
363 profile_
? user_prefs::UserPrefs::Get(profile_
) : NULL
;
365 GTK_ENTRY(url_entry_
),
367 chrome::FormatBookmarkURLForDisplay(url
, prefs
)).c_str());
368 g_signal_connect(url_entry_
, "changed",
369 G_CALLBACK(OnEntryChangedThunk
), this);
370 gtk_entry_set_activates_default(GTK_ENTRY(url_entry_
), TRUE
);
371 table
= gtk_util::CreateLabeledControlsGroup(NULL
,
372 l10n_util::GetStringUTF8(IDS_BOOKMARK_EDITOR_NAME_LABEL
).c_str(),
374 l10n_util::GetStringUTF8(IDS_BOOKMARK_EDITOR_URL_LABEL
).c_str(),
380 table
= gtk_util::CreateLabeledControlsGroup(NULL
,
381 l10n_util::GetStringUTF8(IDS_BOOKMARK_EDITOR_NAME_LABEL
).c_str(),
386 gtk_box_pack_start(GTK_BOX(vbox
), table
, FALSE
, FALSE
, 0);
389 GtkTreeIter selected_iter
;
390 int64 selected_id
= 0;
391 if (details_
.type
== EditDetails::EXISTING_NODE
)
392 selected_id
= details_
.existing_node
->parent()->id();
394 selected_id
= parent_
->id();
395 tree_store_
= MakeFolderTreeStore();
396 AddToTreeStore(bb_model_
, selected_id
, tree_store_
, &selected_iter
);
397 tree_view_
= MakeTreeViewForStore(tree_store_
);
398 gtk_widget_set_size_request(tree_view_
, kTreeWidth
, kTreeHeight
);
399 tree_selection_
= gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view_
));
400 g_signal_connect(tree_view_
, "button-press-event",
401 G_CALLBACK(OnTreeViewButtonPressEventThunk
), this);
403 BookmarkExpandedStateTracker::Nodes expanded_nodes
=
404 bb_model_
->expanded_state_tracker()->GetExpandedNodes();
405 if (!expanded_nodes
.empty()) {
407 for (BookmarkExpandedStateTracker::Nodes::iterator i
=
408 expanded_nodes
.begin(); i
!= expanded_nodes
.end(); ++i
) {
409 ids
.insert((*i
)->id());
411 ExpandNodesData data
= { &ids
, tree_view_
};
412 gtk_tree_model_foreach(GTK_TREE_MODEL(tree_store_
), &ExpandNodes
,
413 reinterpret_cast<gpointer
>(&data
));
416 GtkTreePath
* path
= NULL
;
418 path
= gtk_tree_model_get_path(GTK_TREE_MODEL(tree_store_
),
421 // We don't have a selected parent (Probably because we're making a new
422 // bookmark). Select the first item in the list.
423 path
= gtk_tree_path_new_from_string("0");
426 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(tree_view_
), path
);
427 gtk_tree_selection_select_path(tree_selection_
, path
);
428 gtk_tree_path_free(path
);
430 GtkWidget
* scroll_window
= gtk_scrolled_window_new(NULL
, NULL
);
431 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll_window
),
433 GTK_POLICY_AUTOMATIC
);
434 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scroll_window
),
435 GTK_SHADOW_ETCHED_IN
);
436 gtk_container_add(GTK_CONTAINER(scroll_window
), tree_view_
);
438 gtk_box_pack_start(GTK_BOX(vbox
), scroll_window
, TRUE
, TRUE
, 0);
440 g_signal_connect(gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view_
)),
441 "changed", G_CALLBACK(OnSelectionChangedThunk
), this);
444 gtk_box_pack_start(GTK_BOX(content_area
), vbox
, TRUE
, TRUE
, 0);
446 g_signal_connect(dialog_
, "response",
447 G_CALLBACK(OnResponseThunk
), this);
448 g_signal_connect(dialog_
, "delete-event",
449 G_CALLBACK(OnWindowDeleteEventThunk
), this);
450 g_signal_connect(dialog_
, "destroy",
451 G_CALLBACK(OnWindowDestroyThunk
), this);
454 void BookmarkEditorGtk::Show() {
455 // Manually call our OnEntryChanged handler to set the initial state.
456 OnEntryChanged(NULL
);
458 gtk_util::ShowDialog(dialog_
);
461 void BookmarkEditorGtk::Close() {
462 // Under the model that we've inherited from Windows, dialogs can receive
463 // more than one Close() call inside the current message loop event.
465 gtk_widget_destroy(dialog_
);
470 void BookmarkEditorGtk::BookmarkNodeMoved(BookmarkModel
* model
,
471 const BookmarkNode
* old_parent
,
473 const BookmarkNode
* new_parent
,
478 void BookmarkEditorGtk::BookmarkNodeAdded(BookmarkModel
* model
,
479 const BookmarkNode
* parent
,
484 void BookmarkEditorGtk::BookmarkNodeRemoved(BookmarkModel
* model
,
485 const BookmarkNode
* parent
,
487 const BookmarkNode
* node
) {
488 if ((details_
.type
== EditDetails::EXISTING_NODE
&&
489 details_
.existing_node
->HasAncestor(node
)) ||
490 (parent_
&& parent_
->HasAncestor(node
))) {
491 // The node, or its parent was removed. Close the dialog.
498 void BookmarkEditorGtk::BookmarkAllNodesRemoved(BookmarkModel
* model
) {
502 void BookmarkEditorGtk::BookmarkNodeChildrenReordered(
503 BookmarkModel
* model
, const BookmarkNode
* node
) {
507 void BookmarkEditorGtk::Reset() {
508 // TODO(erg): The windows implementation tries to be smart. For now, just
513 GURL
BookmarkEditorGtk::GetInputURL() const {
515 return GURL(); // Happens when we're editing a folder.
516 return URLFixerUpper::FixupURL(gtk_entry_get_text(GTK_ENTRY(url_entry_
)),
520 base::string16
BookmarkEditorGtk::GetInputTitle() const {
521 return base::UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(name_entry_
)));
524 void BookmarkEditorGtk::ApplyEdits() {
525 DCHECK(bb_model_
->loaded());
527 GtkTreeIter currently_selected_iter
;
529 if (!gtk_tree_selection_get_selected(tree_selection_
, NULL
,
530 ¤tly_selected_iter
)) {
536 ApplyEdits(¤tly_selected_iter
);
539 void BookmarkEditorGtk::ApplyEdits(GtkTreeIter
* selected_parent
) {
540 // We're going to apply edits to the bookmark bar model, which will call us
541 // back. Normally when a structural edit occurs we reset the tree model.
542 // We don't want to do that here, so we remove ourselves as an observer.
543 bb_model_
->RemoveObserver(this);
545 GURL
new_url(GetInputURL());
546 base::string16
new_title(GetInputTitle());
548 if (!show_tree_
|| !selected_parent
) {
549 // TODO: this is wrong. Just because there is no selection doesn't mean new
550 // folders weren't added.
551 BookmarkEditor::ApplyEditsWithNoFolderChange(
552 bb_model_
, parent_
, details_
, new_title
, new_url
);
556 // Create the new folders and update the titles.
557 const BookmarkNode
* new_parent
= CommitTreeStoreDifferencesBetween(
558 bb_model_
, tree_store_
, selected_parent
);
560 SaveExpandedNodesData data
;
561 data
.bookmark_model
= bb_model_
;
562 gtk_tree_view_map_expanded_rows(GTK_TREE_VIEW(tree_view_
),
564 reinterpret_cast<gpointer
>(&data
));
565 bb_model_
->expanded_state_tracker()->SetExpandedNodes(data
.nodes
);
568 // Bookmarks must be parented.
573 BookmarkEditor::ApplyEditsWithPossibleFolderChange(
574 bb_model_
, new_parent
, details_
, new_title
, new_url
);
576 // Remove the folders that were removed. This has to be done after all the
577 // other changes have been committed.
578 bookmark_utils::DeleteBookmarkFolders(bb_model_
, deletes_
);
581 void BookmarkEditorGtk::AddNewFolder(GtkTreeIter
* parent
, GtkTreeIter
* child
) {
582 gtk_tree_store_append(tree_store_
, child
, parent
);
586 FOLDER_ICON
, GtkThemeService::GetFolderIcon(true).ToGdkPixbuf(),
588 l10n_util::GetStringUTF8(IDS_BOOKMARK_EDITOR_NEW_FOLDER_NAME
).c_str(),
589 ITEM_ID
, static_cast<int64
>(0),
594 void BookmarkEditorGtk::OnSelectionChanged(GtkWidget
* selection
) {
595 if (!gtk_tree_selection_get_selected(tree_selection_
, NULL
, NULL
))
596 gtk_widget_set_sensitive(new_folder_button_
, FALSE
);
598 gtk_widget_set_sensitive(new_folder_button_
, TRUE
);
601 void BookmarkEditorGtk::OnResponse(GtkWidget
* dialog
, int response_id
) {
602 if (response_id
== GTK_RESPONSE_ACCEPT
)
608 gboolean
BookmarkEditorGtk::OnWindowDeleteEvent(GtkWidget
* widget
,
612 // Return true to prevent the gtk dialog from being destroyed. Close will
613 // destroy it for us and the default gtk_dialog_delete_event_handler() will
614 // force the destruction without us being able to stop it.
618 void BookmarkEditorGtk::OnWindowDestroy(GtkWidget
* widget
) {
619 base::MessageLoop::current()->DeleteSoon(FROM_HERE
, this);
622 void BookmarkEditorGtk::OnEntryChanged(GtkWidget
* entry
) {
623 gboolean can_close
= TRUE
;
624 if (details_
.GetNodeType() != BookmarkNode::FOLDER
) {
625 if (GetInputURL().is_valid()) {
626 gtk_widget_modify_base(url_entry_
, GTK_STATE_NORMAL
, NULL
);
628 gtk_widget_modify_base(url_entry_
, GTK_STATE_NORMAL
, &kErrorColor
);
632 gtk_dialog_set_response_sensitive(GTK_DIALOG(dialog_
), GTK_RESPONSE_ACCEPT
,
636 void BookmarkEditorGtk::OnNewFolderClicked(GtkWidget
* button
) {
640 gboolean
BookmarkEditorGtk::OnTreeViewButtonPressEvent(GtkWidget
* widget
,
641 GdkEventButton
* event
) {
642 if (event
->button
== 3) {
643 if (!menu_controller_
.get())
644 menu_controller_
.reset(new ContextMenuController(this));
645 menu_controller_
->RunMenu(gfx::Point(event
->x_root
, event
->y_root
),
652 void BookmarkEditorGtk::NewFolder() {
654 if (!gtk_tree_selection_get_selected(tree_selection_
,
657 NOTREACHED() << "Something should always be selected if New Folder " <<
662 GtkTreeIter new_item_iter
;
663 AddNewFolder(&iter
, &new_item_iter
);
665 GtkTreePath
* path
= gtk_tree_model_get_path(
666 GTK_TREE_MODEL(tree_store_
), &new_item_iter
);
667 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(tree_view_
), path
);
669 // Make the folder name editable.
670 gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree_view_
), path
,
671 gtk_tree_view_get_column(GTK_TREE_VIEW(tree_view_
), 0),
674 gtk_tree_path_free(path
);