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
37namespace {
38
39// Background color of text field when URL is invalid.
40const GdkColor kErrorColor = GDK_COLOR_RGB(0xFF, 0xBC, 0xBC);
41
42// Preferred initial dimensions, in pixels, of the folder tree.
43static const int kTreeWidth = 300;
44static const int kTreeHeight = 150;
45
46}  // namespace
47
48class 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
186void 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
198BookmarkEditorGtk::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
214BookmarkEditorGtk::~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
221void 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
370void BookmarkEditorGtk::Show() {
371  // Manually call our OnEntryChanged handler to set the initial state.
372  OnEntryChanged(NULL);
373
374  gtk_util::ShowDialog(dialog_);
375}
376
377void 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
386void 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
394void BookmarkEditorGtk::BookmarkNodeAdded(BookmarkModel* model,
395                                          const BookmarkNode* parent,
396                                          int index) {
397  Reset();
398}
399
400void 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
414void BookmarkEditorGtk::BookmarkNodeChildrenReordered(
415    BookmarkModel* model, const BookmarkNode* node) {
416  Reset();
417}
418
419void BookmarkEditorGtk::Reset() {
420  // TODO(erg): The windows implementation tries to be smart. For now, just
421  // close the window.
422  Close();
423}
424
425GURL 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
432string16 BookmarkEditorGtk::GetInputTitle() const {
433  return UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(name_entry_)));
434}
435
436void 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                                         &currently_selected_iter)) {
443      ApplyEdits(NULL);
444      return;
445    }
446  }
447
448  ApplyEdits(&currently_selected_iter);
449}
450
451void 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
481void 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
493void 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
500void BookmarkEditorGtk::OnResponse(GtkWidget* dialog, int response_id) {
501  if (response_id == GTK_RESPONSE_ACCEPT)
502    ApplyEdits();
503
504  Close();
505}
506
507gboolean 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
517void BookmarkEditorGtk::OnWindowDestroy(GtkWidget* widget) {
518  MessageLoop::current()->DeleteSoon(FROM_HERE, this);
519}
520
521void 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
545void BookmarkEditorGtk::OnNewFolderClicked(GtkWidget* button) {
546  NewFolder();
547}
548
549gboolean 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
561void 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