diff --git a/app/browser/lib/menuUtil.js b/app/browser/lib/menuUtil.js index c8b6b55be2e..dc07b7515e2 100644 --- a/app/browser/lib/menuUtil.js +++ b/app/browser/lib/menuUtil.js @@ -10,31 +10,6 @@ const eventUtil = require('../../../js/lib/eventUtil') const siteUtil = require('../../../js/state/siteUtil') const locale = require('../../locale') -/** - * Get an electron MenuItem object for the PARENT menu (File, Edit, etc) based on its label - * @param {string} label - the text associated with the menu - * NOTE: label may be a localized string - */ -module.exports.getParentMenuDetails = (appMenu, label) => { - let menuIndex = -1 - let menuItem = null - - if (label && appMenu && appMenu.items && appMenu.items.length > 0) { - menuIndex = appMenu.items.findIndex(function (item, index) { - return item && item.label === label - }) - - if (menuIndex !== -1) { - menuItem = appMenu.items[menuIndex] - } - } - - return { - menu: menuItem, - index: menuIndex - } -} - /** * Get the an electron MenuItem object from a Menu based on its label * @param {string} label - the text associated with the menu diff --git a/app/browser/menu.js b/app/browser/menu.js index 8b50f402de8..f0e7a8934ad 100644 --- a/app/browser/menu.js +++ b/app/browser/menu.js @@ -18,6 +18,7 @@ const messages = require('../../js/constants/messages') const settings = require('../../js/constants/settings') const siteTags = require('../../js/constants/siteTags') const dialog = electron.dialog +const BrowserWindow = electron.BrowserWindow const { fileUrl } = require('../../js/lib/appUrlUtil') const menuUtil = require('./lib/menuUtil') const getSetting = require('../../js/settings').getSetting @@ -586,6 +587,8 @@ const createMenu = () => { }) } + appActions.setMenubarTemplate(Immutable.fromJS(template)) + let oldMenu = appMenu appMenu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(appMenu) @@ -643,6 +646,15 @@ const doAction = (action) => { }) } break + case windowConstants.WINDOW_CLICK_MENUBAR_SUBMENU: + appDispatcher.waitFor([appStore.dispatchToken], () => { + const clickedMenuItem = menuUtil.getMenuItem(appMenu, action.label) + if (clickedMenuItem) { + const focusedWindow = BrowserWindow.getFocusedWindow() + clickedMenuItem.click(clickedMenuItem, focusedWindow, focusedWindow.webContents) + } + }) + break default: } } diff --git a/app/common/lib/formatUtil.js b/app/common/lib/formatUtil.js new file mode 100644 index 00000000000..9d917ad4ee4 --- /dev/null +++ b/app/common/lib/formatUtil.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict' + +const macOrderLookup = (value) => { + switch (value) { + case 'Alt': + case 'Option': + case 'AltGr': + return 0 + case 'Shift': + return 1 + case 'Control': + case 'Ctrl': + return 2 + case 'Super': + case 'CmdOrCtrl': + case 'CommandOrControl': + case 'Command': + case 'Cmd': + return 3 + default: + return 4 + } +} +const defaultOrderLookup = (value) => { + switch (value) { + case 'CmdOrCtrl': + case 'CommandOrControl': + case 'Control': + case 'Ctrl': + return 0 + case 'Alt': + case 'AltGr': + return 1 + case 'Shift': + return 2 + default: + return 3 + } +} + +/** + * Format an electron accelerator in the order you'd expect in a menu + * Accelerator reference: https://github.com/electron/electron/blob/master/docs/api/accelerator.md + */ +module.exports.formatAccelerator = (accelerator) => { + let result = accelerator + let splitResult = accelerator.split('+') + // sort in proper order, based on OS + // also, replace w/ name or symbol + if (process.platform === 'darwin') { + splitResult.sort(function (left, right) { + if (macOrderLookup(left) === macOrderLookup(right)) return 0 + if (macOrderLookup(left) > macOrderLookup(right)) return 1 + return -1 + }) + // NOTE: these characters might only show properly on Mac + result = splitResult.join('') + result = result.replace('CommandOrControl', '⌘') + result = result.replace('CmdOrCtrl', '⌘') + result = result.replace('Command', '⌘') + result = result.replace('Cmd', '⌘') + result = result.replace('Alt', '⌥') + result = result.replace('AltGr', '⌥') + result = result.replace('Super', '⌘') + result = result.replace('Option', '⌥') + result = result.replace('Shift', '⇧') + result = result.replace('Control', '^') + result = result.replace('Ctrl', '^') + } else { + splitResult.sort(function (left, right) { + if (defaultOrderLookup(left) === defaultOrderLookup(right)) return 0 + if (defaultOrderLookup(left) > defaultOrderLookup(right)) return 1 + return -1 + }) + result = splitResult.join('+') + result = result.replace('CommandOrControl', 'Ctrl') + result = result.replace('CmdOrCtrl', 'Ctrl') + result = result.replace('Control', 'Ctrl') + } + return result +} + +/** + * Clamp values down to a given range (min/max). + * Value is wrapped when out of bounds. ex: + * min-1 = max + * max+1 = min + */ +module.exports.wrappingClamp = (value, min, max) => { + const range = (max - min) + 1 + return value - Math.floor((value - min) / range) * range +} diff --git a/app/extensions/brave/locales/en-US/app.properties b/app/extensions/brave/locales/en-US/app.properties index 03c1b48e0b9..bffc6ff33f5 100644 --- a/app/extensions/brave/locales/en-US/app.properties +++ b/app/extensions/brave/locales/en-US/app.properties @@ -202,3 +202,7 @@ dismissDenyRunInsecureContent=Stay Insecure denyRunInsecureContent=Stop Loading Unsafe Scripts runInsecureContentWarning=This page is trying to load scripts from insecure sources. If you allow this content to run it will not be encrypted and it may transmit unencrypted data to other sites. denyRunInsecureContentWarning=This page is currently loading scripts from insecure sources. +windowCaptionButtonMinimize=Minimize +windowCaptionButtonMaximize=Maximize +windowCaptionButtonRestore=Restore Down +windowCaptionButtonClose=Close diff --git a/app/locale.js b/app/locale.js index fd0b648d0fb..14006d0b5b8 100644 --- a/app/locale.js +++ b/app/locale.js @@ -207,7 +207,12 @@ var rendererIdentifiers = function () { 'downloadItemDelete', 'downloadItemClear', 'downloadToolbarHide', - 'downloadItemClearCompleted' + 'downloadItemClearCompleted', + // Caption buttons in titlebar (min/max/close - Windows only) + 'windowCaptionButtonMinimize', + 'windowCaptionButtonMaximize', + 'windowCaptionButtonRestore', + 'windowCaptionButtonClose' ] } diff --git a/app/renderer/components/menubar.js b/app/renderer/components/menubar.js new file mode 100644 index 00000000000..a0b62a21c03 --- /dev/null +++ b/app/renderer/components/menubar.js @@ -0,0 +1,213 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const React = require('react') +const Immutable = require('immutable') +const ImmutableComponent = require('../../../js/components/immutableComponent') +const windowActions = require('../../../js/actions/windowActions') +const separatorMenuItem = require('../../common/commonMenu').separatorMenuItem +const keyCodes = require('../../../js/constants/keyCodes') +const { wrappingClamp } = require('../../common/lib/formatUtil') + +const showContextMenu = (rect, submenu, lastFocusedSelector) => { + windowActions.setContextMenuDetail(Immutable.fromJS({ + left: rect.left, + top: rect.bottom, + template: submenu.map((submenuItem) => { + if (submenuItem.type === separatorMenuItem.type) { + return submenuItem + } + submenuItem.click = function (e) { + e.preventDefault() + if (lastFocusedSelector) { + // Send focus back to the active web frame + const results = document.querySelectorAll(lastFocusedSelector) + if (results.length === 1) { + results[0].focus() + } + } + windowActions.clickMenubarSubmenu(submenuItem.label) + } + return submenuItem + }) + })) +} + +class MenubarItem extends ImmutableComponent { + constructor () { + super() + this.onClick = this.onClick.bind(this) + this.onMouseOver = this.onMouseOver.bind(this) + } + onClick (e) { + if (e && e.stopPropagation) { + e.stopPropagation() + } + // If clicking on an already selected item, deselect it + const selected = this.props.menubar.props.selectedLabel + if (selected && selected === this.props.label) { + windowActions.setContextMenuDetail() + windowActions.setMenubarSelectedLabel() + return + } + // Otherwise, mark item as selected and show its context menu + windowActions.setMenubarSelectedLabel(this.props.label) + const rect = e.target.getBoundingClientRect() + showContextMenu(rect, this.props.submenu, this.props.lastFocusedSelector) + } + onMouseOver (e) { + const selected = this.props.menubar.props.selectedLabel + if (selected && selected !== this.props.label) { + this.onClick(e) + } + } + render () { + return + { this.props.label } + + } +} + +/** + * Menubar that can be optionally be displayed at the top of a window (in favor of the system menu). + * First intended use is with Windows to enable a slim titlebar. + * NOTE: the system menu is still created and used in order to keep the accelerators working. + */ +class Menubar extends ImmutableComponent { + constructor () { + super() + this.onKeyDown = this.onKeyDown.bind(this) + } + componentWillMount () { + document.addEventListener('keydown', this.onKeyDown) + } + componentWillUnmount () { + document.removeEventListener('keydown', this.onKeyDown) + } + getTemplateByLabel (label) { + const element = this.props.template.find((element) => { + return element.get('label') === label + }) + return element ? element.get('submenu') : null + } + get selectedTemplate () { + return this.getTemplateByLabel(this.props.selectedLabel) + } + get selectedTemplateItemsOnly () { + // exclude the separators AND items that are not visible + return this.selectedTemplate.filter((element) => { + if (element.get('type') === separatorMenuItem.type) return false + if (element.has('visible')) return element.get('visible') + return true + }) + } + get selectedIndexMax () { + const result = this.selectedTemplateItemsOnly + if (result && result.size && result.size > 0) { + return result.size + } + return 0 + } + getRectByLabel (label) { + const selected = document.querySelectorAll('.menubar .menubarItem[data-label=\'' + label + '\']') + if (selected.length === 1) { + return selected.item(0).getBoundingClientRect() + } + return null + } + get selectedRect () { + return this.getRectByLabel(this.props.selectedLabel) + } + onKeyDown (e) { + switch (e.which) { + case keyCodes.ENTER: + e.preventDefault() + if (this.selectedTemplate) { + const selectedLabel = this.selectedTemplateItemsOnly.getIn([this.props.selectedIndex, 'label']) + windowActions.clickMenubarSubmenu(selectedLabel) + windowActions.resetMenuState() + } + break + + case keyCodes.LEFT: + case keyCodes.RIGHT: + if (!this.props.autohide && !this.props.selectedLabel) break + + e.preventDefault() + if (this.props.template.size > 0) { + const selectedIndex = this.props.template.findIndex((element) => { + return element.get('label') === this.props.selectedLabel + }) + const nextIndex = selectedIndex === -1 + ? 0 + : wrappingClamp( + selectedIndex + (e.which === keyCodes.LEFT ? -1 : 1), + 0, + this.props.template.size - 1) + + // BSCTODO: consider submenus (ex: for bookmark folders) + + const nextLabel = this.props.template.getIn([nextIndex, 'label']) + const nextRect = this.getRectByLabel(nextLabel) + + windowActions.setMenubarSelectedLabel(nextLabel) + + // Context menu already being displayed; auto-open the next one + if (this.props.contextMenuDetail && this.selectedTemplate && nextRect) { + windowActions.setSubmenuSelectedIndex(0) + showContextMenu(nextRect, this.getTemplateByLabel(nextLabel).toJS(), this.props.lastFocusedSelector) + } + } + break + + case keyCodes.UP: + case keyCodes.DOWN: + if (!this.props.autohide && !this.props.selectedLabel) break + + e.preventDefault() + if (this.props.selectedLabel && this.selectedTemplate) { + if (!this.props.contextMenuDetail && this.selectedRect) { + // First time hitting up/down; popup the context menu + windowActions.setSubmenuSelectedIndex(0) + showContextMenu(this.selectedRect, this.selectedTemplate.toJS(), this.props.lastFocusedSelector) + } else { + // Context menu already visible; move selection up or down + const nextIndex = wrappingClamp( + this.props.selectedIndex + (e.which === keyCodes.UP ? -1 : 1), + 0, + this.selectedIndexMax - 1) + windowActions.setSubmenuSelectedIndex(nextIndex) + } + } + break + } + } + shouldComponentUpdate (nextProps, nextState) { + return this.props.selectedLabel !== nextProps.selectedLabel + } + render () { + return
+ { + this.props.template.map((menubarItem) => { + let props = { + label: menubarItem.get('label'), + submenu: menubarItem.get('submenu').toJS(), + menubar: this, + lastFocusedSelector: this.props.lastFocusedSelector + } + if (props.label === this.props.selectedLabel) { + props.selected = true + } + return + }) + } +
+ } +} + +module.exports = Menubar diff --git a/app/renderer/components/windowCaptionButtons.js b/app/renderer/components/windowCaptionButtons.js new file mode 100644 index 00000000000..4b91ddae83c --- /dev/null +++ b/app/renderer/components/windowCaptionButtons.js @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const os = require('os') +const React = require('react') +const ImmutableComponent = require('../../../js/components/immutableComponent') +const locale = require('../../../js/l10n') +const currentWindow = require('../currentWindow') + +class WindowCaptionButtons extends ImmutableComponent { + constructor () { + super() + this.onDoubleClick = this.onDoubleClick.bind(this) + this.onMinimizeClick = this.onMinimizeClick.bind(this) + this.onMaximizeClick = this.onMaximizeClick.bind(this) + this.onCloseClick = this.onCloseClick.bind(this) + this.osClass = this.getPlatformCssClass() + } + + get buttonClass () { + return (this.props.windowMaximized ? 'fullscreen' : '') + } + + getPlatformCssClass () { + switch (os.platform()) { + case 'win32': + if (/6.1./.test(os.release())) { + return 'win7' + } else { + return 'win10' + } + default: + return 'hidden' + } + } + + onMinimizeClick (e) { + currentWindow.minimize() + } + + onMaximizeClick (e) { + return (!currentWindow.isMaximized()) ? currentWindow.maximize() : currentWindow.unmaximize() + } + + onCloseClick (e) { + currentWindow.close() + } + + onDoubleClick (e) { + if (!e.target.className.includes('navigatorWrapper')) { + return + } + this.onMaximizeClick(e) + } + + render () { + return
+
+ + + +
+
+ } +} + +module.exports = WindowCaptionButtons diff --git a/app/sessionStore.js b/app/sessionStore.js index 0df60aa1a22..63bc076f908 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -412,6 +412,7 @@ module.exports.defaultAppState = () => { guid: [], timestamp: 0 } - } + }, + menubar: {} } } diff --git a/docs/appActions.md b/docs/appActions.md index 0cc3cbde426..0b8e110151a 100644 --- a/docs/appActions.md +++ b/docs/appActions.md @@ -403,6 +403,16 @@ Dispatches a message when appWindowId loses focus +### setMenubarTemplate(menubarTemplate) + +Saves current menubar template for use w/ Windows titlebar + +**Parameters** + +**menubarTemplate**: `Object`, JSON used to build the menu + + + ### networkConnected() Dispatches a message when the network is re-connected diff --git a/docs/state.md b/docs/state.md index b6cf143c2ee..9ddfe464ba0 100644 --- a/docs/state.md +++ b/docs/state.md @@ -219,6 +219,9 @@ AppStore newtab: { gridLayout: string // 'small', 'medium', 'large' } + }, + menubar: { + template: object // windows only: template object with Menubar control } } ``` @@ -367,6 +370,12 @@ WindowStore }, releaseNotes: { isVisible: boolean, // Whether or not to show release notes + }, + menubar: { // windows only + isVisible: boolean, // true if Menubar control is visible + selectedLabel: string, // label of menu that is selected (or null for none selected) + selectedIndex: number, // index of the selected context menu item + lastFocusedSelector: string // selector for the last selected element (browser ui, not frame content) } }, searchDetail: { diff --git a/docs/windowActions.md b/docs/windowActions.md index 8031d182346..4a6c6c7fcb1 100644 --- a/docs/windowActions.md +++ b/docs/windowActions.md @@ -788,6 +788,90 @@ blocked active mixed content on +### toggleMenubarVisible(isVisible) + +(Windows only) +Dispatches a message to indicate the custom rendered Menubar should be toggled (shown/hidden) + +**Parameters** + +**isVisible**: `boolean`, (optional) + + + +### clickMenubarSubmenu(label) + +(Windows only) +Used to trigger the click() action for a menu +Called from the Menubar control, handled in menu.js + +**Parameters** + +**label**: `string`, text of the label that was clicked + + + +### setMenubarSelectedLabel(label) + +(Windows only) +Used to track which menubar item is currently selected (or null for none selected) + +**Parameters** + +**label**: `string`, text of the menubar item label that was clicked (file, edit, etc) + + + +### resetMenuState() + +Used by `main.js` when click happens on content area (not on a link or react control). +- closes context menu +- closes popup menu +- nulls out menubar item selected (Windows only) +- hides menubar if auto-hide preference is set (Windows only) + + + +### setSubmenuSelectedIndex(index) + +(Windows only) +Used to track selected index of a context menu +Needed because arrow keys can be used to navigate the custom menu + +**Parameters** + +**index**: `number`, zero based index of the item. + Index excludes menu separators and hidden items. + + + +### setLastFocusedSelector(selector) + +(Windows only at the moment) +Used to track last selected element (typically the URL bar or the frame) +Important because focus is lost when using the custom menu and needs +to be returned in order for the cut/copy operation to work + +**Parameters** + +**selector**: `string`, selector used w/ querySelectorAll to return focus + after a menu item is selected (via the custom titlebar / menubar) + + + +### gotResponseDetails(tabId, details) + +Used to get response details (such as the HTTP response code) from a response +See `eventStore.js` for an example use-case + +**Parameters** + +**tabId**: `number`, the tab id to set + +**details**: `Object`, object containing response details + + + * * * diff --git a/img/windows/close.svg b/img/windows/close.svg new file mode 100644 index 00000000000..fcca53e1a5f --- /dev/null +++ b/img/windows/close.svg @@ -0,0 +1 @@ +close_icon \ No newline at end of file diff --git a/js/actions/appActions.js b/js/actions/appActions.js index b32c7e478c0..ce8fbb88ae7 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -466,6 +466,17 @@ const appActions = { }) }, + /** + * Saves current menubar template for use w/ Windows titlebar + * @param {Object} menubarTemplate - JSON used to build the menu + */ + setMenubarTemplate: function (menubarTemplate) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_SET_MENUBAR_TEMPLATE, + menubarTemplate + }) + }, + /** * Dispatches a message when the network is re-connected * after being disconnected diff --git a/js/actions/windowActions.js b/js/actions/windowActions.js index 820159d4675..60f62d7cf3b 100644 --- a/js/actions/windowActions.js +++ b/js/actions/windowActions.js @@ -1007,6 +1007,92 @@ const windowActions = { }) }, + /** + * (Windows only) + * Dispatches a message to indicate the custom rendered Menubar should be toggled (shown/hidden) + * @param {boolean} isVisible (optional) + */ + toggleMenubarVisible: function (isVisible, defaultLabel) { + dispatch({ + actionType: WindowConstants.WINDOW_TOGGLE_MENUBAR_VISIBLE, + isVisible, + defaultLabel + }) + }, + + /** + * (Windows only) + * Used to trigger the click() action for a menu + * Called from the Menubar control, handled in menu.js + * @param {string} label - text of the label that was clicked + */ + clickMenubarSubmenu: function (label) { + dispatch({ + actionType: WindowConstants.WINDOW_CLICK_MENUBAR_SUBMENU, + label + }) + }, + + /** + * (Windows only) + * Used to track which menubar item is currently selected (or null for none selected) + * @param {string} label - text of the menubar item label that was clicked (file, edit, etc) + */ + setMenubarSelectedLabel: function (label) { + dispatch({ + actionType: WindowConstants.WINDOW_SET_MENUBAR_SELECTED_LABEL, + label + }) + }, + + /** + * Used by `main.js` when click happens on content area (not on a link or react control). + * - closes context menu + * - closes popup menu + * - nulls out menubar item selected (Windows only) + * - hides menubar if auto-hide preference is set (Windows only) + */ + resetMenuState: function () { + dispatch({ + actionType: WindowConstants.WINDOW_RESET_MENU_STATE + }) + }, + + /** + * (Windows only) + * Used to track selected index of a context menu + * Needed because arrow keys can be used to navigate the custom menu + * @param {number} index - zero based index of the item. + * Index excludes menu separators and hidden items. + */ + setSubmenuSelectedIndex: function (index) { + dispatch({ + actionType: WindowConstants.WINDOW_SET_SUBMENU_SELECTED_INDEX, + index + }) + }, + + /** + * (Windows only at the moment) + * Used to track last selected element (typically the URL bar or the frame) + * Important because focus is lost when using the custom menu and needs + * to be returned in order for the cut/copy operation to work + * @param {string} selector - selector used w/ querySelectorAll to return focus + * after a menu item is selected (via the custom titlebar / menubar) + */ + setLastFocusedSelector: function (selector) { + dispatch({ + actionType: WindowConstants.WINDOW_SET_LAST_FOCUSED_SELECTOR, + selector + }) + }, + + /** + * Used to get response details (such as the HTTP response code) from a response + * See `eventStore.js` for an example use-case + * @param {number} tabId - the tab id to set + * @param {Object} details - object containing response details + */ gotResponseDetails: function (tabId, details) { dispatch({ actionType: WindowConstants.WINDOW_GOT_RESPONSE_DETAILS, diff --git a/js/components/contextMenu.js b/js/components/contextMenu.js index 636f123ba1a..95045cdfdd0 100644 --- a/js/components/contextMenu.js +++ b/js/components/contextMenu.js @@ -8,6 +8,8 @@ const ImmutableComponent = require('./immutableComponent') const windowActions = require('../actions/windowActions') const cx = require('../lib/classSet.js') const KeyCodes = require('../constants/keyCodes') +const { formatAccelerator } = require('../../app/common/lib/formatUtil') +const separatorMenuItem = require('../../app/common/commonMenu').separatorMenuItem class ContextMenuItem extends ImmutableComponent { componentDidMount () { @@ -27,11 +29,20 @@ class ContextMenuItem extends ImmutableComponent { get hasSubmenu () { return this.submenu && this.submenu.size > 0 } + get accelerator () { + const accelerator = this.props.contextMenuItem.get('accelerator') + return accelerator && typeof accelerator === 'string' + ? accelerator.trim() + : null + } + get hasAccelerator () { + return this.accelerator !== null + } onClick (clickAction, shouldHide, e) { e.stopPropagation() if (clickAction) { if (shouldHide) { - windowActions.setContextMenuDetail() + windowActions.resetMenuState() } clickAction(e) } @@ -143,7 +154,8 @@ class ContextMenuItem extends ImmutableComponent { contextMenuItem: true, hasFaIcon: faIcon, checkedMenuItem: this.props.contextMenuItem.get('checked'), - hasIcon: icon || faIcon + hasIcon: icon || faIcon, + selectedByKeyboard: this.props.selected })} role='listitem' draggable={this.props.contextMenuItem.get('draggable')} @@ -180,7 +192,12 @@ class ContextMenuItem extends ImmutableComponent { - : null + : this.hasAccelerator + ? + + {formatAccelerator(this.accelerator)} + + : null } } @@ -195,17 +212,33 @@ class ContextMenuSingle extends ImmutableComponent { if (this.props.y) { styles.top = this.props.y } + const visibleMenuItems = this.props.template.filter((element) => { + return element.has('visible') + ? element.get('visible') + : true + }) + + let index = 0 return
{ - this.props.template.map((contextMenuItem) => - ) + visibleMenuItems.map((contextMenuItem) => { + let props = { + contextMenuItem: contextMenuItem, + submenuIndex: this.props.submenuIndex, + lastZoomPercentage: this.props.lastZoomPercentage, + contextMenuDetail: this.props.contextMenuDetail, + selected: false + } + // don't count separators when finding selectedIndex + if (contextMenuItem.get('type') !== separatorMenuItem.type) { + props.selected = index === this.props.selectedIndex + index++ + } + return + }) }
} @@ -216,7 +249,7 @@ class ContextMenuSingle extends ImmutableComponent { */ class ContextMenu extends ImmutableComponent { onClick () { - windowActions.setContextMenuDetail() + windowActions.resetMenuState() } get openedSubmenuDetails () { return this.props.contextMenuDetail.get('openedSubmenuDetails') || new Immutable.List() @@ -252,14 +285,16 @@ class ContextMenu extends ImmutableComponent { + template={this.props.contextMenuDetail.get('template')} + selectedIndex={this.props.selectedIndex} /> { this.openedSubmenuDetails.map((openedSubmenuDetail, i) => ) + y={openedSubmenuDetail.get('y')} + selectedIndex={this.props.selectedIndex} />) } } diff --git a/js/components/main.js b/js/components/main.js index 78cf6e1f190..2ab67795d72 100644 --- a/js/components/main.js +++ b/js/components/main.js @@ -39,6 +39,8 @@ const ContextMenu = require('./contextMenu') const PopupWindow = require('./popupWindow') const NoScriptInfo = require('./noScriptInfo') const LongPressButton = require('./longPressButton') +const Menubar = require('../../app/renderer/components/menubar') +const WindowCaptionButtons = require('../../app/renderer/components/windowCaptionButtons') // Constants const config = require('../constants/config') @@ -48,6 +50,7 @@ const settings = require('../constants/settings') const siteTags = require('../constants/siteTags') const dragTypes = require('../constants/dragTypes') const keyCodes = require('../constants/keyCodes') +const isWindows = process.platform === 'win32' // State handling const basicAuthState = require('../../app/common/state/basicAuthState') @@ -120,6 +123,60 @@ class Main extends ImmutableComponent { }) } + registerCustomTitlebarHandlers () { + if (this.customTitlebar.enabled) { + document.addEventListener('keyup', (e) => { + const customTitlebar = this.customTitlebar + switch (e.which) { + case keyCodes.ALT: + e.preventDefault() + + const menubarTemplate = this.props.appState.getIn(['menu', 'template']) + const defaultLabel = menubarTemplate.getIn([0, 'label']) + + if (getSetting(settings.AUTO_HIDE_MENU)) { + windowActions.toggleMenubarVisible(null, defaultLabel) + } else { + if (customTitlebar.menubarSelectedLabel) { + windowActions.setMenubarSelectedLabel() + windowActions.setContextMenuDetail() + } else { + windowActions.setMenubarSelectedLabel(defaultLabel) + } + } + break + case keyCodes.ESC: + if (getSetting(settings.AUTO_HIDE_MENU) && customTitlebar.menubarVisible && !customTitlebar.menubarSelectedLabel) { + e.preventDefault() + windowActions.toggleMenubarVisible(false) + break + } + if (customTitlebar.menubarSelectedLabel) { + e.preventDefault() + windowActions.setMenubarSelectedLabel() + windowActions.setContextMenuDetail() + } + break + } + }) + + document.addEventListener('focus', (e) => { + let selector = document.activeElement.id + ? '#' + document.activeElement.id + : null + + if (!selector && document.activeElement.tagName === 'WEBVIEW') { + const frameKeyAttribute = document.activeElement.getAttribute('data-frame-key') + if (frameKeyAttribute) { + selector = 'webview[data-frame-key="' + frameKeyAttribute + '"]' + } + } + + windowActions.setLastFocusedSelector(selector) + }, true) + } + } + exitFullScreen () { const activeFrame = FrameStateUtil.getActiveFrame(this.props.windowState) if (activeFrame && activeFrame.get('isFullScreen')) { @@ -256,6 +313,7 @@ class Main extends ImmutableComponent { componentDidMount () { this.registerSwipeListener() this.registerWindowLevelShortcuts() + this.registerCustomTitlebarHandlers() ipc.on(messages.SHORTCUT_NEW_FRAME, (event, url, options = {}) => { if (options.singleFrame) { @@ -585,32 +643,30 @@ class Main extends ImmutableComponent { if (!e.target.className.includes('navigatorWrapper')) { return } - if (currentWindow.isMaximized()) { - currentWindow.maximize() - } else { - currentWindow.unmaximize() - } + return (!currentWindow.isMaximized()) ? currentWindow.maximize() : currentWindow.unmaximize() } onMouseDown (e) { + // BSCTODO: update this to use eventUtil.eventElHasAncestorWithClasses let node = e.target while (node) { if (node.classList && (node.classList.contains('popupWindow') || node.classList.contains('contextMenu') || - node.classList.contains('extensionButton'))) { + node.classList.contains('extensionButton') || + node.classList.contains('menubarItem'))) { // Middle click (on context menu) needs to fire the click event. // We need to prevent the default "Auto-Scrolling" behavior. if (node.classList.contains('contextMenu') && e.button === 1) { e.preventDefault() } + // Click event is handled downstream return } node = node.parentNode } - // TODO(bridiver) combine context menu and popup window - windowActions.setContextMenuDetail() - windowActions.setPopupWindowDetail() + // Hide context menus, popup menus, and menu selections + windowActions.resetMenuState() } onClickWindow (e) { @@ -714,6 +770,22 @@ class Main extends ImmutableComponent { return buttons } + get customTitlebar () { + const customTitlebarEnabled = isWindows + const captionButtonsVisible = customTitlebarEnabled && !this.props.windowState.getIn(['ui', 'isFullScreen']) + const menubarVisible = customTitlebarEnabled && (!getSetting(settings.AUTO_HIDE_MENU) || this.props.windowState.getIn(['ui', 'menubar', 'isVisible'])) + return { + enabled: customTitlebarEnabled, + captionButtonsVisible: captionButtonsVisible, + menubarVisible: menubarVisible, + menubarTemplate: menubarVisible ? this.props.appState.getIn(['menu', 'template']) : null, + menubarSelectedLabel: this.props.windowState.getIn(['ui', 'menubar', 'selectedLabel']), + menubarSelectedIndex: this.props.windowState.getIn(['ui', 'menubar', 'selectedIndex']), + lastFocusedSelector: this.props.windowState.getIn(['ui', 'menubar', 'lastFocusedSelector']), + isMaximized: this.props.windowState.getIn(['ui', 'isMaximized']) + } + } + render () { const comparatorByKeyAsc = (a, b) => a.get('key') > b.get('key') ? 1 : b.get('key') > a.get('key') ? -1 : 0 @@ -743,7 +815,7 @@ class Main extends ImmutableComponent { const releaseNotesIsVisible = this.props.windowState.getIn(['ui', 'releaseNotes', 'isVisible']) const braverySettings = siteSettings.activeSettings(activeSiteSettings, this.props.appState, appConfig) const loginRequiredDetail = activeFrame ? basicAuthState.getLoginRequiredDetail(this.props.appState, activeFrame.get('tabId')) : null - + const customTitlebar = this.customTitlebar const shouldAllowWindowDrag = !this.props.windowState.get('contextMenuDetail') && !this.props.windowState.get('bookmarkDetail') && !siteInfoIsVisible && @@ -757,7 +829,8 @@ class Main extends ImmutableComponent { return
{ this.mainWindow = node }} onMouseDown={this.onMouseDown} @@ -766,7 +839,8 @@ class Main extends ImmutableComponent { this.props.windowState.get('contextMenuDetail') ? + contextMenuDetail={this.props.windowState.get('contextMenuDetail')} + selectedIndex={customTitlebar.menubarSelectedIndex} /> : null } { @@ -776,128 +850,153 @@ class Main extends ImmutableComponent { : null }
-
-
- - +
+
+
+ { + customTitlebar.menubarVisible + ?
+ +
+ : null + } +
+
+ + +
+ { this.navBar = node }} + navbar={activeFrame && activeFrame.get('navbar')} + frames={this.props.windowState.get('frames')} + sites={this.props.appState.get('sites')} + activeFrameKey={activeFrame && activeFrame.get('key') || undefined} + location={activeFrame && activeFrame.get('location') || ''} + title={activeFrame && activeFrame.get('title') || ''} + scriptsBlocked={activeFrame && activeFrame.getIn(['noScript', 'blocked'])} + partitionNumber={activeFrame && activeFrame.get('partitionNumber') || 0} + history={activeFrame && activeFrame.get('history') || emptyList} + suggestionIndex={activeFrame && activeFrame.getIn(['navbar', 'urlbar', 'suggestions', 'selectedIndex']) || 0} + isSecure={activeFrame && activeFrame.getIn(['security', 'isSecure'])} + locationValueSuffix={activeFrame && activeFrame.getIn(['navbar', 'urlbar', 'suggestions', 'urlSuffix']) || ''} + startLoadTime={activeFrame && activeFrame.get('startLoadTime') || undefined} + endLoadTime={activeFrame && activeFrame.get('endLoadTime') || undefined} + loading={activeFrame && activeFrame.get('loading')} + mouseInTitlebar={this.props.windowState.getIn(['ui', 'mouseInTitlebar'])} + searchDetail={this.props.windowState.get('searchDetail')} + enableNoScript={this.enableNoScript(activeSiteSettings)} + settings={this.props.appState.get('settings')} + noScriptIsVisible={noScriptIsVisible} + /> +
+ { + this.extensionButtons + } +
+
+
- { this.navBar = node }} - navbar={activeFrame && activeFrame.get('navbar')} - frames={this.props.windowState.get('frames')} - sites={this.props.appState.get('sites')} - activeFrameKey={activeFrame && activeFrame.get('key') || undefined} - location={activeFrame && activeFrame.get('location') || ''} - title={activeFrame && activeFrame.get('title') || ''} - scriptsBlocked={activeFrame && activeFrame.getIn(['noScript', 'blocked'])} - partitionNumber={activeFrame && activeFrame.get('partitionNumber') || 0} - history={activeFrame && activeFrame.get('history') || emptyList} - suggestionIndex={activeFrame && activeFrame.getIn(['navbar', 'urlbar', 'suggestions', 'selectedIndex']) || 0} - isSecure={activeFrame && activeFrame.getIn(['security', 'isSecure'])} - locationValueSuffix={activeFrame && activeFrame.getIn(['navbar', 'urlbar', 'suggestions', 'urlSuffix']) || ''} - startLoadTime={activeFrame && activeFrame.get('startLoadTime') || undefined} - endLoadTime={activeFrame && activeFrame.get('endLoadTime') || undefined} - loading={activeFrame && activeFrame.get('loading')} - mouseInTitlebar={this.props.windowState.getIn(['ui', 'mouseInTitlebar'])} - searchDetail={this.props.windowState.get('searchDetail')} - enableNoScript={this.enableNoScript(activeSiteSettings)} - settings={this.props.appState.get('settings')} - noScriptIsVisible={noScriptIsVisible} - /> - { - siteInfoIsVisible - ? - : null - } - { - braveryPanelIsVisible - ? - : null - } - { - clearBrowsingDataPanelIsVisible - ? - : null - } - { - autofillAddressPanelIsVisible - ? - : null - } { - autofillCreditCardPanelIsVisible - ? - : null - } - { - loginRequiredDetail - ? + customTitlebar.captionButtonsVisible + ? : null } - { - this.props.windowState.get('bookmarkDetail') - ? +
+ { + siteInfoIsVisible + ? + : null + } + { + braveryPanelIsVisible + ? + : null + } + { + clearBrowsingDataPanelIsVisible + ? + : null + } + { + autofillAddressPanelIsVisible + ? + : null + } + { + autofillCreditCardPanelIsVisible + ? + : null + } + { + loginRequiredDetail + ? : null - } - { - noScriptIsVisible - ? - : null - } - { - releaseNotesIsVisible - ? + } + { + this.props.windowState.get('bookmarkDetail') + ? + : null + } + { + noScriptIsVisible + ? : null - } -
- { - this.extensionButtons - } -
-
+ } + { + releaseNotesIsVisible + ? + : null + } + { this.props.appState.get('notifications') && this.props.appState.get('notifications').size && activeFrame diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index 7fd79a236a4..6e3db31edc1 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -48,7 +48,8 @@ const AppConstants = { APP_NETWORK_DISCONNECTED: _, APP_CHANGE_NEW_TAB_DETAIL: _, APP_TAB_CREATED: _, - APP_TAB_DESTROYED: _ + APP_TAB_DESTROYED: _, + APP_SET_MENUBAR_TEMPLATE: _ } module.exports = mapValuesByKeys(AppConstants) diff --git a/js/constants/keyCodes.js b/js/constants/keyCodes.js index 0c79d3b2002..9c89654fb9d 100644 --- a/js/constants/keyCodes.js +++ b/js/constants/keyCodes.js @@ -10,6 +10,7 @@ const KeyCodes = { SHIFT: 16, BACKSPACE: 8, CTRL: 17, + ALT: 18, DELETE: 46, CMD1: 91, CMD2: 93, diff --git a/js/constants/windowConstants.js b/js/constants/windowConstants.js index 3d9ea49b148..b915f71c92a 100644 --- a/js/constants/windowConstants.js +++ b/js/constants/windowConstants.js @@ -69,6 +69,12 @@ const windowConstants = { WINDOW_SET_AUTOFILL_ADDRESS_DETAIL: _, WINDOW_SET_AUTOFILL_CREDIT_CARD_DETAIL: _, WINDOW_SET_BLOCKED_RUN_INSECURE_CONTENT: _, + WINDOW_TOGGLE_MENUBAR_VISIBLE: _, + WINDOW_CLICK_MENUBAR_SUBMENU: _, + WINDOW_SET_MENUBAR_SELECTED_LABEL: _, + WINDOW_RESET_MENU_STATE: _, + WINDOW_SET_SUBMENU_SELECTED_INDEX: _, + WINDOW_SET_LAST_FOCUSED_SELECTOR: _, WINDOW_GOT_RESPONSE_DETAILS: _ } diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 96495ea52ec..66589db6244 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -28,7 +28,6 @@ const EventEmitter = require('events').EventEmitter const Immutable = require('immutable') const diff = require('immutablediff') const debounce = require('../lib/debounce.js') -const isDarwin = process.platform === 'darwin' const locale = require('../../app/locale') const path = require('path') @@ -36,6 +35,8 @@ const path = require('path') const basicAuthState = require('../../app/common/state/basicAuthState') const extensionState = require('../../app/common/state/extensionState') const tabState = require('../../app/common/state/tabState') +const isDarwin = process.platform === 'darwin' +const isWindows = process.platform === 'win32' // Only used internally const CHANGE_EVENT = 'app-state-change' @@ -136,7 +137,8 @@ const createWindow = (browserOpts, defaults, frameOpts, windowState) => { titleBarStyle: 'hidden-inset', autoHideMenuBar: autoHideMenuBarSetting, title: appConfig.name, - webPreferences: defaults.webPreferences + webPreferences: defaults.webPreferences, + frame: !isWindows } if (process.platform === 'linux') { @@ -145,6 +147,8 @@ const createWindow = (browserOpts, defaults, frameOpts, windowState) => { let mainWindow = new BrowserWindow(Object.assign(windowProps, browserOpts)) + mainWindow.setMenuBarVisibility(true) + if (windowState.ui && windowState.ui.isMaximized) { mainWindow.maximize() } @@ -675,6 +679,9 @@ const handleAppAction = (action) => { case ExtensionConstants.EXTENSION_DISABLED: appState = extensionState.extensionDisabled(appState, action) break + case AppConstants.APP_SET_MENUBAR_TEMPLATE: + appState = appState.setIn(['menu', 'template'], action.menubarTemplate) + break default: } diff --git a/js/stores/windowStore.js b/js/stores/windowStore.js index dc2a002ee2f..049230f72c6 100644 --- a/js/stores/windowStore.js +++ b/js/stores/windowStore.js @@ -31,7 +31,9 @@ let windowState = Immutable.fromJS({ ui: { tabs: { }, - mouseInTitlebar: false + mouseInTitlebar: false, + menubar: { + } }, searchDetail: null }) @@ -777,6 +779,52 @@ const doAction = (action) => { windowState.deleteIn(blockedRunInsecureContentPath.concat(['security', 'blockedRunInsecureContent'])) } break + case WindowConstants.WINDOW_TOGGLE_MENUBAR_VISIBLE: + if (getSetting(settings.AUTO_HIDE_MENU)) { + // Close existing context menus + doAction({actionType: WindowConstants.WINDOW_SET_CONTEXT_MENU_DETAIL}) + // Use value if provided; if not, toggle to opposite. + const newVisibleStatus = typeof action.isVisible === 'boolean' + ? action.isVisible + : !windowState.getIn(['ui', 'menubar', 'isVisible']) + // Clear selection when menu is shown + if (newVisibleStatus) { + const actionProps = { actionType: WindowConstants.WINDOW_SET_MENUBAR_SELECTED_LABEL } + if (action.defaultLabel) { + actionProps.label = action.defaultLabel + } + doAction(actionProps) + } + windowState = windowState.setIn(['ui', 'menubar', 'isVisible'], newVisibleStatus) + } + break + case WindowConstants.WINDOW_SET_MENUBAR_SELECTED_LABEL: + windowState = windowState.setIn(['ui', 'menubar', 'selectedLabel'], + action.label && typeof action.label === 'string' + ? action.label + : null) + break + case WindowConstants.WINDOW_RESET_MENU_STATE: + doAction({actionType: WindowConstants.WINDOW_SET_POPUP_WINDOW_DETAIL}) + doAction({actionType: WindowConstants.WINDOW_SET_MENUBAR_SELECTED_LABEL}) + if (getSetting(settings.AUTO_HIDE_MENU)) { + doAction({actionType: WindowConstants.WINDOW_TOGGLE_MENUBAR_VISIBLE, isVisible: false}) + } else { + doAction({actionType: WindowConstants.WINDOW_SET_CONTEXT_MENU_DETAIL}) + } + doAction({actionType: WindowConstants.WINDOW_SET_SUBMENU_SELECTED_INDEX}) + break + case WindowConstants.WINDOW_SET_SUBMENU_SELECTED_INDEX: + const proposedIndex = Number(action.index) + windowState = windowState.setIn(['ui', 'menubar', 'selectedIndex'], + isNaN(proposedIndex) + ? null + : proposedIndex) + break + case WindowConstants.WINDOW_SET_LAST_FOCUSED_SELECTOR: + windowState = windowState.setIn(['ui', 'menubar', 'lastFocusedSelector'], action.selector) + break + default: } diff --git a/less/bookmarksToolbar.less b/less/bookmarksToolbar.less index 8eedb824dd2..b39e4cb75a2 100644 --- a/less/bookmarksToolbar.less +++ b/less/bookmarksToolbar.less @@ -13,6 +13,13 @@ --bookmarks-toolbar-padding: 10px; } +// (Windows) Bookmarks toolbar not right-clickable unless it has no-drag +.platform--win32 .bookmarksToolbar { + &.allowDragging { + -webkit-app-region: no-drag !important; + } +} + .bookmarksToolbar { background: @toolbarBackground; border-bottom: 1px solid #aaaaaa; diff --git a/less/contextMenu.less b/less/contextMenu.less index 1b2549d5e37..ddb252a80d4 100644 --- a/less/contextMenu.less +++ b/less/contextMenu.less @@ -10,7 +10,7 @@ color: black; cursor: default; display: flex; - font-size: 14px; + font-size: @contextMenuFontSize; &.contextMenuScrollable { overflow-y: scroll; } @@ -43,6 +43,11 @@ } } + .selectedByKeyboard { + background-color: lighten(@menuSelectionColor, 5%); + color: white; + } + .contextMenuIcon { font-size: 14px; margin-right: 8px; @@ -68,7 +73,6 @@ } } box-sizing: border-box; - max-width: 300px; display: flex; align-items: center; text-overflow: ellipsis; @@ -139,9 +143,26 @@ float: right; margin: auto 0 auto 5px; } + .accelerator { + .submenuIndicator; + font-size: @contextMenuFontSize; + margin-left: 10px; + } } .fa { font-size: @bookmarksFolderIconSize; } } + + +// Make context menu style match menubar (Windows only- for use w/ slim titlebar) +.platform--win32 .contextMenuItem { + font: menu; + font-size: 12px; +} + +.platform--win32 .accelerator { + font: menu; + font-size: 12px !important; +} diff --git a/less/navigationBar.less b/less/navigationBar.less index 954e66a4029..8620651885f 100644 --- a/less/navigationBar.less +++ b/less/navigationBar.less @@ -18,7 +18,7 @@ } } -// On MacOS we need to keep a padding left to avoid overlapping +// (macOS) We need to keep a padding left to avoid overlapping // with the window buttons to close/maximize/minimize the window. .platform--darwin .navigatorWrapper .backforward { margin-left: @navbarLeftMarginDarwin; @@ -29,6 +29,407 @@ - (@navbarBraveButtonWidth + 2 * @navbarButtonSpacing); } +// Windows specific fixes +.platform--win32 { + .navigatorWrapper { + margin-left: @navbarLeftMarginWindows; + } + + div#window.frameless { + border: 1px solid #000; + box-sizing: border-box; + } + + // Extension button not clickable unless it has no-drag + .extensionButton { + -webkit-app-region: no-drag !important; + } + + // changes to ensure window can be as small as 480px wide + // and still be useable and have the caption buttons intact + @media (max-width: @breakpointExtensionButtonPadding) { + .navigatorWrapper .topLevelEndButtons { + margin-left: 0; + } + } + @media (max-width: @breakpointSmallWin32) { + .loadTime { display: none; } + #urlInput { max-width: 75px; } + #titleBar { width: 100px; } + } + @media (max-width: @breakpointTinyWin32) { + #urlInput { max-width: 50px; } + } +} + +// Styles had to be reworked to properly position the new caption buttons for Windows +.navbarCaptionButtonContainer { + display: flex; + border-bottom: 1px solid #aaaaaa; +} +.navbarMenubarFlexContainer { + display: flex; + flex: 1; + box-sizing: border-box; + position: relative; + overflow: visible; + white-space: nowrap; +} +.navbarMenubarBlockContainer { + display: block; + width: 100%; +} + +// Window Caption Buttons (for use w/ slim titlebar) +.windowCaptionButtons { + display: inline-block; + -webkit-app-region: drag; + + .hidden { + display: none; + } + + .container { + -webkit-app-region: no-drag; + } + + button.captionButton { + outline: none; + vertical-align: top; + } + + &.fullscreen { + .win7 { + margin-top: 1px; + } + } + + .win7 { + margin-right: 6px; + + button.captionButton { + outline: 0; + height: 18px; + color: #b1acac; + border: 1px solid #838383; + border-top: 0; + display: inline-block; + background-color: #e0e0e0; + box-shadow: inset 1px 1px rgba(255, 255, 255, 0.9); + width: 25px; + + &.minimize { + width: 28px; + border-right: 0px; + &:hover { + background-color: #f5f5f5; + } + &:active { + background-color: #cacacb; + } + border-radius: 0 0 0 4px; + + .widget { + width: 10px; + height: 3px; + border: 1px solid #838383; + background: #fefefe; + display: inline-block; + border-radius: 1px; + } + } + + &.maximize { + border-right: 0px; + width: 27px; + &:hover { + background-color: #f5f5f5; + .widget { + .widget2 { + background-color: #f5f5f5; + } + } + } + &:active { + background-color: #cacacb; + .widget { + .widget2 { + background-color: #cacacb; + } + } + } + &.fullscreen { + &:hover { + background-color: #e5e5e5; + } + &:active { + background-color: #cacacb; + } + .widget { + .widget1 { + width: 8px; + top: 2px; + left: 10px; + } + .widget2 { + width: 8px; + height: 8px; + top: -5px; + left: 6px; + background: #fefefe; + border-radius: 1px; + } + .widget3 { + display: inline-block; + width: 2px; + height: 2px; + border: 1px solid #838383; + background: #fefefe; + position: relative; + top: -20px; + left: -2px; + } + } + } + + .widget { + .widget1 { + width: 10px; + height: 8px; + border: 1px solid #838383; + background: #fefefe; + position: relative; + top: 2px; + left: 7px; + border-radius: 1px; + } + .widget2 { + width: 4px; + height: 2px; + border: 1px solid #838383; + background-color: #e0e0e0; + position: relative; + top: -5px; + left: 10px; + border-radius: 0; + } + .widget3 { display: none; } + .widget4 { display: none; } + .widget5 { display: none; } + } + } + + &.close { + width: 48px; + border-radius: 0 0 4px 0; + &:hover { + background-color: #d9504e; + } + &:active { + background-color: #d7393d; + } + + .widget { + background: url('../img/windows/close.svg') no-repeat; + display: inline-block; + height: 12px; + width: 12px; + position: relative; + top: 3px; + + .widget1 { display:none; } + .widget2 { display:none; } + .widget3 { display:none; } + } + } + } + } + + .win10 { + button.captionButton { + width: 45px; + height: 29px; + border: 0; + background-color: transparent; + + &.fullscreen { + height:21px; + } + + &.minimize { + &:hover { + background-color: #e5e5e5; + } + &:active { + background-color: #cacacb; + } + .widget { + width: 10px; + height: 1px; + background: black; + display: inline-block; + vertical-align: middle; + } + } + + &.maximize { + border-right: 0px; + &:hover { + background-color: #e5e5e5; + } + &:active { + background-color: #cacacb; + } + &.fullscreen { + &:hover { + background-color: #e5e5e5; + } + &:active { + background-color: #cacacb; + } + .widget { + margin-top: 8px; + .widget1 { width: 6px; height: 6px; } + .widget2 { display: inline-block; } + .widget3 { + display: inline-block; + width: 8px; + height: 1px; + background: black; + position: relative; + top: -21px; + left: 1px; + } + .widget4 { + display: inline-block; + width: 1px; + height: 8px; + background: black; + position: relative; + top: -14px; + left: 0px; + } + .widget5 { + display: inline-block; + width: 2px; + height: 1px; + background: black; + position: relative; + top: -14px; + left: -2px; + } + } + } + .widget { + width: 12px; + height: 12px; + display: inline-block; + vertical-align: middle; + + .widget1 { + width: 8px; + height: 8px; + border: 1px solid black; + background: none; + position: relative; + } + .widget2 { + display: none; + width: 1px; + height: 2px; + background: black; + position: relative; + top: -20px; + left: 2px; + } + .widget3 { display: none; } + .widget4 { display: none; } + .widget5 { display: none; } + } + } + + &.close { + &:hover { + background-color: #e5182c; + .widget { + .widget1{ background:white; } + .widget2{ background:white; } + } + } + &:active { + background-color: #ef717c; + .widget { + .widget1{ background:white; } + .widget2{ background:white; } + } + } + .widget { + width: 11px; + height: 11px; + display: inline-block; + vertical-align: middle; + + .widget1 { + width: 14px; + height: 1px; + background: black; + display: inline-block; + transform: rotate(45deg); + position: relative; + top: -6px; + left: -2px; + } + .widget2 { + width: 14px; + height: 1px; + background: black; + display: inline-block; + transform: rotate(315deg); + position: relative; + top: -21px; + left: -2px; + } + .widget3 { display: none; } + } + } + } + } +} + +// Menubar (for use w/ slim titlebar) +.menubarContainer { + -webkit-app-region: drag; + width: 100%; + height: 29px; + display: inline-block; + margin-top: 5px; + margin-left: 4px; + + .menubar { + display: inline-block; + cursor: default; + -webkit-user-select: none; + + .menubarItem { + color: black; + font: menu; + font-size: 12px; + -webkit-app-region: no-drag; + padding: 0 10px 1px; + border: 1px solid transparent; + + &:hover { + background-color: #e5f3ff; + border: 1px solid #cce8ff; + } + } + + .selected { + background-color: #cce8ff !important; + border: 1px solid #99d1ff !important; + } + } +} // Here I grouped the rules to keep the layout of the top bar consistent. // The tricky part is to keep the title in `#navigator` centered when the @@ -120,7 +521,6 @@ justify-content: space-between; -webkit-app-region: drag; align-items: center; - border-bottom: 1px solid #aaaaaa; height: @navbarHeight; box-sizing: border-box; diff --git a/less/tabs.less b/less/tabs.less index c83aaa02c66..59b3e645b34 100644 --- a/less/tabs.less +++ b/less/tabs.less @@ -4,6 +4,13 @@ @import "variables.less"; +// (Windows) Tabs row not right-clickable unless it has no-drag +.platform--win32 .tabStripContainer { + &.allowDragging { + -webkit-app-region: no-drag !important; + } +} + .tabs { background: @tabsBackground; box-sizing: border-box; diff --git a/less/variables.less b/less/variables.less index 0389891118b..3ce797d11ea 100644 --- a/less/variables.less +++ b/less/variables.less @@ -35,6 +35,7 @@ @defaultSpacing: 12px; @progressBarColor: #3498DB; @defaultFontSize: 13px; +@contextMenuFontSize: 14px; @audioColor: @highlightBlue; @focusUrlbarOutline: @highlightBlue; @loadTimeColor: @highlightBlue; @@ -74,6 +75,7 @@ @navbarBraveButtonWidth: 23px; @navbarBraveButtonMarginLeft: 80px; @navbarLeftMarginDarwin: 70px; +@navbarLeftMarginWindows: 5px; @findbarBackground: #F7F7F7; @@ -126,6 +128,9 @@ @zindexWindowFullScreenBanner: 4100; @breakpointNarrowViewport: 600px; +@breakpointExtensionButtonPadding: 720px; +@breakpointSmallWin32: 650px; +@breakpointTinyWin32: 500px; @transitionDuration: 100ms; @transition: all 600ms linear; diff --git a/test/components/windowTest.js b/test/components/windowTest.js index fb255f5fec2..b8afa5de7ed 100644 --- a/test/components/windowTest.js +++ b/test/components/windowTest.js @@ -1,7 +1,8 @@ /* global describe, it, before */ const Brave = require('../lib/brave') -const {activeWebview} = require('../lib/selectors') +const {activeWebview, minimizeButton, maximizeButton, closeButton} = require('../lib/selectors') +const isWindows = process.platform === 'win32' describe('application window', function () { describe('application launch', function () { @@ -146,6 +147,56 @@ describe('application window', function () { }) }) + if (isWindows) { + describe('window top action buttons', function () { + Brave.beforeAll(this) + + before(function * () { + yield this.app.client + .waitUntilWindowLoaded() + .waitForUrl(Brave.newTabUrl) + .windowByIndex(0) + .resizeWindow(600, 700) + .waitUntilWindowLoaded() + }) + + it('should be maximized when maximize button is clicked', function * () { + yield this.app.client + .click(maximizeButton) + .windowByIndex(0) + .getWindowWidth().should.eventually.be.getPrimaryDisplayWidth() + .getWindowHeight().should.eventually.be.getPrimaryDisplayHeight() + }) + + it('should be minimized when minimize button is clicked', function * () { + yield this.app.client + .click(minimizeButton) + .waitUntil(function () { + return this.windowByIndex(0).isWindowMinimized() + }) + }) + + it('should close the new window when close button is clicked', function * () { + yield this.app.client + .windowByIndex(0) + .newWindowAction() + .waitUntil(function () { + return this.getWindowCount().then((count) => { + return count === 2 + }) + }) + .windowByIndex(1) + .waitUntilWindowLoaded() + .click(closeButton) + .waitUntil(function () { + return this.getWindowCount().then((count) => { + return count === 1 + }) + }) + }) + }) + } + describe('windw.open with click', function () { describe('with features', function () { Brave.beforeAll(this) diff --git a/test/lib/selectors.js b/test/lib/selectors.js index 4ff78271d1f..7a748caa0ad 100644 --- a/test/lib/selectors.js +++ b/test/lib/selectors.js @@ -1,4 +1,7 @@ module.exports = { + minimizeButton: '.min-btn', + maximizeButton: '.max-btn', + closeButton: '.close-btn', urlInput: '#urlInput', activeWebview: '.frameWrapper.isActive webview', activeTab: '.tab.active', diff --git a/test/unit/lib/formatUtilTest.js b/test/unit/lib/formatUtilTest.js new file mode 100644 index 00000000000..134f55efa4e --- /dev/null +++ b/test/unit/lib/formatUtilTest.js @@ -0,0 +1,61 @@ +/* global describe, before, after, it */ +const formatUtil = require('../../../app/common/lib/formatUtil') +const assert = require('assert') + +require('../braveUnit') + +describe('formatUtil', function () { + describe('formatAccelerator', function () { + describe('when platform is Windows', function () { + before(function () { + this.originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') + Object.defineProperty(process, 'platform', { + value: 'win32' + }) + }) + + after(function () { + Object.defineProperty(process, 'platform', this.originalPlatform) + }) + + it('puts the modifiers in the correct order', function () { + const result = formatUtil.formatAccelerator('A+Shift+Alt+CmdOrCtrl') + assert.equal(result, 'Ctrl+Alt+Shift+A') + }) + it('leaves modifiers alone if order is correct', function () { + const result = formatUtil.formatAccelerator('Ctrl+Shift+O') + assert.equal(result, 'Ctrl+Shift+O') + }) + }) + + describe('when platform is macOS', function () { + before(function () { + this.originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') + Object.defineProperty(process, 'platform', { + value: 'darwin' + }) + }) + + after(function () { + Object.defineProperty(process, 'platform', this.originalPlatform) + }) + + it('replaces the key names with the correct symbols', function () { + const result = formatUtil.formatAccelerator('Alt+CmdOrCtrl+Ctrl+Shift+O') + assert.equal(result, '⌥⇧^⌘O') + }) + }) + }) + + describe('wrappingClamp', function () { + it('does not change value if within bounds', function () { + assert.equal(formatUtil.wrappingClamp(5, 1, 10), 5) + }) + it('wraps negatively', function () { + assert.equal(formatUtil.wrappingClamp(-7, 1, 10), 3) + }) + it('wraps positively', function () { + assert.equal(formatUtil.wrappingClamp(18, 1, 10), 8) + }) + }) +}) diff --git a/test/unit/lib/menuUtilTest.js b/test/unit/lib/menuUtilTest.js index e5ad3b06934..6e786572f0a 100644 --- a/test/unit/lib/menuUtilTest.js +++ b/test/unit/lib/menuUtilTest.js @@ -58,30 +58,6 @@ describe('menuUtil', function () { mockery.disable() }) - describe('getParentMenuDetails', function () { - const emptyValue = { - menu: null, - index: -1 - } - it('returns an object with the electron MenuItem/index based on the label', function () { - const menu = menuUtil.getParentMenuDetails(defaultMenu, 'Edit') - assert.equal(menu.index, 1) - assert.equal(menu.menu, defaultMenu.items[1]) - }) - it('returns an object with null/-1 if input menu is not truthy', function () { - const menu = menuUtil.getParentMenuDetails(null, 'Edit') - assert.deepEqual(menu, emptyValue) - }) - it('returns an object with null/-1 if label is not truthy', function () { - const menu = menuUtil.getParentMenuDetails(defaultMenu, undefined) - assert.deepEqual(menu, emptyValue) - }) - it('returns an object with null/-1 if label is not found', function () { - const menu = menuUtil.getParentMenuDetails(defaultMenu, 'History') - assert.deepEqual(menu, emptyValue) - }) - }) - describe('getMenuItem', function () { it('returns the electron MenuItem based on the label', function () { const menuItem = menuUtil.getMenuItem(defaultMenu, 'quit')