Bug 1354533 - Update the History panelview when it's shown inside the new Library panel. r?Gijs,mak
This patch changes the history-panelmenu widget with the following:
- Move the Recently Closed Tabs and Recently Closed Windows lists into their own
respective (nested) subview,
- Add a Recent History header to list of history items,
- Extend the list of Recent History items to be max 42 items long,
- Share more code with Bookmarks panel,
- Generalizes panelview event dispatching to always support customizable widgets.
MozReview-Commit-ID: 4sBR6llIvxG
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -485,16 +485,19 @@ toolbar:not(#TabsToolbar) > #personal-bo
#reload-button[disabled]:not(:-moz-window-inactive) > .toolbarbutton-icon {
opacity: 1 !important;
}
#PanelUI-feeds > .feed-toolbarbutton:-moz-locale-dir(rtl) {
direction: rtl;
}
+#appMenu_historyMenu > .bookmark-item,
+#appMenu-library-recentlyClosedTabs > .panel-subview-body > .bookmark-item,
+#appMenu-library-recentlyClosedWindows > .panel-subview-body > .bookmark-item,
#panelMenu_bookmarksMenu > .bookmark-item {
max-width: none;
}
#main-window:-moz-lwtheme {
background-repeat: no-repeat;
background-position: top right;
}
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -170,22 +170,42 @@ function clearSubview(aSubview) {
const CustomizableWidgets = [
{
id: "history-panelmenu",
type: "view",
viewId: "PanelUI-history",
shortcutId: "key_gotoHistory",
tooltiptext: "history-panelmenu.tooltiptext2",
defaultArea: CustomizableUI.AREA_PANEL,
+ recentlyClosedTabsPanel: "appMenu-library-recentlyClosedTabs",
+ recentlyClosedWindowsPanel: "appMenu-library-recentlyClosedWindows",
+ handleEvent(event) {
+ switch (event.type) {
+ case "PanelMultiViewHidden":
+ this.onPanelMultiViewHidden(event);
+ break;
+ case "ViewShowing":
+ this.onSubViewShowing(event);
+ break;
+ default:
+ throw new Error(`Unsupported event for '${this.id}'`);
+ }
+ },
onViewShowing(aEvent) {
// Populate our list of history
const kMaxResults = 15;
let doc = aEvent.target.ownerDocument;
let win = doc.defaultView;
+ if (AppConstants.MOZ_PHOTON_THEME && win.gPhotonStructure) {
+ // For the Photon panelview we're going to do something different!
+ this.onPhotonViewShowing(aEvent);
+ return;
+ }
+
let options = PlacesUtils.history.getNewQueryOptions();
options.excludeQueries = true;
options.queryType = options.QUERY_TYPE_HISTORY;
options.sortingMode = options.SORT_BY_DATE_DESCENDING;
options.maxResults = kMaxResults;
let query = PlacesUtils.history.getNewQuery();
let items = doc.getElementById("PanelUI-historyItems");
@@ -272,31 +292,105 @@ const CustomizableWidgets = [
separator.hidden = !elementCount;
while (--elementCount >= 0) {
let element = windowsFragment.children[elementCount];
CustomizableUI.addShortcut(element);
element.classList.add("subviewbutton", "cui-withicon");
}
recentlyClosedWindows.appendChild(windowsFragment);
},
+ onPhotonViewShowing(event) {
+ if (this._panelMenuView)
+ return;
+
+ let panelview = event.target;
+ let document = panelview.ownerDocument;
+ let window = document.defaultView;
+
+ // We restrict the amount of results to 42. Not 50, but 42. Why? Because 42.
+ let query = "place:queryType=" + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY +
+ "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING +
+ "&maxResults=42&excludeQueries=1";
+ this._panelMenuView = new window.PlacesPanelview(document.getElementById("appMenu_historyMenu"),
+ panelview, query);
+ // When either of these sub-subviews show, populate them with recently closed
+ // objects data.
+ document.getElementById(this.recentlyClosedTabsPanel).addEventListener("ViewShowing", this);
+ document.getElementById(this.recentlyClosedWindowsPanel).addEventListener("ViewShowing", this);
+ // When the popup is hidden (thus the panelmultiview node as well), make
+ // sure to stop listening to PlacesDatabase updates.
+ panelview.panelMultiView.addEventListener("PanelMultiViewHidden", this);
+ },
onCreated(aNode) {
+ // Skip this for the Photon panelview.
+ let doc = aNode.ownerDocument;
+ if (AppConstants.MOZ_PHOTON_THEME && doc.defaultView.gPhotonStructure)
+ return;
+
// Middle clicking recently closed items won't close the panel - cope:
let onRecentlyClosedClick = function(aEvent) {
if (aEvent.button == 1) {
CustomizableUI.hidePanelForNode(this);
}
};
- let doc = aNode.ownerDocument;
let recentlyClosedTabs = doc.getElementById("PanelUI-recentlyClosedTabs");
let recentlyClosedWindows = doc.getElementById("PanelUI-recentlyClosedWindows");
recentlyClosedTabs.addEventListener("click", onRecentlyClosedClick);
recentlyClosedWindows.addEventListener("click", onRecentlyClosedClick);
},
onViewHiding(aEvent) {
log.debug("History view is being hidden!");
+ },
+ onPanelMultiViewHidden(event) {
+ let panelMultiView = event.target;
+ let document = panelMultiView.ownerDocument;
+ if (this._panelMenuView) {
+ this._panelMenuView.uninit();
+ delete this._panelMenuView;
+ document.getElementById(this.recentlyClosedTabsPanel).removeEventListener("ViewShowing", this);
+ document.getElementById(this.recentlyClosedWindowsPanel).removeEventListener("ViewShowing", this);
+ }
+ panelMultiView.removeEventListener("PanelMultiViewHidden", this);
+ },
+ onSubViewShowing(event) {
+ let panelview = event.target;
+ let document = event.target.ownerDocument;
+ let window = document.defaultView;
+ let viewType = panelview.id == this.recentlyClosedTabsPanel ? "Tabs" : "Windows";
+
+ while (panelview.firstChild) {
+ panelview.firstChild.remove();
+ }
+ panelview._emptyMenuitem = panelview._startMarker = panelview._endMarker = null;
+
+ let utils = RecentlyClosedTabsAndWindowsMenuUtils;
+ let method = `get${viewType}Fragment`;
+ let fragment = utils[method](window, "toolbarbutton");
+ let elementCount = fragment.childElementCount;
+ this._panelMenuView._setEmptyPopupStatus(panelview, !elementCount);
+ if (!elementCount)
+ return;
+
+ let body = document.createElement("vbox");
+ body.className = "panel-subview-body";
+ body.appendChild(fragment);
+ let footer;
+ while (--elementCount >= 0) {
+ let element = body.childNodes[elementCount];
+ CustomizableUI.addShortcut(element);
+ element.classList.add("subviewbutton");
+ if (element.classList.contains("restoreallitem")) {
+ footer = element;
+ element.classList.add("panel-subview-footer");
+ } else {
+ element.classList.add("subviewbutton-iconic", "bookmark-item");
+ }
+ }
+ panelview.appendChild(body);
+ panelview.appendChild(footer);
}
}, {
id: "sync-button",
label: "remotetabs-panelmenu.label",
tooltiptext: "remotetabs-panelmenu.tooltiptext2",
type: "view",
viewId: "PanelUI-remotetabs",
defaultArea: CustomizableUI.AREA_PANEL,
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -332,17 +332,17 @@ this.PanelMultiView = class {
} else {
this._clickCapturer.removeEventListener("click", this);
}
this._panel.removeEventListener("mousemove", this);
this._panel.removeEventListener("popupshowing", this);
this._panel.removeEventListener("popupshown", this);
this._panel.removeEventListener("popuphidden", this);
this.window.removeEventListener("keydown", this);
- this.node.dispatchEvent(new this.window.CustomEvent("destructed"));
+ this._dispatchViewEvent(this.node, "destructed");
this.node = this._clickCapturer = this._viewContainer = this._mainViewContainer =
this._subViews = this._viewStack = this.__dwu = this._panelViewCache = null;
}
/**
* Remove any child subviews into the panelViewCache, to ensure
* they remain usable even if this panelmultiview instance is removed
* from the DOM.
@@ -405,18 +405,17 @@ this.PanelMultiView = class {
} else {
this._mainViewContainer.appendChild(aNewMainView);
}
}
showMainView() {
if (this.showingSubView) {
let viewNode = this._currentSubView;
- let evt = new this.window.CustomEvent("ViewHiding", { bubbles: true, cancelable: true });
- viewNode.dispatchEvent(evt);
+ this._dispatchViewEvent(viewNode, "ViewHiding");
if (this.panelViews) {
viewNode.removeAttribute("current");
this.showSubView(this._mainViewId);
this.node.setAttribute("viewtype", "main");
} else {
this._transitionHeight(() => {
viewNode.removeAttribute("current");
this._currentSubView = null;
@@ -468,47 +467,36 @@ this.PanelMultiView = class {
// of the main view, i.e. whilst the panel is shown and/ or visible.
if (!this._mainViewHeight) {
this._mainViewHeight = previousRect.height;
this._viewContainer.style.minHeight = this._mainViewHeight + "px";
}
}
}
+ this._viewShowing = viewNode;
+
+ // Make sure that new panels always have a title set.
+ if (this.panelViews && aAnchor) {
+ if (!viewNode.hasAttribute("title"))
+ viewNode.setAttribute("title", aAnchor.getAttribute("label"));
+ viewNode.classList.add("PanelUI-subView");
+ }
+ if (this.panelViews && this._mainViewWidth)
+ viewNode.style.maxWidth = viewNode.style.minWidth = this._mainViewWidth + "px";
+
// Emit the ViewShowing event so that the widget definition has a chance
// to lazily populate the subview with things.
let detail = {
blockers: new Set(),
- addBlocker(aPromise) {
- this.blockers.add(aPromise);
- },
+ addBlocker(promise) {
+ this.blockers.add(promise);
+ }
};
-
- // Make sure that new panels always have a title set.
- let cancel = false;
- if (this.panelViews && aAnchor) {
- if (aAnchor && !viewNode.hasAttribute("title"))
- viewNode.setAttribute("title", aAnchor.getAttribute("label"));
- viewNode.classList.add("PanelUI-subView");
- let custWidget = CustomizableWidgets.find(widget => widget.viewId == viewNode.id);
- if (custWidget) {
- if (custWidget.onInit)
- custWidget.onInit(aAnchor);
- custWidget.onViewShowing({ target: viewNode, preventDefault: () => cancel = true, detail });
- }
- }
- if (this.panelViews && this._mainViewWidth)
- viewNode.style.maxWidth = viewNode.style.minWidth = this._mainViewWidth + "px";
-
- this._viewShowing = viewNode;
- let evt = new window.CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
- viewNode.dispatchEvent(evt);
-
- if (!cancel)
- cancel = evt.defaultPrevented;
+ let cancel = this._dispatchViewEvent(viewNode, "ViewShowing", aAnchor, detail);
if (detail.blockers.size) {
try {
let results = await Promise.all(detail.blockers);
cancel = cancel || results.some(val => val === false);
} catch (e) {
Cu.reportError(e);
cancel = true;
}
@@ -538,18 +526,17 @@ this.PanelMultiView = class {
//
// All three of these actions make use of CSS transformations, so they
// should all occur simultaneously.
if (this.panelViews && playTransition) {
// Sliding the next subview in means that the previous panelview stays
// where it is and the active panelview slides in from the left in LTR
// mode, right in RTL mode.
let onTransitionEnd = () => {
- evt = new window.CustomEvent("ViewHiding", { bubbles: true, cancelable: true });
- previousViewNode.dispatchEvent(evt);
+ this._dispatchViewEvent(previousViewNode, "ViewHiding");
previousViewNode.removeAttribute("current");
this.descriptionHeightWorkaround(viewNode);
};
// There's absolutely no need to show off our epic animation skillz when
// the panel's not even open.
if (this._panel.state != "open") {
onTransitionEnd();
@@ -651,38 +638,69 @@ this.PanelMultiView = class {
if (!reverse)
viewNode.style.removeProperty("margin-inline-start");
if (aAnchor)
aAnchor.removeAttribute("open");
this._viewContainer.removeAttribute("transition-reverse");
- viewNode.dispatchEvent(new window.CustomEvent("ViewShown",
- { bubbles: true, cancelable: false }));
+ this._dispatchViewEvent(viewNode, "ViewShown");
}, { once: true });
});
}, { once: true });
});
} else if (!this.panelViews) {
this._transitionHeight(() => {
viewNode.setAttribute("current", true);
this.node.setAttribute("viewtype", "subview");
// Now that the subview is visible, we can check the height of the
// description elements it contains.
this.descriptionHeightWorkaround(viewNode);
- viewNode.dispatchEvent(new window.CustomEvent("ViewShown",
- { bubbles: true, cancelable: false }));
+ this._dispatchViewEvent(viewNode, "ViewShown");
});
this._shiftMainView(aAnchor);
}
})().catch(e => Cu.reportError(e));
}
/**
+ * Helper method to emit an event on a panelview, whilst also making sure that
+ * the correct method is called on CustomizableWidget instances.
+ *
+ * @param {panelview} viewNode Target of the event to dispatch.
+ * @param {String} eventName Name of the event to dispatch.
+ * @param {DOMNode} [anchor] Node where the panel is anchored to. Optional.
+ * @param {Object} [detail] Event detail object. Optional.
+ * @return {Boolean} `true` if the event was canceled by an event handler, `false`
+ * otherwise.
+ */
+ _dispatchViewEvent(viewNode, eventName, anchor, detail) {
+ let cancel = false;
+ if (this.panelViews) {
+ let custWidget = CustomizableWidgets.find(widget => widget.viewId == viewNode.id);
+ let method = "on" + eventName;
+ if (custWidget && custWidget[method]) {
+ if (anchor && custWidget.onInit)
+ custWidget.onInit(anchor);
+ custWidget[method]({ target: viewNode, preventDefault: () => cancel = true, detail });
+ }
+ }
+
+ let evt = new this.window.CustomEvent(eventName, {
+ bubbles: true,
+ cancelable: eventName == "ViewShowing"
+ });
+ viewNode.dispatchEvent(evt);
+ if (!cancel)
+ cancel = evt.defaultPrevented;
+ return cancel;
+ }
+
+ /**
* Calculate the correct bounds of a panelview node offscreen to minimize the
* amount of paint flashing and keep the stack vs panel layouts from interfering.
*
* @param {panelview} viewNode Node to measure the bounds of.
* @param {Rect} previousRect Rect representing the previous view
* (used to fill in any blanks).
* @param {Function} callback Called when we got the measurements in and pass
* them on as its first argument.
@@ -952,16 +970,17 @@ this.PanelMultiView = class {
this._mainViewHeight = 0;
}
// Always try to layout the panel normally when reopening it. This is
// also the layout that will be used in customize mode.
if (this._mainView.hasAttribute("blockinboxworkaround")) {
this._mainView.style.removeProperty("height");
this._mainView.removeAttribute("exceeding");
}
+ this._dispatchViewEvent(this.node, "PanelMultiViewHidden");
break;
}
}
/**
* Allow for navigating subview buttons using the arrow keys and the Enter key.
* The Up and Down keys can be used to navigate the list up and down and the
* Enter, Right or Left - depending on the text direction - key can be used to
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -79,42 +79,83 @@
</panelview>
<panelview id="PanelUI-history" flex="1">
<label value="&appMenuHistory.label;" class="panel-subview-header"/>
<vbox class="panel-subview-body">
<toolbarbutton id="appMenuViewHistorySidebar"
label="&appMenuHistory.viewSidebar.label;"
type="checkbox"
+#ifndef MOZ_PHOTON_THEME
class="subviewbutton"
+#else
+ class="subviewbutton subviewbutton-iconic"
+#endif
key="key_gotoHistory"
oncommand="SidebarUI.toggle('viewHistorySidebar'); PanelUI.hide();">
<observes element="viewHistorySidebar" attribute="checked"/>
</toolbarbutton>
<toolbarbutton id="appMenuClearRecentHistory"
label="&appMenuHistory.clearRecent.label;"
+#ifndef MOZ_PHOTON_THEME
class="subviewbutton"
+#else
+ class="subviewbutton subviewbutton-iconic"
+#endif
command="Tools:Sanitize"/>
<toolbarbutton id="appMenuRestoreLastSession"
label="&appMenuHistory.restoreSession.label;"
+#ifndef MOZ_PHOTON_THEME
class="subviewbutton"
+#else
+ class="subviewbutton subviewbutton-iconic"
+#endif
command="Browser:RestoreLastSession"/>
+#ifndef MOZ_PHOTON_THEME
<menuseparator id="PanelUI-recentlyClosedTabs-separator"/>
<vbox id="PanelUI-recentlyClosedTabs" tooltip="bhTooltip"/>
<menuseparator id="PanelUI-recentlyClosedWindows-separator"/>
<vbox id="PanelUI-recentlyClosedWindows" tooltip="bhTooltip"/>
<menuseparator id="PanelUI-historyItems-separator"/>
<vbox id="PanelUI-historyItems" tooltip="bhTooltip"/>
+#else
+ <toolbarseparator/>
+ <toolbarbutton id="appMenuRecentlyClosedTabs"
+ label="&historyUndoMenu.label;"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-library-recentlyClosedTabs', this)"/>
+ <toolbarbutton id="appMenuRecentlyClosedWindows"
+ label="&historyUndoWindowMenu.label;"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-library-recentlyClosedWindows', this)"/>
+ <toolbarseparator/>
+ <label value="&appMenuHistory.recentHistory.label;"
+ class="subview-subheader"/>
+ <toolbaritem id="appMenu_historyMenu"
+ orient="vertical"
+ smoothscroll="false"
+ flatList="true"
+ tooltip="bhTooltip">
+ <!-- history menu items will go here -->
+ </toolbaritem>
+#endif
</vbox>
<toolbarbutton id="PanelUI-historyMore"
class="panel-subview-footer subviewbutton"
label="&appMenuHistory.showAll.label;"
oncommand="PlacesCommandHook.showPlacesOrganizer('History'); CustomizableUI.hidePanelForNode(this);"/>
</panelview>
+#ifdef MOZ_PHOTON_THEME
+ <panelview id="appMenu-library-recentlyClosedTabs"/>
+ <panelview id="appMenu-library-recentlyClosedWindows"/>
+#endif
+
<panelview id="PanelUI-remotetabs" flex="1" class="PanelUI-subView"
descriptionheightworkaround="true">
<label value="&appMenuRemoteTabs.label;" class="panel-subview-header"/>
<vbox class="panel-subview-body">
<!-- this widget has 3 boxes in the body, but only 1 is ever visible -->
<!-- When Sync is ready to sync -->
<vbox id="PanelUI-remotetabs-main" observes="sync-syncnow-state">
<vbox id="PanelUI-remotetabs-buttons">
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -1972,17 +1972,17 @@ PlacesPanelMenuView.prototype = {
}
for (let i = 0; i < this._resultNode.childCount; ++i) {
this._insertNewItem(this._resultNode.getChild(i), null);
}
}
};
-class PlacesPanelview extends PlacesViewBase {
+var PlacesPanelview = class extends PlacesViewBase {
constructor(container, panelview, place, options = {}) {
options.rootElt = container;
options.viewElt = panelview;
super(place, options);
this._viewElt._placesView = this;
// We're simulating a popup show, because a panelview may only be shown when
// its containing popup is already shown.
this._onPopupShowing({ originalTarget: this._viewElt });
@@ -2110,19 +2110,22 @@ class PlacesPanelview extends PlacesView
panelview._emptyMenuitem.className = "subviewbutton";
if (typeof this.options.extraClasses.entry == "string")
panelview._emptyMenuitem.classList.add(this.options.extraClasses.entry);
}
if (empty) {
panelview.setAttribute("emptyplacesresult", "true");
// Don't add the menuitem if there is static content.
- if (!panelview._startMarker.previousSibling &&
- !panelview._endMarker.nextSibling)
+ // We also support external usage for custom crafted panels - which'll have
+ // no markers present.
+ if (!panelview._startMarker ||
+ (!panelview._startMarker.previousSibling && !panelview._endMarker.nextSibling)) {
panelview.insertBefore(panelview._emptyMenuitem, panelview._endMarker);
+ }
} else {
panelview.removeAttribute("emptyplacesresult");
try {
panelview.removeChild(panelview._emptyMenuitem);
} catch (ex) {}
}
}
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -347,16 +347,17 @@ These should match what Safari and other
<!ENTITY appMenuCustomize.tooltip "Customize the Menu and Toolbars">
<!ENTITY appMenuCustomizeExit.label "Exit Customize">
<!ENTITY appMenuCustomizeExit.tooltip "Finish Customizing">
<!ENTITY appMenuHistory.label "History">
<!ENTITY appMenuHistory.showAll.label "Show All History">
<!ENTITY appMenuHistory.clearRecent.label "Clear Recent History…">
<!ENTITY appMenuHistory.restoreSession.label "Restore Previous Session">
<!ENTITY appMenuHistory.viewSidebar.label "View History Sidebar">
+<!ENTITY appMenuHistory.recentHistory.label "Recent History">
<!ENTITY appMenuHelp.label "Help">
<!ENTITY appMenuHelp.tooltip "Open Help Menu">
<!ENTITY appMenuRemoteTabs.label "Synced Tabs">
<!-- LOCALIZATION NOTE (appMenuRemoteTabs.notabs.label): This is shown beneath
the name of a device when that device has no open tabs -->
<!ENTITY appMenuRemoteTabs.notabs.label "No open tabs">
<!-- LOCALIZATION NOTE (appMenuRemoteTabs.showMore.label, appMenuRemoteTabs.showMore.tooltip):
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -178,17 +178,17 @@ toolbarpaletteitem[place="palette"] > #z
}
%endif
#add-share-provider {
list-style-image: url(chrome://browser/skin/menuPanel-small.svg);
-moz-image-region: rect(0px, 96px, 16px, 80px);
}
-
+#appMenuRecentlyClosedWindows,
#appMenu-new-window-button {
list-style-image: url(chrome://browser/skin/new-window.svg);
}
#appMenu-private-window-button {
list-style-image: url(chrome://browser/skin/privateBrowsing.svg);
}
@@ -252,16 +252,17 @@ toolbarpaletteitem[place="palette"] > #z
#appMenu-fullscreen-button[checked] {
list-style-image: url(chrome://browser/skin/fullscreen-exit.svg);
}
#appMenu-library-history-button {
list-style-image: url(chrome://browser/skin/history.svg);
}
+#appMenuRecentlyClosedTabs,
#appMenu-library-remotetabs-button {
list-style-image: url("chrome://browser/skin/synced-tabs.svg");
}
#PanelUI-remotetabs-syncnow {
list-style-image: url("chrome://browser/skin/sync.svg");
}
@@ -284,9 +285,17 @@ toolbarpaletteitem[place="palette"] > #z
list-style-image: url("chrome://browser/skin/bookmark.svg");
}
%ifdef MOZ_PHOTON_THEME
#bookmarks-menu-button[cui-areatype="menu-panel"],
toolbarpaletteitem[place="palette"] > #bookmarks-menu-button {
list-style-image: url("chrome://browser/skin/bookmark-star-on-tray.svg");
}
+
+#appMenuClearRecentHistory {
+ list-style-image: url("chrome://browser/skin/forget.svg");
+}
+
+#appMenuRestoreLastSession {
+ list-style-image: url("chrome://browser/skin/reload.svg");
+}
%endif