PieMenu.qml 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  1. /****************************************************************************
  2. **
  3. ** Copyright (C) 2016 The Qt Company Ltd.
  4. ** Contact: https://www.qt.io/licensing/
  5. **
  6. ** This file is part of the Qt Quick Extras module of the Qt Toolkit.
  7. **
  8. ** $QT_BEGIN_LICENSE:LGPL$
  9. ** Commercial License Usage
  10. ** Licensees holding valid commercial Qt licenses may use this file in
  11. ** accordance with the commercial license agreement provided with the
  12. ** Software or, alternatively, in accordance with the terms contained in
  13. ** a written agreement between you and The Qt Company. For licensing terms
  14. ** and conditions see https://www.qt.io/terms-conditions. For further
  15. ** information use the contact form at https://www.qt.io/contact-us.
  16. **
  17. ** GNU Lesser General Public License Usage
  18. ** Alternatively, this file may be used under the terms of the GNU Lesser
  19. ** General Public License version 3 as published by the Free Software
  20. ** Foundation and appearing in the file LICENSE.LGPL3 included in the
  21. ** packaging of this file. Please review the following information to
  22. ** ensure the GNU Lesser General Public License version 3 requirements
  23. ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
  24. **
  25. ** GNU General Public License Usage
  26. ** Alternatively, this file may be used under the terms of the GNU
  27. ** General Public License version 2.0 or (at your option) the GNU General
  28. ** Public license version 3 or any later version approved by the KDE Free
  29. ** Qt Foundation. The licenses are as published by the Free Software
  30. ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
  31. ** included in the packaging of this file. Please review the following
  32. ** information to ensure the GNU General Public License requirements will
  33. ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
  34. ** https://www.gnu.org/licenses/gpl-3.0.html.
  35. **
  36. ** $QT_END_LICENSE$
  37. **
  38. ****************************************************************************/
  39. import QtQuick 2.2
  40. import QtQuick.Controls 1.4
  41. import QtQuick.Controls.Styles 1.4
  42. import QtQuick.Controls.Private 1.0
  43. import QtQuick.Extras 1.4
  44. import QtQuick.Extras.Private 1.0
  45. import QtQuick.Extras.Private.CppUtils 1.0 as CppUtils
  46. /*!
  47. \qmltype PieMenu
  48. \inqmlmodule QtQuick.Extras
  49. \since 5.5
  50. \ingroup extras
  51. \ingroup extras-interactive
  52. \brief A popup menu that displays several menu items along an arc.
  53. \image piemenu.png A PieMenu
  54. The PieMenu provides a radial context menu as an alternative to a
  55. traditional menu. All of the items in a PieMenu are an equal distance
  56. from the center of the control.
  57. \section2 Populating the Menu
  58. To create a menu, define at least one MenuItem as a child of it:
  59. \code
  60. PieMenu {
  61. id: pieMenu
  62. MenuItem {
  63. text: "Action 1"
  64. onTriggered: print("Action 1")
  65. }
  66. MenuItem {
  67. text: "Action 2"
  68. onTriggered: print("Action 2")
  69. }
  70. MenuItem {
  71. text: "Action 3"
  72. onTriggered: print("Action 3")
  73. }
  74. }
  75. \endcode
  76. By default, only the currently selected item's text is displayed above the
  77. menu. To provide text that is always visible when there is no current item,
  78. set the \l title property.
  79. \section2 Displaying the Menu
  80. The typical use case for a menu is to open at the point of the mouse
  81. cursor after a right click occurs. To do that, define a MouseArea that
  82. covers the region upon which clicks should open the menu. When the
  83. MouseArea is right-clicked, call the popup() function:
  84. \code
  85. MouseArea {
  86. anchors.fill: parent
  87. acceptedButtons: Qt.RightButton
  88. onClicked: pieMenu.popup(mouseX, mouseY)
  89. }
  90. \endcode
  91. If the menu is opened in a position where some of its menu items would be
  92. outside of \l boundingItem, it is automatically moved to a position where
  93. they will not be hidden. By default, the boundingItem is set to the parent
  94. of the menu. It can also be set to \c null to prevent this behavior.
  95. PieMenu can be displayed at any position on the screen. With a traditional
  96. context menu, the menu would be positioned with its top left corner at the
  97. position of the right click, but since PieMenu is radial, we position it
  98. centered over the position of the right click.
  99. To create a PieMenu that opens after a long press and selects items upon
  100. releasing, you can combine ActivationMode.ActivateOnRelease with a
  101. MouseArea using a Timer:
  102. \code
  103. MouseArea {
  104. id: touchArea
  105. anchors.fill: parent
  106. Timer {
  107. id: pressAndHoldTimer
  108. interval: 300
  109. onTriggered: pieMenu.popup(touchArea.mouseX, touchArea.mouseY);
  110. }
  111. onPressed: pressAndHoldTimer.start()
  112. onReleased: pressAndHoldTimer.stop();
  113. }
  114. PieMenu {
  115. id: pieMenu
  116. triggerMode: TriggerMode.TriggerOnRelease
  117. MenuItem {
  118. text: "Action 1"
  119. onTriggered: print("Action 1")
  120. }
  121. MenuItem {
  122. text: "Action 2"
  123. onTriggered: print("Action 2")
  124. }
  125. MenuItem {
  126. text: "Action 3"
  127. onTriggered: print("Action 3")
  128. }
  129. }
  130. \endcode
  131. You can hide individual menu items by setting their visible property to
  132. \c false. Hiding items does not affect the
  133. \l {PieMenuStyle::}{startAngle} or
  134. \l {PieMenuStyle::}{endAngle}; the
  135. remaining items will grow to consume the available space.
  136. You can create a custom appearance for a PieMenu by assigning a \l {PieMenuStyle}
  137. */
  138. Control {
  139. id: pieMenu
  140. visible: false
  141. style: Settings.styleComponent(Settings.style, "PieMenuStyle.qml", pieMenu)
  142. /*!
  143. This property reflects the angle (in radians) created by the imaginary
  144. line from the center of the menu to the position of the cursor.
  145. Its value is undefined when the menu is not visible.
  146. */
  147. readonly property real selectionAngle: {
  148. var centerX = width / 2;
  149. var centerY = height / 2;
  150. var targetX = __protectedScope.selectionPos.x;
  151. var targetY = __protectedScope.selectionPos.y;
  152. var xDistance = centerX - targetX;
  153. var yDistance = centerY - targetY;
  154. var angleToTarget = Math.atan2(xDistance, yDistance) * -1;
  155. angleToTarget;
  156. }
  157. /*!
  158. \qmlproperty enumeration PieMenu::activationMode
  159. This property determines the method for selecting items in the menu.
  160. \list
  161. \li An activationMode of \a ActivationMode.ActivateOnPress means that menu
  162. items will only be selected when a mouse press event occurs over them.
  163. \li An activationMode of \a ActivationMode.ActivateOnRelease means that menu
  164. items will only be selected when a mouse release event occurs over them.
  165. This means that the user must keep the mouse button down after opening
  166. the menu and release the mouse over the item they wish to select.
  167. \li An activationMode of \a ActivationMode.ActivateOnClick means that menu
  168. items will only be selected when the user clicks once over them.
  169. \endlist
  170. \warning Changing the activationMode while the menu is visible will
  171. result in undefined behavior.
  172. \deprecated Use triggerMode instead.
  173. */
  174. property alias activationMode: pieMenu.triggerMode
  175. /*!
  176. \qmlproperty enumeration PieMenu::triggerMode
  177. This property determines the method for selecting items in the menu.
  178. \list
  179. \li A triggerMode of \a TriggerMode.TriggerOnPress means that menu
  180. items will only be selected when a mouse press event occurs over them.
  181. \li A triggerMode of \a TriggerMode.TriggerOnRelease means that menu
  182. items will only be selected when a mouse release event occurs over them.
  183. This means that the user must keep the mouse button down after opening
  184. the menu and release the mouse over the item they wish to select.
  185. \li A triggerMode of \a TriggerMode.TriggerOnClick means that menu
  186. items will only be selected when the user clicks once over them.
  187. \endlist
  188. \warning Changing the triggerMode while the menu is visible will
  189. result in undefined behavior.
  190. */
  191. property int triggerMode: TriggerMode.TriggerOnClick
  192. /*!
  193. \qmlproperty list<MenuItem> menuItems
  194. The list of menu items displayed by this menu.
  195. You can assign menu items by declaring them as children of PieMenu:
  196. \code
  197. PieMenu {
  198. MenuItem {
  199. text: "Action 1"
  200. onTriggered: function() { print("Action 1"); }
  201. }
  202. MenuItem {
  203. text: "Action 2"
  204. onTriggered: function() { print("Action 2"); }
  205. }
  206. MenuItem {
  207. text: "Action 3"
  208. onTriggered: function() { print("Action 3"); }
  209. }
  210. }
  211. \endcode
  212. */
  213. default property alias menuItems: defaultPropertyHack.menuItems
  214. QtObject {
  215. // Can't specify a list as a default property (QTBUG-10822)
  216. id: defaultPropertyHack
  217. property list<MenuItem> menuItems
  218. }
  219. /*!
  220. \qmlproperty int PieMenu::currentIndex
  221. The index of the the menu item that is currently under the mouse,
  222. or \c -1 if there is no such item.
  223. */
  224. readonly property alias currentIndex: protectedScope.currentIndex
  225. /*!
  226. \qmlproperty int PieMenu::currentItem
  227. The menu item that is currently under the mouse, or \c null if there is
  228. no such item.
  229. */
  230. readonly property alias currentItem: protectedScope.currentItem
  231. /*!
  232. This property defines the text that is shown above the menu when
  233. there is no current menu item (currentIndex is \c -1).
  234. The default value is \c "" (an empty string).
  235. */
  236. property string title: ""
  237. /*!
  238. The item which the menu must stay within.
  239. A typical use case for PieMenu involves:
  240. \list
  241. \li A MouseArea that determines the clickable area within which the
  242. menu can be opened.
  243. \li The bounds that the menu must not go outside of.
  244. \endlist
  245. Although they sound similar, they have different purposes. Consider the
  246. example below:
  247. \image piemenu-boundingItem-example.png Canvas boundingItem example
  248. The user can only open the menu within the inner rectangle. In this
  249. case, they've opened the menu on the edge of the MouseArea, but there
  250. would not be enough room to display the entire menu centered at the
  251. cursor position, so it was moved to the left.
  252. If for some reason we didn't want this restriction, we can set
  253. boundingItem to \c null:
  254. \image piemenu-boundingItem-null-example.png Canvas null boundingItem example
  255. By default, the menu's \l {Item::}{parent} is the boundingItem.
  256. */
  257. property Item boundingItem: parent
  258. /*!
  259. \qmlmethod void popup(real x, real y)
  260. Opens the menu at coordinates \a x, \a y.
  261. */
  262. function popup(x, y) {
  263. if (x !== undefined)
  264. pieMenu.x = x - pieMenu.width / 2;
  265. if (y !== undefined)
  266. pieMenu.y = y - pieMenu.height / 2;
  267. pieMenu.visible = true;
  268. }
  269. /*!
  270. \qmlmethod void addItem(string text)
  271. Adds a \a text item to the end of the menu items.
  272. Equivalent to passing calling \c insertItem(menuItems.length, text).
  273. Returns the newly added item.
  274. */
  275. function addItem(text) {
  276. return insertItem(menuItems.length, text);
  277. }
  278. /*!
  279. \qmlmethod void insertItem(int before, string text)
  280. Inserts a MenuItem with \a text before the index at \a before.
  281. To insert an item at the end, pass \c menuItems.length.
  282. Returns the newly inserted item, or \c null if \a before is invalid.
  283. */
  284. function insertItem(before, text) {
  285. if (before < 0 || before > menuItems.length) {
  286. return null;
  287. }
  288. var newItems = __protectedScope.copyItemsToJsArray();
  289. var newItem = Qt.createQmlObject("import QtQuick.Controls 1.1; MenuItem {}", pieMenu, "");
  290. newItem.text = text;
  291. newItems.splice(before, 0, newItem);
  292. menuItems = newItems;
  293. return newItem;
  294. }
  295. /*!
  296. \qmlmethod void removeItem(item)
  297. Removes \a item from the menu.
  298. */
  299. function removeItem(item) {
  300. for (var i = 0; i < menuItems.length; ++i) {
  301. if (menuItems[i] === item) {
  302. var newItems = __protectedScope.copyItemsToJsArray();
  303. newItems.splice(i, 1);
  304. menuItems = newItems;
  305. break;
  306. }
  307. }
  308. }
  309. MouseArea {
  310. id: mouseArea
  311. anchors.fill: parent
  312. hoverEnabled: !Settings.hasTouchScreen && triggerMode !== TriggerMode.TriggerOnRelease
  313. acceptedButtons: Qt.LeftButton | Qt.RightButton
  314. onContainsMouseChanged: if (!containsMouse) __protectedScope.currentIndex = -1
  315. objectName: "PieMenu internal MouseArea"
  316. // The mouse thief also updates the selectionPos, so we can't bind to
  317. // this mouseArea's mouseX/mouseY.
  318. onPositionChanged: {
  319. __protectedScope.selectionPos = Qt.point(mouseX, mouseY)
  320. }
  321. }
  322. /*! \internal */
  323. property alias __mouseThief: mouseThief
  324. CppUtils.MouseThief {
  325. id: mouseThief
  326. onPressed: {
  327. __protectedScope.selectionPos = Qt.point(mouseX, mouseY);
  328. if (__protectedScope.handleEvent(ActivationMode.ActivateOnPress)) {
  329. mouseThief.acceptCurrentEvent();
  330. // We handled the press event, so we can reset this now.
  331. mouseThief.receivedPressEvent = false;
  332. }
  333. }
  334. onReleased: {
  335. __protectedScope.selectionPos = Qt.point(mouseX, mouseY);
  336. if (__protectedScope.handleEvent(ActivationMode.ActivateOnRelease)) {
  337. mouseThief.acceptCurrentEvent();
  338. // We handled the press event, so we can reset this now.
  339. mouseThief.receivedPressEvent = false;
  340. }
  341. __protectedScope.pressedIndex = -1;
  342. }
  343. onClicked: {
  344. __protectedScope.selectionPos = Qt.point(mouseX, mouseY);
  345. if (__protectedScope.handleEvent(ActivationMode.ActivateOnClick)) {
  346. mouseThief.acceptCurrentEvent();
  347. }
  348. // Clicked is the last stage in a click event (press, release, click),
  349. // so we can safely set this to false now.
  350. mouseThief.receivedPressEvent = false;
  351. }
  352. onTouchUpdate: __protectedScope.selectionPos = Qt.point(mouseX, mouseY)
  353. }
  354. onVisibleChanged: {
  355. // parent check is for when it's created without a parent,
  356. // which we do in the tests, for example.
  357. if (parent) {
  358. if (visible) {
  359. if (boundingItem)
  360. __protectedScope.moveWithinBounds();
  361. // We need to grab the mouse so that we can detect released()
  362. // (which is only emitted after pressed(), which our MouseArea can't
  363. // emit as it didn't have focus until we were made visible).
  364. mouseThief.grabMouse(mouseArea);
  365. } else {
  366. mouseThief.ungrabMouse();
  367. __protectedScope.selectionPos = Qt.point(width / 2, height / 2);
  368. }
  369. }
  370. }
  371. onSelectionAngleChanged: __protectedScope.checkForCurrentItem()
  372. /*! \internal */
  373. property QtObject __protectedScope: QtObject {
  374. id: protectedScope
  375. property int currentIndex: -1
  376. property MenuItem currentItem: currentIndex != -1 ? visibleItems[currentIndex] : null
  377. property point selectionPos: Qt.point(width / 2, height / 2)
  378. property int pressedIndex: -1
  379. readonly property var localRect: mapFromItem(mouseArea, mouseArea.mouseX, mouseArea.mouseY)
  380. readonly property var visibleItems: {
  381. var items = [];
  382. for (var i = 0; i < menuItems.length; ++i) {
  383. if (menuItems[i].visible) {
  384. items.push(menuItems[i]);
  385. }
  386. }
  387. return items;
  388. }
  389. onSelectionPosChanged: __protectedScope.checkForCurrentItem()
  390. // Can't bind directly, because the menu sets this to (0, 0) on closing.
  391. onLocalRectChanged: {
  392. if (visible)
  393. selectionPos = Qt.point(localRect.x, localRect.y);
  394. }
  395. function copyItemsToJsArray() {
  396. var newItems = [];
  397. for (var j = 0; j < menuItems.length; ++j) {
  398. newItems.push(menuItems[j]);
  399. }
  400. return newItems;
  401. }
  402. /*!
  403. Returns \c true if the mouse is over the section at \a itemIndex.
  404. */
  405. function isMouseOver(itemIndex) {
  406. if (__style == null)
  407. return false;
  408. // Our mouse angle's origin is north naturally, but the section angles need to be
  409. // altered to have their origin north, so we need to remove the alteration here in order to compare properly.
  410. // For example, section 0 will start at -1.57, whereas we want it to start at 0.
  411. var sectionStart = __protectedScope.sectionStartAngle(itemIndex) + Math.PI / 2;
  412. var sectionEnd = __protectedScope.sectionEndAngle(itemIndex) + Math.PI / 2;
  413. var selAngle = selectionAngle;
  414. var isWithinOurAngle = false;
  415. if (sectionStart > CppUtils.MathUtils.pi2) {
  416. sectionStart %= CppUtils.MathUtils.pi2;
  417. } else if (sectionStart < -CppUtils.MathUtils.pi2) {
  418. sectionStart %= -CppUtils.MathUtils.pi2;
  419. }
  420. if (sectionEnd > CppUtils.MathUtils.pi2) {
  421. sectionEnd %= CppUtils.MathUtils.pi2;
  422. } else if (sectionEnd < -CppUtils.MathUtils.pi2) {
  423. sectionEnd %= -CppUtils.MathUtils.pi2;
  424. }
  425. // If the section crosses the -180 => 180 wrap-around point (from atan2),
  426. // temporarily rotate the section so it doesn't.
  427. if (sectionStart > Math.PI) {
  428. var difference = sectionStart - Math.PI;
  429. selAngle -= difference;
  430. sectionStart -= difference;
  431. sectionEnd -= difference;
  432. } else if (sectionStart < -Math.PI) {
  433. difference = Math.abs(sectionStart - (-Math.PI));
  434. selAngle += difference;
  435. sectionStart += difference;
  436. sectionEnd += difference;
  437. }
  438. if (sectionEnd > Math.PI) {
  439. difference = sectionEnd - Math.PI;
  440. selAngle -= difference;
  441. sectionStart -= difference;
  442. sectionEnd -= difference;
  443. } else if (sectionEnd < -Math.PI) {
  444. difference = Math.abs(sectionEnd - (-Math.PI));
  445. selAngle += difference;
  446. sectionStart += difference;
  447. sectionEnd += difference;
  448. }
  449. // If we moved the mouse past -180 or 180, we need to move it back within,
  450. // without changing its actual direction.
  451. if (selAngle > Math.PI) {
  452. selAngle = selAngle - CppUtils.MathUtils.pi2;
  453. } else if (selAngle < -Math.PI) {
  454. selAngle += CppUtils.MathUtils.pi2;
  455. }
  456. if (sectionStart > sectionEnd) {
  457. isWithinOurAngle = selAngle >= sectionEnd && selAngle < sectionStart;
  458. } else {
  459. isWithinOurAngle = selAngle >= sectionStart && selAngle < sectionEnd;
  460. }
  461. var x1 = width / 2;
  462. var y1 = height / 2;
  463. var x2 = __protectedScope.selectionPos.x;
  464. var y2 = __protectedScope.selectionPos.y;
  465. var distanceFromCenter = Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2);
  466. var cancelRadiusSquared = __style.cancelRadius * __style.cancelRadius;
  467. var styleRadiusSquared = __style.radius * __style.radius;
  468. var isWithinOurRadius = distanceFromCenter >= cancelRadiusSquared
  469. && distanceFromCenter < styleRadiusSquared;
  470. return isWithinOurAngle && isWithinOurRadius;
  471. }
  472. readonly property real arcRange: endAngleRadians - startAngleRadians
  473. /*!
  474. The size of one section in radians.
  475. */
  476. readonly property real sectionSize: arcRange / visibleItems.length
  477. readonly property real startAngleRadians: CppUtils.MathUtils.degToRadOffset(__style.startAngle)
  478. readonly property real endAngleRadians: CppUtils.MathUtils.degToRadOffset(__style.endAngle)
  479. readonly property real circumferenceOfFullRange: 2 * Math.PI * __style.radius
  480. readonly property real percentageOfFullRange: (arcRange / (Math.PI * 2))
  481. readonly property real circumferenceOfSection: (sectionSize / arcRange) * (percentageOfFullRange * circumferenceOfFullRange)
  482. function sectionStartAngle(section) {
  483. var start = startAngleRadians + section * sectionSize;
  484. return start;
  485. }
  486. function sectionCenterAngle(section) {
  487. return (sectionStartAngle(section) + sectionEndAngle(section)) / 2;
  488. }
  489. function sectionEndAngle(section) {
  490. var end = startAngleRadians + section * sectionSize + sectionSize;
  491. return end;
  492. }
  493. function handleEvent(eventType) {
  494. if (!visible)
  495. return false;
  496. checkForCurrentItem();
  497. if (eventType === TriggerMode.TriggerOnPress)
  498. pressedIndex = currentIndex;
  499. if (eventType === TriggerMode.TriggerOnPress && triggerMode === TriggerMode.TriggerOnClick) {
  500. // We *MUST* accept press events if we plan on also accepting the release
  501. // (aka click, since we create that ourselves) event. If we don't, the
  502. // external mouse area gets the press event but not the release event,
  503. // and won't open until a release event is received, which means until the
  504. // user taps twice on the external mouse area.
  505. // Usually, we accept the current event in the onX MouseThief event handlers above,
  506. // but there we set receivedPressEvent to false if this function says it handled
  507. // the event, which we don't want, since TriggerOnClick is expecting to have
  508. // received a press event. So, we ensure that receivedPressEvent stays true
  509. // by saying we didn't handle the event, even though we actually do.
  510. mouseThief.acceptCurrentEvent();
  511. return false;
  512. }
  513. if (triggerMode === eventType) {
  514. if (eventType === TriggerMode.TriggerOnClick && !mouseThief.receivedPressEvent) {
  515. // When the trigger mode is TriggerOnClick, we can't
  516. // act on a click event if we didn't receive the press.
  517. return false;
  518. }
  519. // Setting visible to false resets the selectionPos to the center
  520. // of the menu, which in turn causes the currentItem check to be re-evaluated,
  521. // which sees that there's no current item because the selectionPos is centered.
  522. // To avoid all of that, we store these variables before setting visible to false.
  523. var currentItemBeforeClosing = currentItem;
  524. var selectionPosBeforeClosing = selectionPos;
  525. var currentIndexBeforeClosing = currentIndex;
  526. // If the cursor was over an item; trigger it. If it wasn't,
  527. // close our menu regardless. We do this first so that it's
  528. // possible to keep the menu open by setting visible to true in onTriggered.
  529. visible = false;
  530. if (currentItemBeforeClosing) {
  531. currentItemBeforeClosing.trigger();
  532. }
  533. if (visible && !Settings.hasTouchScreen && !Settings.isMobile) {
  534. // The user kept the menu open in onTriggered, so restore the hover stuff.
  535. selectionPos = selectionPosBeforeClosing;
  536. currentIndex = currentIndexBeforeClosing;
  537. }
  538. // If the trigger mode and event are Release, we should ensure
  539. // that we received a press event beforehand. If we didn't, we shouldn't steal
  540. // the event in MouseThief's event filter.
  541. return mouseThief.receivedPressEvent;
  542. }
  543. return false;
  544. }
  545. function checkForCurrentItem() {
  546. // Use a temporary varibable because setting currentIndex to -1 here
  547. // will trigger onCurrentIndexChanged.
  548. if (!!visibleItems) {
  549. var hoveredIndex = -1;
  550. for (var i = 0; i < visibleItems.length; ++i) {
  551. if (isMouseOver(i)) {
  552. hoveredIndex = i;
  553. break;
  554. }
  555. }
  556. currentIndex = hoveredIndex;
  557. }
  558. }
  559. function simplifyAngle(angle) {
  560. var simplified = angle % 360;
  561. if (simplified < 0)
  562. simplified += 360;
  563. return simplified;
  564. }
  565. function isWithinBottomEdge() {
  566. var start = simplifyAngle(pieMenu.__style.startAngle);
  567. var end = simplifyAngle(pieMenu.__style.endAngle);
  568. return start >= 270 && end <= 90 && ((start < 360 && end <= 360) || (start >= 0 && end > 0));
  569. }
  570. function isWithinTopEdge() {
  571. var start = simplifyAngle(pieMenu.__style.startAngle);
  572. var end = simplifyAngle(pieMenu.__style.endAngle);
  573. return start >= 90 && start < 270 && end > 90 && end <= 270;
  574. }
  575. function isWithinLeftEdge() {
  576. var start = simplifyAngle(pieMenu.__style.startAngle);
  577. var end = simplifyAngle(pieMenu.__style.endAngle);
  578. return (start === 360 || start >= 0) && start < 180 && end > 0 && end <= 180;
  579. }
  580. function isWithinRightEdge() {
  581. var start = simplifyAngle(pieMenu.__style.startAngle);
  582. var end = simplifyAngle(pieMenu.__style.endAngle);
  583. return start >= 180 && start < 360 && end > 180 && (end === 360 || end === 0);
  584. }
  585. /*!
  586. Moves the menu if it would open with parts outside of \a rootParent.
  587. */
  588. function moveWithinBounds() {
  589. // Find the bounding rect of the bounding item in the parent's referential.
  590. var topLeft = boundingItem.mapToItem(pieMenu.parent, 0, 0);
  591. var topRight = boundingItem.mapToItem(pieMenu.parent, boundingItem.width, 0);
  592. var bottomLeft = boundingItem.mapToItem(pieMenu.parent, 0, boundingItem.height);
  593. var bottomRight = boundingItem.mapToItem(pieMenu.parent, boundingItem.width, boundingItem.height);
  594. // If the boundingItem is rotated, normalize the bounding rect.
  595. topLeft.x = Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
  596. topLeft.y = Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);
  597. bottomRight.x = Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
  598. bottomRight.y = Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);
  599. if (pieMenu.x < topLeft.x && !isWithinLeftEdge()) {
  600. // The width and height of the menu is always that of a full circle,
  601. // so the menu is not always outside an edge when it's outside the edge -
  602. // it depends on the start and end angles.
  603. pieMenu.x = topLeft.x;
  604. } else if (pieMenu.x + pieMenu.width > bottomRight.x && !isWithinRightEdge()) {
  605. pieMenu.x = bottomRight.x - pieMenu.width;
  606. }
  607. if (pieMenu.y < topLeft.y && !isWithinTopEdge()) {
  608. pieMenu.y = topLeft.y;
  609. } else if (pieMenu.y + pieMenu.height > bottomRight.y && !isWithinBottomEdge()) {
  610. pieMenu.y = bottomRight.y - pieMenu.height;
  611. }
  612. }
  613. }
  614. }