OLD | NEW |
| (Empty) |
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. | |
4 | |
5 #include "chrome/browser/ui/gtk/bookmarks/bookmark_bubble_gtk.h" | |
6 | |
7 #include <gtk/gtk.h> | |
8 | |
9 #include "base/basictypes.h" | |
10 #include "base/bind.h" | |
11 #include "base/i18n/rtl.h" | |
12 #include "base/logging.h" | |
13 #include "base/message_loop/message_loop.h" | |
14 #include "base/strings/string16.h" | |
15 #include "base/strings/utf_string_conversions.h" | |
16 #include "chrome/browser/bookmarks/bookmark_model.h" | |
17 #include "chrome/browser/bookmarks/bookmark_model_factory.h" | |
18 #include "chrome/browser/bookmarks/bookmark_utils.h" | |
19 #include "chrome/browser/chrome_notification_types.h" | |
20 #include "chrome/browser/profiles/profile.h" | |
21 #include "chrome/browser/signin/signin_promo.h" | |
22 #include "chrome/browser/themes/theme_properties.h" | |
23 #include "chrome/browser/ui/bookmarks/bookmark_editor.h" | |
24 #include "chrome/browser/ui/bookmarks/recently_used_folders_combo_model.h" | |
25 #include "chrome/browser/ui/browser.h" | |
26 #include "chrome/browser/ui/browser_finder.h" | |
27 #include "chrome/browser/ui/browser_list.h" | |
28 #include "chrome/browser/ui/chrome_pages.h" | |
29 #include "chrome/browser/ui/gtk/gtk_theme_service.h" | |
30 #include "chrome/browser/ui/gtk/gtk_util.h" | |
31 #include "chrome/browser/ui/sync/sync_promo_ui.h" | |
32 #include "content/public/browser/notification_source.h" | |
33 #include "content/public/browser/user_metrics.h" | |
34 #include "grit/generated_resources.h" | |
35 #include "ui/base/gtk/gtk_hig_constants.h" | |
36 #include "ui/base/l10n/l10n_util.h" | |
37 #include "ui/gfx/canvas_paint_gtk.h" | |
38 | |
39 using base::UserMetricsAction; | |
40 | |
41 namespace { | |
42 | |
43 enum { | |
44 COLUMN_NAME, | |
45 COLUMN_IS_SEPARATOR, | |
46 COLUMN_COUNT | |
47 }; | |
48 | |
49 // Thickness of the bubble's border. | |
50 const int kBubbleBorderThickness = 1; | |
51 | |
52 // Color of the bubble's border. | |
53 const SkColor kBubbleBorderColor = SkColorSetRGB(0x63, 0x63, 0x63); | |
54 | |
55 // Background color of the sync promo. | |
56 const GdkColor kPromoBackgroundColor = GDK_COLOR_RGB(0xf5, 0xf5, 0xf5); | |
57 | |
58 // Color of the border of the sync promo. | |
59 const SkColor kPromoBorderColor = SkColorSetRGB(0xe5, 0xe5, 0xe5); | |
60 | |
61 // Color of the text in the sync promo. | |
62 const GdkColor kPromoTextColor = GDK_COLOR_RGB(0x66, 0x66, 0x66); | |
63 | |
64 // Vertical padding inside the sync promo. | |
65 const int kPromoVerticalPadding = 15; | |
66 | |
67 // Pango markup for the "Sign in" link in the sync promo. | |
68 const char kPromoLinkMarkup[] = | |
69 "<a href='signin'><span underline='none'>%s</span></a>"; | |
70 | |
71 // Style to make the sync promo link blue. | |
72 const char kPromoLinkStyle[] = | |
73 "style \"sign-in-link\" {\n" | |
74 " GtkWidget::link-color=\"blue\"\n" | |
75 "}\n" | |
76 "widget \"*sign-in-link\" style \"sign-in-link\"\n"; | |
77 | |
78 gboolean IsSeparator(GtkTreeModel* model, GtkTreeIter* iter, gpointer data) { | |
79 gboolean is_separator; | |
80 gtk_tree_model_get(model, iter, COLUMN_IS_SEPARATOR, &is_separator, -1); | |
81 return is_separator; | |
82 } | |
83 | |
84 } // namespace | |
85 | |
86 BookmarkBubbleGtk* BookmarkBubbleGtk::bookmark_bubble_ = NULL; | |
87 | |
88 // static | |
89 void BookmarkBubbleGtk::Show(GtkWidget* anchor, | |
90 Profile* profile, | |
91 const GURL& url, | |
92 bool newly_bookmarked) { | |
93 // Sometimes Ctrl+D may get pressed more than once on top level window | |
94 // before the bookmark bubble window is shown and takes the keyboad focus. | |
95 if (bookmark_bubble_) | |
96 return; | |
97 bookmark_bubble_ = new BookmarkBubbleGtk(anchor, | |
98 profile, | |
99 url, | |
100 newly_bookmarked); | |
101 } | |
102 | |
103 void BookmarkBubbleGtk::BubbleClosing(BubbleGtk* bubble, | |
104 bool closed_by_escape) { | |
105 if (closed_by_escape) { | |
106 remove_bookmark_ = newly_bookmarked_; | |
107 apply_edits_ = false; | |
108 } | |
109 } | |
110 | |
111 void BookmarkBubbleGtk::Observe(int type, | |
112 const content::NotificationSource& source, | |
113 const content::NotificationDetails& details) { | |
114 DCHECK(type == chrome::NOTIFICATION_BROWSER_THEME_CHANGED); | |
115 | |
116 if (theme_service_->UsingNativeTheme()) { | |
117 for (std::vector<GtkWidget*>::iterator it = labels_.begin(); | |
118 it != labels_.end(); ++it) { | |
119 gtk_widget_modify_fg(*it, GTK_STATE_NORMAL, NULL); | |
120 } | |
121 } else { | |
122 for (std::vector<GtkWidget*>::iterator it = labels_.begin(); | |
123 it != labels_.end(); ++it) { | |
124 gtk_widget_modify_fg(*it, GTK_STATE_NORMAL, &ui::kGdkBlack); | |
125 } | |
126 } | |
127 | |
128 UpdatePromoColors(); | |
129 } | |
130 | |
131 BookmarkBubbleGtk::BookmarkBubbleGtk(GtkWidget* anchor, | |
132 Profile* profile, | |
133 const GURL& url, | |
134 bool newly_bookmarked) | |
135 : url_(url), | |
136 profile_(profile), | |
137 model_(BookmarkModelFactory::GetForProfile(profile)), | |
138 theme_service_(GtkThemeService::GetFrom(profile_)), | |
139 anchor_(anchor), | |
140 promo_(NULL), | |
141 promo_label_(NULL), | |
142 name_entry_(NULL), | |
143 folder_combo_(NULL), | |
144 bubble_(NULL), | |
145 newly_bookmarked_(newly_bookmarked), | |
146 apply_edits_(true), | |
147 remove_bookmark_(false), | |
148 factory_(this) { | |
149 GtkWidget* label = gtk_label_new(l10n_util::GetStringUTF8( | |
150 newly_bookmarked_ ? IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARKED : | |
151 IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARK).c_str()); | |
152 labels_.push_back(label); | |
153 remove_button_ = theme_service_->BuildChromeLinkButton( | |
154 l10n_util::GetStringUTF8(IDS_BOOKMARK_BUBBLE_REMOVE_BOOKMARK)); | |
155 GtkWidget* edit_button = gtk_button_new_with_label( | |
156 l10n_util::GetStringUTF8(IDS_BOOKMARK_BUBBLE_OPTIONS).c_str()); | |
157 GtkWidget* close_button = gtk_button_new_with_label( | |
158 l10n_util::GetStringUTF8(IDS_DONE).c_str()); | |
159 | |
160 GtkWidget* bubble_container = gtk_vbox_new(FALSE, 0); | |
161 | |
162 // Prevent the content of the bubble to be drawn on the border. | |
163 gtk_container_set_border_width(GTK_CONTAINER(bubble_container), | |
164 kBubbleBorderThickness); | |
165 | |
166 // Our content is arranged in 3 rows. |top| contains a left justified | |
167 // message, and a right justified remove link button. |table| is the middle | |
168 // portion with the name entry and the folder combo. |bottom| is the final | |
169 // row with a spacer, and the edit... and close buttons on the right. | |
170 GtkWidget* content = gtk_vbox_new(FALSE, 5); | |
171 gtk_container_set_border_width( | |
172 GTK_CONTAINER(content), | |
173 ui::kContentAreaBorder - kBubbleBorderThickness); | |
174 GtkWidget* top = gtk_hbox_new(FALSE, 0); | |
175 | |
176 gtk_misc_set_alignment(GTK_MISC(label), 0, 1); | |
177 gtk_box_pack_start(GTK_BOX(top), label, | |
178 TRUE, TRUE, 0); | |
179 gtk_box_pack_start(GTK_BOX(top), remove_button_, | |
180 FALSE, FALSE, 0); | |
181 | |
182 InitFolderComboModel(); | |
183 | |
184 // Create the edit entry for updating the bookmark name / title. | |
185 name_entry_ = gtk_entry_new(); | |
186 gtk_entry_set_text(GTK_ENTRY(name_entry_), GetTitle().c_str()); | |
187 | |
188 // We use a table to allow the labels to line up with each other, along | |
189 // with the entry and folder combo lining up. | |
190 GtkWidget* table = gtk_util::CreateLabeledControlsGroup( | |
191 &labels_, | |
192 l10n_util::GetStringUTF8(IDS_BOOKMARK_BUBBLE_TITLE_TEXT).c_str(), | |
193 name_entry_, | |
194 l10n_util::GetStringUTF8(IDS_BOOKMARK_BUBBLE_FOLDER_TEXT).c_str(), | |
195 folder_combo_, | |
196 NULL); | |
197 | |
198 GtkWidget* bottom = gtk_hbox_new(FALSE, 0); | |
199 // We want the buttons on the right, so just use an expanding label to fill | |
200 // all of the extra space on the right. | |
201 gtk_box_pack_start(GTK_BOX(bottom), gtk_label_new(""), | |
202 TRUE, TRUE, 0); | |
203 gtk_box_pack_start(GTK_BOX(bottom), edit_button, | |
204 FALSE, FALSE, 4); | |
205 gtk_box_pack_start(GTK_BOX(bottom), close_button, | |
206 FALSE, FALSE, 0); | |
207 | |
208 gtk_box_pack_start(GTK_BOX(content), top, TRUE, TRUE, 0); | |
209 gtk_box_pack_start(GTK_BOX(content), table, TRUE, TRUE, 0); | |
210 gtk_box_pack_start(GTK_BOX(content), bottom, TRUE, TRUE, 0); | |
211 // We want the focus to start on the entry, not on the remove button. | |
212 gtk_container_set_focus_child(GTK_CONTAINER(content), table); | |
213 | |
214 gtk_box_pack_start(GTK_BOX(bubble_container), content, TRUE, TRUE, 0); | |
215 | |
216 if (SyncPromoUI::ShouldShowSyncPromo(profile_)) { | |
217 std::string link_text = | |
218 l10n_util::GetStringUTF8(IDS_BOOKMARK_SYNC_PROMO_LINK); | |
219 char* link_markup = g_markup_printf_escaped(kPromoLinkMarkup, | |
220 link_text.c_str()); | |
221 base::string16 link_markup_utf16; | |
222 base::UTF8ToUTF16(link_markup, strlen(link_markup), &link_markup_utf16); | |
223 g_free(link_markup); | |
224 | |
225 std::string promo_markup = l10n_util::GetStringFUTF8( | |
226 IDS_BOOKMARK_SYNC_PROMO_MESSAGE, | |
227 link_markup_utf16); | |
228 | |
229 promo_ = gtk_event_box_new(); | |
230 gtk_widget_set_app_paintable(promo_, TRUE); | |
231 | |
232 promo_label_ = gtk_label_new(NULL); | |
233 gtk_label_set_markup(GTK_LABEL(promo_label_), promo_markup.c_str()); | |
234 gtk_misc_set_alignment(GTK_MISC(promo_label_), 0.0, 0.0); | |
235 gtk_misc_set_padding(GTK_MISC(promo_label_), | |
236 ui::kContentAreaBorder, | |
237 kPromoVerticalPadding); | |
238 | |
239 // Custom link color. | |
240 gtk_rc_parse_string(kPromoLinkStyle); | |
241 | |
242 UpdatePromoColors(); | |
243 | |
244 gtk_container_add(GTK_CONTAINER(promo_), promo_label_); | |
245 gtk_box_pack_start(GTK_BOX(bubble_container), promo_, TRUE, TRUE, 0); | |
246 g_signal_connect(promo_, | |
247 "realize", | |
248 G_CALLBACK(&OnSyncPromoRealizeThunk), | |
249 this); | |
250 g_signal_connect(promo_, | |
251 "expose-event", | |
252 G_CALLBACK(&OnSyncPromoExposeThunk), | |
253 this); | |
254 g_signal_connect(promo_label_, | |
255 "activate-link", | |
256 G_CALLBACK(&OnSignInClickedThunk), | |
257 this); | |
258 } | |
259 | |
260 bubble_ = BubbleGtk::Show(anchor_, | |
261 NULL, | |
262 bubble_container, | |
263 BubbleGtk::ANCHOR_TOP_RIGHT, | |
264 BubbleGtk::MATCH_SYSTEM_THEME | | |
265 BubbleGtk::POPUP_WINDOW | | |
266 BubbleGtk::GRAB_INPUT, | |
267 theme_service_, | |
268 this); // delegate | |
269 if (!bubble_) { | |
270 NOTREACHED(); | |
271 return; | |
272 } | |
273 | |
274 g_signal_connect(content, "destroy", | |
275 G_CALLBACK(&OnDestroyThunk), this); | |
276 g_signal_connect(name_entry_, "activate", | |
277 G_CALLBACK(&OnNameActivateThunk), this); | |
278 g_signal_connect(folder_combo_, "changed", | |
279 G_CALLBACK(&OnFolderChangedThunk), this); | |
280 g_signal_connect(edit_button, "clicked", | |
281 G_CALLBACK(&OnEditClickedThunk), this); | |
282 g_signal_connect(close_button, "clicked", | |
283 G_CALLBACK(&OnCloseClickedThunk), this); | |
284 g_signal_connect(remove_button_, "clicked", | |
285 G_CALLBACK(&OnRemoveClickedThunk), this); | |
286 | |
287 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED, | |
288 content::Source<ThemeService>(theme_service_)); | |
289 theme_service_->InitThemesFor(this); | |
290 } | |
291 | |
292 BookmarkBubbleGtk::~BookmarkBubbleGtk() { | |
293 DCHECK(bookmark_bubble_); | |
294 bookmark_bubble_ = NULL; | |
295 | |
296 if (apply_edits_) { | |
297 ApplyEdits(); | |
298 } else if (remove_bookmark_) { | |
299 const BookmarkNode* node = model_->GetMostRecentlyAddedNodeForURL(url_); | |
300 if (node) | |
301 model_->Remove(node->parent(), node->parent()->GetIndexOf(node)); | |
302 } | |
303 } | |
304 | |
305 void BookmarkBubbleGtk::OnDestroy(GtkWidget* widget) { | |
306 // We are self deleting, we have a destroy signal setup to catch when we | |
307 // destroyed (via the BubbleGtk being destroyed), and delete ourself. | |
308 delete this; | |
309 } | |
310 | |
311 void BookmarkBubbleGtk::OnNameActivate(GtkWidget* widget) { | |
312 bubble_->Close(); | |
313 } | |
314 | |
315 void BookmarkBubbleGtk::OnFolderChanged(GtkWidget* widget) { | |
316 int index = gtk_combo_box_get_active(GTK_COMBO_BOX(folder_combo_)); | |
317 if (index == folder_combo_model_->GetItemCount() - 1) { | |
318 content::RecordAction( | |
319 UserMetricsAction("BookmarkBubble_EditFromCombobox")); | |
320 // GTK doesn't handle having the combo box destroyed from the changed | |
321 // signal. Since showing the editor also closes the bubble, delay this | |
322 // so that GTK can unwind. Specifically gtk_menu_shell_button_release | |
323 // will run, and we need to keep the combo box alive until then. | |
324 base::MessageLoop::current()->PostTask( | |
325 FROM_HERE, | |
326 base::Bind(&BookmarkBubbleGtk::ShowEditor, factory_.GetWeakPtr())); | |
327 } | |
328 } | |
329 | |
330 void BookmarkBubbleGtk::OnEditClicked(GtkWidget* widget) { | |
331 content::RecordAction(UserMetricsAction("BookmarkBubble_Edit")); | |
332 ShowEditor(); | |
333 } | |
334 | |
335 void BookmarkBubbleGtk::OnCloseClicked(GtkWidget* widget) { | |
336 bubble_->Close(); | |
337 } | |
338 | |
339 void BookmarkBubbleGtk::OnRemoveClicked(GtkWidget* widget) { | |
340 content::RecordAction(UserMetricsAction("BookmarkBubble_Unstar")); | |
341 | |
342 apply_edits_ = false; | |
343 remove_bookmark_ = true; | |
344 bubble_->Close(); | |
345 } | |
346 | |
347 gboolean BookmarkBubbleGtk::OnSignInClicked(GtkWidget* widget, gchar* uri) { | |
348 GtkWindow* window = GTK_WINDOW(gtk_widget_get_toplevel(anchor_)); | |
349 Browser* browser = chrome::FindBrowserWithWindow(window); | |
350 chrome::ShowBrowserSignin(browser, signin::SOURCE_BOOKMARK_BUBBLE); | |
351 bubble_->Close(); | |
352 return TRUE; | |
353 } | |
354 | |
355 void BookmarkBubbleGtk::OnSyncPromoRealize(GtkWidget* widget) { | |
356 int width = gtk_util::GetWidgetSize(widget).width(); | |
357 gtk_util::SetLabelWidth(promo_label_, width); | |
358 } | |
359 | |
360 gboolean BookmarkBubbleGtk::OnSyncPromoExpose(GtkWidget* widget, | |
361 GdkEventExpose* event) { | |
362 GtkAllocation allocation; | |
363 gtk_widget_get_allocation(widget, &allocation); | |
364 | |
365 gfx::CanvasSkiaPaint canvas(event); | |
366 | |
367 // Draw a border on top of the promo. | |
368 canvas.DrawLine(gfx::Point(0, 0), | |
369 gfx::Point(allocation.width + 1, 0), | |
370 kPromoBorderColor); | |
371 | |
372 // Redraw the rounded corners of the bubble that are hidden by the | |
373 // background of the promo. | |
374 SkPaint points_paint; | |
375 points_paint.setColor(kBubbleBorderColor); | |
376 points_paint.setStrokeWidth(SkIntToScalar(1)); | |
377 canvas.DrawPoint(gfx::Point(0, allocation.height - 1), points_paint); | |
378 canvas.DrawPoint(gfx::Point(allocation.width - 1, allocation.height - 1), | |
379 points_paint); | |
380 | |
381 return FALSE; // Propagate expose to children. | |
382 } | |
383 | |
384 void BookmarkBubbleGtk::UpdatePromoColors() { | |
385 if (!promo_) | |
386 return; | |
387 | |
388 GdkColor promo_background_color; | |
389 | |
390 if (!theme_service_->UsingNativeTheme()) { | |
391 promo_background_color = kPromoBackgroundColor; | |
392 gtk_widget_set_name(promo_label_, "sign-in-link"); | |
393 gtk_util::SetLabelColor(promo_label_, &kPromoTextColor); | |
394 } else { | |
395 promo_background_color = theme_service_->GetGdkColor( | |
396 ThemeProperties::COLOR_TOOLBAR); | |
397 gtk_widget_set_name(promo_label_, "sign-in-link-theme-color"); | |
398 } | |
399 | |
400 gtk_widget_modify_bg(promo_, GTK_STATE_NORMAL, &promo_background_color); | |
401 | |
402 // No visible highlight color when the mouse is over the link. | |
403 gtk_widget_modify_base(promo_label_, | |
404 GTK_STATE_ACTIVE, | |
405 &promo_background_color); | |
406 gtk_widget_modify_base(promo_label_, | |
407 GTK_STATE_PRELIGHT, | |
408 &promo_background_color); | |
409 } | |
410 | |
411 void BookmarkBubbleGtk::ApplyEdits() { | |
412 // Set this to make sure we don't attempt to apply edits again. | |
413 apply_edits_ = false; | |
414 | |
415 const BookmarkNode* node = model_->GetMostRecentlyAddedNodeForURL(url_); | |
416 if (node) { | |
417 const base::string16 new_title( | |
418 base::UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(name_entry_)))); | |
419 | |
420 if (new_title != node->GetTitle()) { | |
421 model_->SetTitle(node, new_title); | |
422 content::RecordAction( | |
423 UserMetricsAction("BookmarkBubble_ChangeTitleInBubble")); | |
424 } | |
425 | |
426 folder_combo_model_->MaybeChangeParent( | |
427 node, gtk_combo_box_get_active(GTK_COMBO_BOX(folder_combo_))); | |
428 } | |
429 } | |
430 | |
431 std::string BookmarkBubbleGtk::GetTitle() { | |
432 const BookmarkNode* node = model_->GetMostRecentlyAddedNodeForURL(url_); | |
433 if (!node) { | |
434 NOTREACHED(); | |
435 return std::string(); | |
436 } | |
437 | |
438 return base::UTF16ToUTF8(node->GetTitle()); | |
439 } | |
440 | |
441 void BookmarkBubbleGtk::ShowEditor() { | |
442 const BookmarkNode* node = model_->GetMostRecentlyAddedNodeForURL(url_); | |
443 | |
444 // Commit any edits now. | |
445 ApplyEdits(); | |
446 | |
447 // Closing might delete us, so we'll cache what we need on the stack. | |
448 Profile* profile = profile_; | |
449 GtkWindow* toplevel = GTK_WINDOW(gtk_widget_get_toplevel(anchor_)); | |
450 | |
451 // Close the bubble, deleting the C++ objects, etc. | |
452 bubble_->Close(); | |
453 | |
454 if (node) { | |
455 BookmarkEditor::Show(toplevel, profile, | |
456 BookmarkEditor::EditDetails::EditNode(node), | |
457 BookmarkEditor::SHOW_TREE); | |
458 } | |
459 } | |
460 | |
461 void BookmarkBubbleGtk::InitFolderComboModel() { | |
462 const BookmarkNode* node = model_->GetMostRecentlyAddedNodeForURL(url_); | |
463 DCHECK(node); | |
464 | |
465 folder_combo_model_.reset(new RecentlyUsedFoldersComboModel(model_, node)); | |
466 | |
467 GtkListStore* store = gtk_list_store_new(COLUMN_COUNT, | |
468 G_TYPE_STRING, G_TYPE_BOOLEAN); | |
469 | |
470 // We always have nodes + 1 entries in the combo. The last entry is an entry | |
471 // that reads 'Choose Another Folder...' and when chosen Bookmark Editor is | |
472 // opened. | |
473 for (int i = 0; i < folder_combo_model_->GetItemCount(); ++i) { | |
474 const bool is_separator = folder_combo_model_->IsItemSeparatorAt(i); | |
475 const std::string name = is_separator ? | |
476 std::string() : base::UTF16ToUTF8(folder_combo_model_->GetItemAt(i)); | |
477 | |
478 GtkTreeIter iter; | |
479 gtk_list_store_append(store, &iter); | |
480 gtk_list_store_set(store, &iter, | |
481 COLUMN_NAME, name.c_str(), | |
482 COLUMN_IS_SEPARATOR, is_separator, | |
483 -1); | |
484 } | |
485 | |
486 folder_combo_ = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store)); | |
487 | |
488 gtk_combo_box_set_active(GTK_COMBO_BOX(folder_combo_), | |
489 folder_combo_model_->GetDefaultIndex()); | |
490 gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(folder_combo_), | |
491 IsSeparator, NULL, NULL); | |
492 g_object_unref(store); | |
493 | |
494 GtkCellRenderer* renderer = gtk_cell_renderer_text_new(); | |
495 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(folder_combo_), renderer, TRUE); | |
496 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(folder_combo_), renderer, | |
497 "text", COLUMN_NAME, | |
498 NULL); | |
499 } | |
OLD | NEW |