radialmenu.c 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. enum RadialMenuControlType
  2. {
  3. MOUSE,
  4. CONTROLLER
  5. }
  6. class RadialMenu : ScriptedWidgetEventHandler
  7. {
  8. protected Widget m_Parent;
  9. protected Widget m_ItemCardsContainer;
  10. protected Widget m_RadialSelector;
  11. protected ImageWidget m_RadialSelectorImage;
  12. protected ImageWidget m_RadialSelectorPointerImage;
  13. protected int m_RadialSelectorOriginalColor;
  14. protected int m_RadialSelectorDisabledColor;
  15. protected Widget m_SelectedObject;
  16. protected ref map<Widget, float> m_RadialItemCards;
  17. protected float m_AngleRadOffset;
  18. protected ref Timer m_UpdateTimer;
  19. //widget
  20. static const string RADIAL_SELECTOR = "RadialSelector";
  21. static const string RADIAL_SELECTOR_IMAGE = "SelectorImage";
  22. static const string RADIAL_SELECTOR_POINTER = "SelectorPointer";
  23. static const string RADIAL_DELIMITER_CONTAINER = "RadialDelimiterContainer";
  24. static const string RADIAL_ITEM_CARD_CONTAINER = "RadialItemCardContainer";
  25. //controls
  26. protected RadialMenuControlType m_ControlType;
  27. private UAIDWrapper m_SelectInputWrapper;
  28. private UAIDWrapper m_BackInputWrapper;
  29. protected float m_ControllerAngle;
  30. protected float m_ControllerTilt;
  31. //controller
  32. protected int m_ControllerTimout;
  33. protected bool m_IsControllerTimoutEnabled = true; //enables/disables controller deselect timeout reset
  34. protected const float CONTROLLER_DESELECT_TIMEOUT = 1000; //timeout [ms] after which selection is automatically deselect when controller is not active
  35. protected const float CONTROLLER_TILT_TRESHOLD_SELECT = 0.8; //tilt value (0.0-1.0) for controller sticks after which the selection will be selected
  36. protected const float CONTROLLER_TILT_TRESHOLD_EXECUTE = 1.0; //tilt value (0.0-1.0) for controller sticks after which the selection will be executed
  37. //mouse
  38. protected bool m_WidgetInitialized;
  39. protected const float MOUSE_SAFE_ZONE_RADIUS = 120; //Radius [px] of safe zone where every previous selection is deselected
  40. //References
  41. protected float m_RadiusOffset; //Radius [% of the main container size]
  42. protected float m_ExecuteDistanceOffset; //Distance offset [% of the main container size] after which the selection will be automatically executed
  43. protected float m_OffsetFromTop; //first item in the menu won't be directly on top but offset by a rad angle value (clock-wise)
  44. protected float m_ItemCardRadiusOffset; //Radius [% of the main container size] for item cards
  45. protected string m_DelimiterLayout; //layout file name with path
  46. ref UIScriptedMenu m_RegisteredClass;
  47. ref static RadialMenu m_Instance;
  48. /*
  49. RADIAL MENU EVENTS
  50. Mouse:
  51. OnMouseSelect
  52. OnMouseDeselect
  53. OnMouseExecute - unused, press events used instead
  54. OnMousePressLeft
  55. OnMousePressRight
  56. Controller:
  57. OnControllerSelect
  58. OnControllerDeselect
  59. OnControllerExecute - unused, press events used instead
  60. OnControllerPressSelect
  61. OnControllerPressBack
  62. Common:
  63. OnControlsChanged - controls has been changed (mouse<->controller)
  64. */
  65. //============================================
  66. // RadialMenu
  67. //============================================
  68. void RadialMenu()
  69. {
  70. m_Instance = this;
  71. //set default control type
  72. #ifdef PLATFORM_CONSOLE
  73. Input inp = GetGame().GetInput();
  74. if (inp && inp.IsEnabledMouseAndKeyboardEvenOnServer())
  75. {
  76. m_ControlType = RadialMenuControlType.MOUSE;
  77. }
  78. else
  79. {
  80. m_ControlType = RadialMenuControlType.CONTROLLER;
  81. }
  82. #endif
  83. #ifdef PLATFORM_WINDOWS
  84. m_ControlType = RadialMenuControlType.MOUSE;
  85. #endif
  86. m_SelectInputWrapper = GetUApi().GetInputByID(UAUISelect).GetPersistentWrapper();
  87. m_BackInputWrapper= GetUApi().GetInputByID(UAUIBack).GetPersistentWrapper();
  88. //radial cards
  89. m_RadialItemCards = new map<Widget, float>;
  90. m_UpdateTimer = new Timer();
  91. m_UpdateTimer.Run(0.01, this, "Update", NULL, true);
  92. }
  93. void ~RadialMenu()
  94. {
  95. }
  96. static RadialMenu GetInstance()
  97. {
  98. return m_Instance;
  99. }
  100. //Set handler
  101. void OnWidgetScriptInit(Widget w)
  102. {
  103. m_ItemCardsContainer = w.FindAnyWidget(RADIAL_ITEM_CARD_CONTAINER);
  104. m_RadialSelector = w.FindAnyWidget(RADIAL_SELECTOR);
  105. m_RadialSelectorImage = ImageWidget.Cast(m_RadialSelector.FindAnyWidget(RADIAL_SELECTOR_IMAGE));
  106. m_RadialSelectorPointerImage = ImageWidget.Cast(m_RadialSelector.FindAnyWidget(RADIAL_SELECTOR_POINTER));
  107. m_RadialSelectorOriginalColor = m_RadialSelectorImage.GetColor();
  108. m_RadialSelectorDisabledColor = ARGB(255,150,150,150);
  109. //parent
  110. m_Parent = w;
  111. m_Parent.SetHandler(this);
  112. }
  113. //controls
  114. void SetControlType(RadialMenuControlType type)
  115. {
  116. if (m_ControlType != type)
  117. {
  118. m_ControlType = type;
  119. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnControlsChanged", NULL, type);
  120. }
  121. }
  122. bool IsUsingMouse()
  123. {
  124. if (m_ControlType == RadialMenuControlType.MOUSE)
  125. {
  126. return true;
  127. }
  128. return false;
  129. }
  130. bool IsUsingController()
  131. {
  132. if (m_ControlType == RadialMenuControlType.CONTROLLER)
  133. {
  134. return true;
  135. }
  136. return false;
  137. }
  138. void SetWidgetInitialized(bool state)
  139. {
  140. m_WidgetInitialized = state;
  141. }
  142. bool IsWidgetInitialized()
  143. {
  144. return m_WidgetInitialized;
  145. }
  146. //============================================
  147. // Setup
  148. //============================================
  149. void RegisterClass(UIScriptedMenu class_name)
  150. {
  151. m_RegisteredClass = class_name;
  152. if (m_UpdateTimer && !m_UpdateTimer.IsRunning())
  153. m_UpdateTimer.Run(0.01, this, "Update", NULL, true);
  154. }
  155. //Set radial menu parameters
  156. //Radius offset [% of the main container size]
  157. void SetRadiusOffset(float radius_offset)
  158. {
  159. m_RadiusOffset = radius_offset;
  160. }
  161. //Distance offset [% of the main container size] after which the selection will be automatically executed
  162. void SetExecuteDistOffset(float execute_dist_offset)
  163. {
  164. m_ExecuteDistanceOffset = execute_dist_offset;
  165. }
  166. //First item in the menu won't be directly on top but offset by a rad angle value (clock-wise)
  167. void SetOffsetFromTop(float offset_from_top)
  168. {
  169. m_OffsetFromTop = offset_from_top;
  170. }
  171. //Radius [% of the main container size] for item cards
  172. void SetItemCardRadiusOffset(float item_card_radius_offset)
  173. {
  174. m_ItemCardRadiusOffset = item_card_radius_offset;
  175. }
  176. //Enable/Disable controller timeout
  177. void ActivateControllerTimeout(bool state)
  178. {
  179. m_IsControllerTimoutEnabled = state;
  180. }
  181. void SetWidgetProperties(string delimiter_layout)
  182. {
  183. m_DelimiterLayout = delimiter_layout;
  184. }
  185. //============================================
  186. // Visual
  187. //============================================
  188. //hide_selector => shows/hides radial selector when refreshing radial menu
  189. void Refresh(bool hide_selector = true)
  190. {
  191. int item_cards_count = GetItemCardsCount();
  192. if (item_cards_count > 0)
  193. m_AngleRadOffset = 2 * Math.PI / item_cards_count;
  194. float angle_rad = -Math.PI / 2;
  195. //--PARAM top offset--
  196. if (m_OffsetFromTop != 0)
  197. {
  198. angle_rad = angle_rad + m_OffsetFromTop;
  199. }
  200. //--------------------
  201. //delete all delimiters
  202. Widget delimiters_panel = m_Parent.FindAnyWidget(RADIAL_DELIMITER_CONTAINER);
  203. if (delimiters_panel)
  204. {
  205. Widget del_child = delimiters_panel.GetChildren();
  206. while (del_child)
  207. {
  208. Widget child_to_destroy1 = del_child;
  209. del_child = del_child.GetSibling();
  210. delete child_to_destroy1;
  211. }
  212. }
  213. //Position item cards, crate radial delimiters
  214. Widget item_cards_panel = m_Parent.FindAnyWidget(RADIAL_ITEM_CARD_CONTAINER);
  215. Widget item_card = item_cards_panel.GetChildren();
  216. //get radius
  217. float original_r = GetRadius();
  218. float item_cards_r = original_r;
  219. //--PARAM top offset--....
  220. if (m_ItemCardRadiusOffset != 0)
  221. {
  222. item_cards_r = item_cards_r * m_ItemCardRadiusOffset;
  223. if (item_cards_r < 0) item_cards_r = 0; //min radius is 0
  224. }
  225. m_RadialItemCards.Clear();
  226. for (int i = 0; i < item_cards_count; ++i)
  227. {
  228. //position item cards
  229. if (item_card)
  230. {
  231. //creates circle from simple widget items
  232. float pos_x = item_cards_r * Math.Cos(angle_rad);
  233. float pos_y = item_cards_r * Math.Sin(angle_rad);
  234. pos_x = pos_x / original_r;
  235. pos_y = pos_y / original_r;
  236. item_card.SetPos(pos_x, pos_y);
  237. //store item card
  238. m_RadialItemCards.Insert(item_card, angle_rad);
  239. //get next child
  240. item_card = item_card.GetSibling();
  241. }
  242. //-------------------------
  243. //create delimiter
  244. if (item_cards_count > 1 && delimiters_panel && m_DelimiterLayout)
  245. {
  246. Widget delimiter_widget = GetGame().GetWorkspace().CreateWidgets(m_DelimiterLayout, delimiters_panel);
  247. float delim_angle_rad = angle_rad + (m_AngleRadOffset / 2);
  248. delimiter_widget.SetPos(0, 0);
  249. delimiter_widget.SetRotation(0, 0, GetAngleInDegrees(delim_angle_rad) + 90);
  250. }
  251. //calculate next angle
  252. angle_rad += m_AngleRadOffset;
  253. }
  254. //hide selector on refresh
  255. if (hide_selector)
  256. {
  257. HideRadialSelector();
  258. }
  259. }
  260. //Radial selector
  261. protected void ShowRadialSelector(Widget selected_item)
  262. {
  263. if (m_RadialSelector && selected_item)
  264. {
  265. int item_count = m_RadialItemCards.Count();
  266. if (item_count > 1)
  267. {
  268. int angle_deg = GetAngleInDegrees(m_RadialItemCards.Get(selected_item));
  269. m_RadialSelector.SetRotation(0, 0, angle_deg + 90); //rotate widget according to its desired rotation
  270. //set radial selector size
  271. float progress = (1 / item_count) * 2;
  272. m_RadialSelectorImage.SetMaskProgress(progress);
  273. m_RadialSelector.Show(true);
  274. bool grey_selector = selected_item.GetFlags() & WidgetFlags.DISABLED;
  275. if (!grey_selector)
  276. {
  277. m_RadialSelectorImage.SetColor(m_RadialSelectorDisabledColor);
  278. m_RadialSelectorPointerImage.SetColor(m_RadialSelectorDisabledColor);
  279. }
  280. else
  281. {
  282. m_RadialSelectorImage.SetColor(m_RadialSelectorOriginalColor);
  283. m_RadialSelectorPointerImage.SetColor(m_RadialSelectorOriginalColor);
  284. }
  285. }
  286. }
  287. }
  288. protected void HideRadialSelector()
  289. {
  290. if (m_RadialSelector)
  291. {
  292. m_RadialSelector.Show(false);
  293. }
  294. }
  295. //============================================
  296. // Widget size calculations
  297. //============================================
  298. protected int GetItemCardsCount()
  299. {
  300. Widget child = m_ItemCardsContainer.GetChildren();
  301. int count = 0;
  302. while (child)
  303. {
  304. ++count;
  305. child = child.GetSibling();
  306. }
  307. return count;
  308. }
  309. protected float GetRadius()
  310. {
  311. float radius = Math.AbsFloat(GetParentMinSize() * 0.5);
  312. //PARAM --radius--
  313. if (m_RadiusOffset > 0)
  314. {
  315. return radius * m_RadiusOffset;
  316. }
  317. //----------------
  318. return radius;
  319. }
  320. protected void GetParentCenter(out float center_x, out float center_y)
  321. {
  322. if (m_Parent)
  323. {
  324. float wx;
  325. float wy;
  326. m_Parent.GetScreenPos(wx, wy);
  327. float ww;
  328. float wh;
  329. m_Parent.GetScreenSize(ww, wh);
  330. center_x = wx + ww / 2; //center
  331. center_y = wy + wh / 2;
  332. }
  333. }
  334. protected float GetParentMinSize()
  335. {
  336. if (m_Parent)
  337. {
  338. float size_x;
  339. float size_y;
  340. m_Parent.GetScreenSize(size_x, size_y);
  341. return Math.Min(size_x, size_y);
  342. }
  343. return 0;
  344. }
  345. //============================================
  346. // Angle calculations
  347. //============================================
  348. //get object by angle (degrees)
  349. protected Widget GetObjectByDegAngle(float deg_angle)
  350. {
  351. for (int i = 0; i < m_RadialItemCards.Count(); ++i)
  352. {
  353. Widget w = m_RadialItemCards.GetKey(i);
  354. float w_angle = GetAngleInDegrees(m_RadialItemCards.Get(w));
  355. float offset = GetAngleInDegrees(m_AngleRadOffset) / 2;
  356. float min_angle = w_angle - offset;
  357. float max_angle = w_angle + offset;
  358. if (min_angle < 0) min_angle += 360; //clamp 0-360
  359. if (max_angle > 360) max_angle -= 360;
  360. if (min_angle > max_angle) //angle radius is in the cycling point 360->
  361. {
  362. if (min_angle <= deg_angle) //is cursor position also before this point
  363. {
  364. if (deg_angle > max_angle)
  365. {
  366. return w;
  367. }
  368. }
  369. else //is cursor position after this point
  370. {
  371. if (deg_angle < max_angle)
  372. {
  373. return w;
  374. }
  375. }
  376. }
  377. else
  378. {
  379. if (deg_angle >= min_angle && deg_angle < max_angle) //min, max angles are within 0-360 radius
  380. {
  381. return w;
  382. }
  383. }
  384. }
  385. return NULL;
  386. }
  387. //returns GUI compatible mouse-to-parent angle
  388. protected float GetMousePointerAngle()
  389. {
  390. int mouse_x;
  391. int mouse_y;
  392. GetMousePos(mouse_x, mouse_y);
  393. float center_x;
  394. float center_y;
  395. GetParentCenter(center_x, center_y);
  396. float tan_x = mouse_x - center_x;
  397. float tan_y = mouse_y - center_y;
  398. float angle = Math.Atan2(tan_y, tan_x);
  399. return angle;
  400. }
  401. //returns distance from parent center
  402. protected float GetMouseDistance()
  403. {
  404. int mouse_x;
  405. int mouse_y;
  406. GetMousePos(mouse_x, mouse_y);
  407. float center_x;
  408. float center_y;
  409. GetParentCenter(center_x, center_y);
  410. float distance = vector.Distance(Vector(mouse_x, mouse_y, 0), Vector(center_x, center_y, 0));
  411. return distance;
  412. }
  413. //return angle 0-360 deg
  414. protected float GetAngleInDegrees(float rad_angle)
  415. {
  416. float rad_deg = rad_angle * Math.RAD2DEG;
  417. int angle_mp = rad_deg / 360;
  418. if (rad_deg < 0)
  419. {
  420. rad_deg = rad_deg - (360 * angle_mp);
  421. rad_deg += 360;
  422. }
  423. return rad_deg;
  424. }
  425. //============================================
  426. // Update
  427. //============================================
  428. //mouse
  429. int last_time = -1;
  430. protected void Update()
  431. {
  432. if (this && !m_RegisteredClass)
  433. {
  434. m_UpdateTimer.Stop();
  435. return;
  436. }
  437. //get delta time
  438. if (last_time < 0)
  439. {
  440. last_time = GetGame().GetTime();
  441. }
  442. int delta_time = GetGame().GetTime() - last_time;
  443. last_time = GetGame().GetTime();
  444. //controls
  445. if (this && m_RegisteredClass && m_RegisteredClass.IsVisible())
  446. {
  447. //mouse controls
  448. if (IsUsingMouse() && m_WidgetInitialized)
  449. {
  450. float mouse_angle = GetMousePointerAngle();
  451. float mouse_distance = GetMouseDistance();
  452. //--PARAM --safe zone radius--
  453. if (mouse_distance <= MOUSE_SAFE_ZONE_RADIUS)
  454. {
  455. //Deselect
  456. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnMouseDeselect", NULL, m_SelectedObject);
  457. m_SelectedObject = NULL;
  458. //hide selector
  459. HideRadialSelector();
  460. }
  461. else
  462. {
  463. //Deselect
  464. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnMouseDeselect", NULL, m_SelectedObject);
  465. //Select
  466. m_SelectedObject = GetObjectByDegAngle(GetAngleInDegrees(mouse_angle));
  467. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnMouseSelect", NULL, m_SelectedObject);
  468. //show selector
  469. ShowRadialSelector(m_SelectedObject);
  470. }
  471. }
  472. //controller controls
  473. else if (IsUsingController())
  474. {
  475. UpdataControllerInput();
  476. //Controller tilt
  477. if (m_ControllerAngle > -1 && m_ControllerTilt > -1)
  478. {
  479. //Right analogue stick
  480. Widget w_selected = GetObjectByDegAngle(m_ControllerAngle);
  481. //Select
  482. if (w_selected)
  483. {
  484. if (w_selected != m_SelectedObject)
  485. {
  486. if (m_ControllerTilt >= CONTROLLER_TILT_TRESHOLD_SELECT)
  487. {
  488. //Deselect
  489. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnControllerDeselect", NULL, m_SelectedObject);
  490. //Select new object
  491. m_SelectedObject = w_selected;
  492. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnControllerSelect", NULL, m_SelectedObject);
  493. //show selector
  494. ShowRadialSelector(m_SelectedObject);
  495. }
  496. }
  497. }
  498. else
  499. {
  500. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnControllerDeselect", NULL, m_SelectedObject);
  501. m_SelectedObject = NULL;
  502. //hide selector
  503. HideRadialSelector();
  504. }
  505. }
  506. //if controller is giving no feedback
  507. else
  508. {
  509. if (m_IsControllerTimoutEnabled)
  510. {
  511. m_ControllerTimout += delta_time;
  512. if (m_ControllerTimout >= CONTROLLER_DESELECT_TIMEOUT)
  513. {
  514. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnControllerDeselect", NULL, m_SelectedObject);
  515. m_SelectedObject = NULL;
  516. //hide selector
  517. HideRadialSelector();
  518. m_ControllerTimout = 0; //reset controller timeout
  519. }
  520. }
  521. }
  522. m_ControllerAngle = -1; //reset angle and tilt
  523. m_ControllerTilt = -1;
  524. }
  525. m_WidgetInitialized = true;
  526. }
  527. }
  528. float NormalizeInvertAngle(float angle)
  529. {
  530. float new_angle = 360 - angle;
  531. int angle_mp = new_angle / 360;
  532. new_angle = new_angle - (360 * angle_mp);
  533. return new_angle;
  534. }
  535. //============================================
  536. // Controls
  537. //============================================
  538. void UpdataControllerInput()
  539. {
  540. Input input = GetGame().GetInput();
  541. //Controller radial
  542. float angle;
  543. float tilt;
  544. input.GetGamepadThumbDirection(GamepadButton.THUMB_RIGHT, angle, tilt);
  545. angle = NormalizeInvertAngle(angle * Math.RAD2DEG);
  546. m_ControllerAngle = angle;
  547. m_ControllerTilt = tilt;
  548. m_ControllerTimout = 0; //reset controller timeout
  549. //Controller buttons
  550. //Select (A,cross)
  551. if (m_SelectInputWrapper.InputP().LocalPress())
  552. {
  553. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnControllerPressSelect", NULL, m_SelectedObject);
  554. }
  555. //Back (B,circle)
  556. if (m_BackInputWrapper.InputP().LocalPress())
  557. {
  558. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnControllerPressBack", NULL, m_SelectedObject);
  559. }
  560. }
  561. override bool OnMouseButtonUp(Widget w, int x, int y, int button)
  562. {
  563. if (button == MouseState.LEFT && m_SelectedObject/* && w == m_SelectedObject*/)
  564. {
  565. //Execute
  566. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnMousePressLeft", NULL, m_SelectedObject);
  567. return true;
  568. }
  569. if (button == MouseState.RIGHT)
  570. {
  571. //Back one level
  572. GetGame().GameScript.CallFunction(m_RegisteredClass, "OnMousePressRight", NULL, NULL);
  573. return true;
  574. }
  575. return false;
  576. }
  577. }