1 // Copyright (c) 2011 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. 4 5 #include "chrome/browser/ui/gtk/bookmarks/bookmark_editor_gtk.h" 6 7 #include <gtk/gtk.h> 8 9 #include "base/basictypes.h" 10 #include "base/logging.h" 11 #include "base/string_util.h" 12 #include "base/utf_string_conversions.h" 13 #include "chrome/browser/bookmarks/bookmark_model.h" 14 #include "chrome/browser/bookmarks/bookmark_utils.h" 15 #include "chrome/browser/history/history.h" 16 #include "chrome/browser/net/url_fixer_upper.h" 17 #include "chrome/browser/profiles/profile.h" 18 #include "chrome/browser/ui/gtk/bookmarks/bookmark_tree_model.h" 19 #include "chrome/browser/ui/gtk/bookmarks/bookmark_utils_gtk.h" 20 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 21 #include "chrome/browser/ui/gtk/gtk_util.h" 22 #include "googleurl/src/gurl.h" 23 #include "grit/chromium_strings.h" 24 #include "grit/generated_resources.h" 25 #include "grit/locale_settings.h" 26 #include "ui/base/l10n/l10n_util.h" 27 #include "ui/base/models/simple_menu_model.h" 28 #include "ui/gfx/gtk_util.h" 29 #include "ui/gfx/point.h" 30 31 #if defined(TOOLKIT_VIEWS) 32 #include "views/controls/menu/menu_2.h" 33 #else 34 #include "chrome/browser/ui/gtk/menu_gtk.h" 35 #endif 36 37 namespace { 38 39 // Background color of text field when URL is invalid. 40 const GdkColor kErrorColor = GDK_COLOR_RGB(0xFF, 0xBC, 0xBC); 41 42 // Preferred initial dimensions, in pixels, of the folder tree. 43 static const int kTreeWidth = 300; 44 static const int kTreeHeight = 150; 45 46 } // namespace 47 48 class BookmarkEditorGtk::ContextMenuController 49 : public ui::SimpleMenuModel::Delegate { 50 public: 51 explicit ContextMenuController(BookmarkEditorGtk* editor) 52 : editor_(editor), 53 running_menu_for_root_(false) { 54 menu_model_.reset(new ui::SimpleMenuModel(this)); 55 menu_model_->AddItemWithStringId(COMMAND_EDIT, IDS_EDIT); 56 menu_model_->AddItemWithStringId( 57 COMMAND_NEW_FOLDER, 58 IDS_BOOMARK_EDITOR_NEW_FOLDER_MENU_ITEM); 59 #if defined(TOOLKIT_VIEWS) 60 menu_.reset(new views::Menu2(menu_model_.get())); 61 #else 62 menu_.reset(new MenuGtk(NULL, menu_model_.get())); 63 #endif 64 } 65 virtual ~ContextMenuController() {} 66 67 void RunMenu(const gfx::Point& point, guint32 event_time) { 68 const BookmarkNode* selected_node = GetSelectedNode(); 69 if (selected_node) 70 running_menu_for_root_ = selected_node->parent()->IsRoot(); 71 #if defined(TOOLKIT_VIEWS) 72 menu_->RunContextMenuAt(point); 73 #else 74 menu_->PopupAsContext(point, event_time); 75 #endif 76 } 77 78 void Cancel() { 79 editor_ = NULL; 80 #if defined(TOOLKIT_VIEWS) 81 menu_->CancelMenu(); 82 #else 83 menu_->Cancel(); 84 #endif 85 } 86 87 private: 88 enum ContextMenuCommand { 89 COMMAND_EDIT, 90 COMMAND_NEW_FOLDER 91 }; 92 93 // Overridden from ui::SimpleMenuModel::Delegate: 94 virtual bool IsCommandIdEnabled(int command_id) const { 95 return !(command_id == COMMAND_EDIT && running_menu_for_root_) && 96 (editor_ != NULL); 97 } 98 99 virtual bool IsCommandIdChecked(int command_id) const { 100 return false; 101 } 102 103 virtual bool GetAcceleratorForCommandId(int command_id, 104 ui::Accelerator* accelerator) { 105 return false; 106 } 107 108 virtual void ExecuteCommand(int command_id) { 109 if (!editor_) 110 return; 111 112 switch (command_id) { 113 case COMMAND_EDIT: { 114 GtkTreeIter iter; 115 if (!gtk_tree_selection_get_selected(editor_->tree_selection_, 116 NULL, 117 &iter)) { 118 return; 119 } 120 121 GtkTreePath* path = gtk_tree_model_get_path( 122 GTK_TREE_MODEL(editor_->tree_store_), &iter); 123 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(editor_->tree_view_), path); 124 125 // Make the folder name editable. 126 gtk_tree_view_set_cursor(GTK_TREE_VIEW(editor_->tree_view_), path, 127 gtk_tree_view_get_column(GTK_TREE_VIEW(editor_->tree_view_), 0), 128 TRUE); 129 130 gtk_tree_path_free(path); 131 break; 132 } 133 case COMMAND_NEW_FOLDER: 134 editor_->NewFolder(); 135 break; 136 default: 137 NOTREACHED(); 138 break; 139 } 140 } 141 142 int64 GetRowIdAt(GtkTreeModel* model, GtkTreeIter* iter) { 143 GValue value = { 0, }; 144 gtk_tree_model_get_value(model, iter, bookmark_utils::ITEM_ID, &value); 145 int64 id = g_value_get_int64(&value); 146 g_value_unset(&value); 147 return id; 148 } 149 150 const BookmarkNode* GetNodeAt(GtkTreeModel* model, GtkTreeIter* iter) { 151 int64 id = GetRowIdAt(model, iter); 152 return (id > 0) ? editor_->bb_model_->GetNodeByID(id) : NULL; 153 } 154 155 const BookmarkNode* GetSelectedNode() { 156 GtkTreeModel* model; 157 GtkTreeIter iter; 158 if (!gtk_tree_selection_get_selected(editor_->tree_selection_, 159 &model, 160 &iter)) { 161 return NULL; 162 } 163 164 return GetNodeAt(model, &iter); 165 } 166 167 // The model and view for the right click context menu. 168 scoped_ptr<ui::SimpleMenuModel> menu_model_; 169 #if defined(TOOLKIT_VIEWS) 170 scoped_ptr<views::Menu2> menu_; 171 #else 172 scoped_ptr<MenuGtk> menu_; 173 #endif 174 175 // The context menu was brought up for. Set to NULL when the menu is canceled. 176 BookmarkEditorGtk* editor_; 177 178 // If true, we're running the menu for the bookmark bar or other bookmarks 179 // nodes. 180 bool running_menu_for_root_; 181 182 DISALLOW_COPY_AND_ASSIGN(ContextMenuController); 183 }; 184 185 // static 186 void BookmarkEditor::Show(gfx::NativeWindow parent_hwnd, 187 Profile* profile, 188 const BookmarkNode* parent, 189 const EditDetails& details, 190 Configuration configuration) { 191 DCHECK(profile); 192 BookmarkEditorGtk* editor = 193 new BookmarkEditorGtk(parent_hwnd, profile, parent, details, 194 configuration); 195 editor->Show(); 196 } 197 198 BookmarkEditorGtk::BookmarkEditorGtk( 199 GtkWindow* window, 200 Profile* profile, 201 const BookmarkNode* parent, 202 const EditDetails& details, 203 BookmarkEditor::Configuration configuration) 204 : profile_(profile), 205 dialog_(NULL), 206 parent_(parent), 207 details_(details), 208 running_menu_for_root_(false), 209 show_tree_(configuration == SHOW_TREE) { 210 DCHECK(profile); 211 Init(window); 212 } 213 214 BookmarkEditorGtk::~BookmarkEditorGtk() { 215 // The tree model is deleted before the view. Reset the model otherwise the 216 // tree will reference a deleted model. 217 218 bb_model_->RemoveObserver(this); 219 } 220 221 void BookmarkEditorGtk::Init(GtkWindow* parent_window) { 222 bb_model_ = profile_->GetBookmarkModel(); 223 DCHECK(bb_model_); 224 bb_model_->AddObserver(this); 225 226 dialog_ = gtk_dialog_new_with_buttons( 227 l10n_util::GetStringUTF8(IDS_BOOMARK_EDITOR_TITLE).c_str(), 228 parent_window, 229 GTK_DIALOG_MODAL, 230 GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT, 231 GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, 232 NULL); 233 gtk_dialog_set_has_separator(GTK_DIALOG(dialog_), FALSE); 234 235 if (show_tree_) { 236 GtkWidget* action_area = GTK_DIALOG(dialog_)->action_area; 237 new_folder_button_ = gtk_button_new_with_label( 238 l10n_util::GetStringUTF8(IDS_BOOMARK_EDITOR_NEW_FOLDER_BUTTON).c_str()); 239 g_signal_connect(new_folder_button_, "clicked", 240 G_CALLBACK(OnNewFolderClickedThunk), this); 241 gtk_container_add(GTK_CONTAINER(action_area), new_folder_button_); 242 gtk_button_box_set_child_secondary(GTK_BUTTON_BOX(action_area), 243 new_folder_button_, TRUE); 244 } 245 246 gtk_dialog_set_default_response(GTK_DIALOG(dialog_), GTK_RESPONSE_ACCEPT); 247 248 // The GTK dialog content area layout (overview) 249 // 250 // +- GtkVBox |vbox| ----------------------------------------------+ 251 // |+- GtkTable |table| ------------------------------------------+| 252 // ||+- GtkLabel ------+ +- GtkEntry |name_entry_| --------------+|| 253 // ||| | | ||| 254 // ||+-----------------+ +---------------------------------------+|| 255 // ||+- GtkLabel ------+ +- GtkEntry |url_entry_| ---------------+|| * 256 // ||| | | ||| 257 // ||+-----------------+ +---------------------------------------+|| 258 // |+-------------------------------------------------------------+| 259 // |+- GtkScrollWindow |scroll_window| ---------------------------+| 260 // ||+- GtkTreeView |tree_view_| --------------------------------+|| 261 // |||+- GtkTreeViewColumn |name_column| -----------------------+||| 262 // |||| |||| 263 // |||| |||| 264 // |||| |||| 265 // |||| |||| 266 // |||+---------------------------------------------------------+||| 267 // ||+-----------------------------------------------------------+|| 268 // |+-------------------------------------------------------------+| 269 // +---------------------------------------------------------------+ 270 // 271 // * The url and corresponding label are not shown if creating a new folder. 272 GtkWidget* content_area = GTK_DIALOG(dialog_)->vbox; 273 gtk_box_set_spacing(GTK_BOX(content_area), gtk_util::kContentAreaSpacing); 274 275 GtkWidget* vbox = gtk_vbox_new(FALSE, 12); 276 277 name_entry_ = gtk_entry_new(); 278 std::string title; 279 if (details_.type == EditDetails::EXISTING_NODE) { 280 title = UTF16ToUTF8(details_.existing_node->GetTitle()); 281 } else if (details_.type == EditDetails::NEW_FOLDER) { 282 title = l10n_util::GetStringUTF8(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME); 283 } 284 gtk_entry_set_text(GTK_ENTRY(name_entry_), title.c_str()); 285 g_signal_connect(name_entry_, "changed", 286 G_CALLBACK(OnEntryChangedThunk), this); 287 gtk_entry_set_activates_default(GTK_ENTRY(name_entry_), TRUE); 288 289 GtkWidget* table; 290 if (details_.type != EditDetails::NEW_FOLDER) { 291 url_entry_ = gtk_entry_new(); 292 std::string url_spec; 293 if (details_.type == EditDetails::EXISTING_NODE) 294 url_spec = details_.existing_node->GetURL().spec(); 295 gtk_entry_set_text(GTK_ENTRY(url_entry_), url_spec.c_str()); 296 g_signal_connect(url_entry_, "changed", 297 G_CALLBACK(OnEntryChangedThunk), this); 298 gtk_entry_set_activates_default(GTK_ENTRY(url_entry_), TRUE); 299 table = gtk_util::CreateLabeledControlsGroup(NULL, 300 l10n_util::GetStringUTF8(IDS_BOOMARK_EDITOR_NAME_LABEL).c_str(), 301 name_entry_, 302 l10n_util::GetStringUTF8(IDS_BOOMARK_EDITOR_URL_LABEL).c_str(), 303 url_entry_, 304 NULL); 305 306 } else { 307 url_entry_ = NULL; 308 table = gtk_util::CreateLabeledControlsGroup(NULL, 309 l10n_util::GetStringUTF8(IDS_BOOMARK_EDITOR_NAME_LABEL).c_str(), 310 name_entry_, 311 NULL); 312 } 313 314 gtk_box_pack_start(GTK_BOX(vbox), table, FALSE, FALSE, 0); 315 316 if (show_tree_) { 317 GtkTreeIter selected_iter; 318 int64 selected_id = 0; 319 if (details_.type == EditDetails::EXISTING_NODE) 320 selected_id = details_.existing_node->parent()->id(); 321 else if (parent_) 322 selected_id = parent_->id(); 323 tree_store_ = bookmark_utils::MakeFolderTreeStore(); 324 bookmark_utils::AddToTreeStore(bb_model_, selected_id, tree_store_, 325 &selected_iter); 326 tree_view_ = bookmark_utils::MakeTreeViewForStore(tree_store_); 327 gtk_widget_set_size_request(tree_view_, kTreeWidth, kTreeHeight); 328 tree_selection_ = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view_)); 329 g_signal_connect(tree_view_, "button-press-event", 330 G_CALLBACK(OnTreeViewButtonPressEventThunk), this); 331 332 GtkTreePath* path = NULL; 333 if (selected_id) { 334 path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree_store_), 335 &selected_iter); 336 } else { 337 // We don't have a selected parent (Probably because we're making a new 338 // bookmark). Select the first item in the list. 339 path = gtk_tree_path_new_from_string("0"); 340 } 341 342 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(tree_view_), path); 343 gtk_tree_selection_select_path(tree_selection_, path); 344 gtk_tree_path_free(path); 345 346 GtkWidget* scroll_window = gtk_scrolled_window_new(NULL, NULL); 347 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll_window), 348 GTK_POLICY_NEVER, 349 GTK_POLICY_AUTOMATIC); 350 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scroll_window), 351 GTK_SHADOW_ETCHED_IN); 352 gtk_container_add(GTK_CONTAINER(scroll_window), tree_view_); 353 354 gtk_box_pack_start(GTK_BOX(vbox), scroll_window, TRUE, TRUE, 0); 355 356 g_signal_connect(gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view_)), 357 "changed", G_CALLBACK(OnSelectionChangedThunk), this); 358 } 359 360 gtk_box_pack_start(GTK_BOX(content_area), vbox, TRUE, TRUE, 0); 361 362 g_signal_connect(dialog_, "response", 363 G_CALLBACK(OnResponseThunk), this); 364 g_signal_connect(dialog_, "delete-event", 365 G_CALLBACK(OnWindowDeleteEventThunk), this); 366 g_signal_connect(dialog_, "destroy", 367 G_CALLBACK(OnWindowDestroyThunk), this); 368 } 369 370 void BookmarkEditorGtk::Show() { 371 // Manually call our OnEntryChanged handler to set the initial state. 372 OnEntryChanged(NULL); 373 374 gtk_util::ShowDialog(dialog_); 375 } 376 377 void BookmarkEditorGtk::Close() { 378 // Under the model that we've inherited from Windows, dialogs can receive 379 // more than one Close() call inside the current message loop event. 380 if (dialog_) { 381 gtk_widget_destroy(dialog_); 382 dialog_ = NULL; 383 } 384 } 385 386 void BookmarkEditorGtk::BookmarkNodeMoved(BookmarkModel* model, 387 const BookmarkNode* old_parent, 388 int old_index, 389 const BookmarkNode* new_parent, 390 int new_index) { 391 Reset(); 392 } 393 394 void BookmarkEditorGtk::BookmarkNodeAdded(BookmarkModel* model, 395 const BookmarkNode* parent, 396 int index) { 397 Reset(); 398 } 399 400 void BookmarkEditorGtk::BookmarkNodeRemoved(BookmarkModel* model, 401 const BookmarkNode* parent, 402 int index, 403 const BookmarkNode* node) { 404 if ((details_.type == EditDetails::EXISTING_NODE && 405 details_.existing_node->HasAncestor(node)) || 406 (parent_ && parent_->HasAncestor(node))) { 407 // The node, or its parent was removed. Close the dialog. 408 Close(); 409 } else { 410 Reset(); 411 } 412 } 413 414 void BookmarkEditorGtk::BookmarkNodeChildrenReordered( 415 BookmarkModel* model, const BookmarkNode* node) { 416 Reset(); 417 } 418 419 void BookmarkEditorGtk::Reset() { 420 // TODO(erg): The windows implementation tries to be smart. For now, just 421 // close the window. 422 Close(); 423 } 424 425 GURL BookmarkEditorGtk::GetInputURL() const { 426 if (!url_entry_) 427 return GURL(); // Happens when we're editing a folder. 428 return URLFixerUpper::FixupURL(gtk_entry_get_text(GTK_ENTRY(url_entry_)), 429 std::string()); 430 } 431 432 string16 BookmarkEditorGtk::GetInputTitle() const { 433 return UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(name_entry_))); 434 } 435 436 void BookmarkEditorGtk::ApplyEdits() { 437 DCHECK(bb_model_->IsLoaded()); 438 439 GtkTreeIter currently_selected_iter; 440 if (show_tree_) { 441 if (!gtk_tree_selection_get_selected(tree_selection_, NULL, 442 ¤tly_selected_iter)) { 443 ApplyEdits(NULL); 444 return; 445 } 446 } 447 448 ApplyEdits(¤tly_selected_iter); 449 } 450 451 void BookmarkEditorGtk::ApplyEdits(GtkTreeIter* selected_parent) { 452 // We're going to apply edits to the bookmark bar model, which will call us 453 // back. Normally when a structural edit occurs we reset the tree model. 454 // We don't want to do that here, so we remove ourselves as an observer. 455 bb_model_->RemoveObserver(this); 456 457 GURL new_url(GetInputURL()); 458 string16 new_title(GetInputTitle()); 459 460 if (!show_tree_ || !selected_parent) { 461 bookmark_utils::ApplyEditsWithNoFolderChange( 462 bb_model_, parent_, details_, new_title, new_url); 463 return; 464 } 465 466 // Create the new folders and update the titles. 467 const BookmarkNode* new_parent = 468 bookmark_utils::CommitTreeStoreDifferencesBetween( 469 bb_model_, tree_store_, selected_parent); 470 471 if (!new_parent) { 472 // Bookmarks must be parented. 473 NOTREACHED(); 474 return; 475 } 476 477 bookmark_utils::ApplyEditsWithPossibleFolderChange( 478 bb_model_, new_parent, details_, new_title, new_url); 479 } 480 481 void BookmarkEditorGtk::AddNewFolder(GtkTreeIter* parent, GtkTreeIter* child) { 482 gtk_tree_store_append(tree_store_, child, parent); 483 gtk_tree_store_set( 484 tree_store_, child, 485 bookmark_utils::FOLDER_ICON, GtkThemeService::GetFolderIcon(true), 486 bookmark_utils::FOLDER_NAME, 487 l10n_util::GetStringUTF8(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME).c_str(), 488 bookmark_utils::ITEM_ID, static_cast<int64>(0), 489 bookmark_utils::IS_EDITABLE, TRUE, 490 -1); 491 } 492 493 void BookmarkEditorGtk::OnSelectionChanged(GtkWidget* selection) { 494 if (!gtk_tree_selection_get_selected(tree_selection_, NULL, NULL)) 495 gtk_widget_set_sensitive(new_folder_button_, FALSE); 496 else 497 gtk_widget_set_sensitive(new_folder_button_, TRUE); 498 } 499 500 void BookmarkEditorGtk::OnResponse(GtkWidget* dialog, int response_id) { 501 if (response_id == GTK_RESPONSE_ACCEPT) 502 ApplyEdits(); 503 504 Close(); 505 } 506 507 gboolean BookmarkEditorGtk::OnWindowDeleteEvent(GtkWidget* widget, 508 GdkEvent* event) { 509 Close(); 510 511 // Return true to prevent the gtk dialog from being destroyed. Close will 512 // destroy it for us and the default gtk_dialog_delete_event_handler() will 513 // force the destruction without us being able to stop it. 514 return TRUE; 515 } 516 517 void BookmarkEditorGtk::OnWindowDestroy(GtkWidget* widget) { 518 MessageLoop::current()->DeleteSoon(FROM_HERE, this); 519 } 520 521 void BookmarkEditorGtk::OnEntryChanged(GtkWidget* entry) { 522 gboolean can_close = TRUE; 523 if (details_.type == EditDetails::NEW_FOLDER) { 524 if (GetInputTitle().empty()) { 525 gtk_widget_modify_base(name_entry_, GTK_STATE_NORMAL, 526 &kErrorColor); 527 can_close = FALSE; 528 } else { 529 gtk_widget_modify_base(name_entry_, GTK_STATE_NORMAL, NULL); 530 } 531 } else { 532 GURL url(GetInputURL()); 533 if (!url.is_valid()) { 534 gtk_widget_modify_base(url_entry_, GTK_STATE_NORMAL, 535 &kErrorColor); 536 can_close = FALSE; 537 } else { 538 gtk_widget_modify_base(url_entry_, GTK_STATE_NORMAL, NULL); 539 } 540 } 541 gtk_dialog_set_response_sensitive(GTK_DIALOG(dialog_), 542 GTK_RESPONSE_ACCEPT, can_close); 543 } 544 545 void BookmarkEditorGtk::OnNewFolderClicked(GtkWidget* button) { 546 NewFolder(); 547 } 548 549 gboolean BookmarkEditorGtk::OnTreeViewButtonPressEvent(GtkWidget* widget, 550 GdkEventButton* event) { 551 if (event->button == 3) { 552 if (!menu_controller_.get()) 553 menu_controller_.reset(new ContextMenuController(this)); 554 menu_controller_->RunMenu(gfx::Point(event->x_root, event->y_root), 555 event->time); 556 } 557 558 return FALSE; 559 } 560 561 void BookmarkEditorGtk::NewFolder() { 562 GtkTreeIter iter; 563 if (!gtk_tree_selection_get_selected(tree_selection_, 564 NULL, 565 &iter)) { 566 NOTREACHED() << "Something should always be selected if New Folder " << 567 "is clicked"; 568 return; 569 } 570 571 GtkTreeIter new_item_iter; 572 AddNewFolder(&iter, &new_item_iter); 573 574 GtkTreePath* path = gtk_tree_model_get_path( 575 GTK_TREE_MODEL(tree_store_), &new_item_iter); 576 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(tree_view_), path); 577 578 // Make the folder name editable. 579 gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree_view_), path, 580 gtk_tree_view_get_column(GTK_TREE_VIEW(tree_view_), 0), 581 TRUE); 582 583 gtk_tree_path_free(path); 584 } 585