diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/nodes/RadialMenu.scss | 83 | ||||
-rw-r--r-- | src/client/views/nodes/RadialMenu.tsx | 218 | ||||
-rw-r--r-- | src/client/views/nodes/RadialMenuItem.tsx | 96 |
3 files changed, 397 insertions, 0 deletions
diff --git a/src/client/views/nodes/RadialMenu.scss b/src/client/views/nodes/RadialMenu.scss new file mode 100644 index 000000000..ce0c263ef --- /dev/null +++ b/src/client/views/nodes/RadialMenu.scss @@ -0,0 +1,83 @@ +@import "../globalCssVariables"; + +.radialMenu-cont { + position: absolute; + z-index: $radialMenu-zindex; + flex-direction: column; +} + +.radialMenu-subMenu-cont { + position: absolute; + display: flex; + z-index: 1000; + flex-direction: column; + border-radius: 15px; + padding-top: 10px; + padding-bottom: 10px; +} + +.radialMenu-item { + // width: 11vw; //10vw + display: flex; //comment out to allow search icon to be inline with search text + align-items: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all .1s; + border-style: none; + white-space: nowrap; + font-size: 13px; + letter-spacing: 2px; + text-transform: uppercase; +} + +s +.radialMenu-itemSelected { + border-style: none; +} + +.radialMenu-group { + // width: 11vw; //10vw + display: flex; //comment out to allow search icon to be inline with search text + justify-content: left; + align-items: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all .1s; + border-width: .11px; + border-style: none; + border-color: $intermediate-color; // rgb(187, 186, 186); + // padding: 10px 0px 10px 0px; + white-space: nowrap; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 2px; + padding-left: 5px; +} + + +.radialMenu-description { + margin-left: 5px; + text-align: left; + display: inline; //need this? +} + + + +.icon-background { + pointer-events: all; + height:100%; + margin-top: 15px; + background-color: transparent; + width: 35px; + text-align: center; + font-size: 20px; + margin-left: 5px; +}
\ No newline at end of file diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx new file mode 100644 index 000000000..4fbb40c5f --- /dev/null +++ b/src/client/views/nodes/RadialMenu.tsx @@ -0,0 +1,218 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import { action, observable, computed, IReactionDisposer, reaction, runInAction } from "mobx"; +import { RadialMenuItem, RadialMenuProps, OriginalMenuProps } from "./RadialMenuItem"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Measure from "react-measure"; +import "./RadialMenu.scss"; + + + +@observer +export class RadialMenu extends React.Component { + static Instance: RadialMenu; + static readonly buffer = 20; + + constructor(props: Readonly<{}>) { + super(props); + + RadialMenu.Instance = this; + } + + @observable private _mouseX: number = -1; + @observable private _mouseY: number = -1; + @observable private _shouldDisplay: boolean = false; + @observable private _mouseDown: boolean = false; + private _reactionDisposer?: IReactionDisposer; + + + @action + onPointerDown = (e: PointerEvent) => { + this._mouseDown = true; + this._mouseX = e.clientX; + this._mouseY = e.clientY; + document.addEventListener("pointermove", this.onPointerMove); + } + + @observable + private _closest:number=-1; + + @action + onPointerMove = (e: PointerEvent) => { + const curX = e.clientX; + const curY = e.clientY; + const deltX = this._mouseX-curX + const deltY = this._mouseY-curY + const scale = Math.hypot(deltY,deltX) + + if (scale <150 && scale > 50){ + const rad = Math.atan2(deltY,deltX)+Math.PI; + let closest =0; + let closestval = 999999999; + for (let x =0; x<this._items.length; x++){ + let curmin= (x/this._items.length)*2*Math.PI; + if (rad-curmin<closestval && rad-curmin>0){ + closestval=rad-curmin + closest=x; + } + } + this._closest=closest; + } + else{ + this._closest=-1; + } + } + @action + onPointerUp = (e: PointerEvent) => { + this._mouseDown = false; + const curX = e.clientX; + const curY = e.clientY; + if (this._mouseX !== curX || this._mouseY !== curY) { + this._shouldDisplay = false; + } + this._shouldDisplay && (this._display = true); + document.removeEventListener("pointermove", this.onPointerMove); + if (this._closest!==-1){ + this._items[this._closest].event(); + } + } + componentWillUnmount() { + document.removeEventListener("pointerdown", this.onPointerDown); + + document.removeEventListener("pointerup", this.onPointerUp); + this._reactionDisposer && this._reactionDisposer(); + } + + @action + componentDidMount = () => { + document.addEventListener("pointerdown", this.onPointerDown); + document.addEventListener("pointerup", this.onPointerUp); + + this._reactionDisposer = reaction( + () => this._shouldDisplay, + () => this._shouldDisplay && !this._mouseDown && runInAction(() => this._display = true) + ); + } + + @observable private _pageX: number = 0; + @observable private _pageY: number = 0; + @observable private _display: boolean = false; + @observable private _yRelativeToTop: boolean = true; + + + @observable private _width: number = 0; + @observable private _height: number = 0; + + + getItems() { + return this._items; + } + + findByDescription = (target: string, toLowerCase = false) => { + return this._items.find(menuItem => { + let reference = menuItem.description; + toLowerCase && (reference = reference.toLowerCase()); + return reference === target; + }); + } + + + @action + addItem(item: RadialMenuProps) { + if (this._items.indexOf(item) === -1) { + this._items.push(item); + } + } + + @observable + private _items: Array<RadialMenuProps> = []; + + @action + displayMenu = (x: number, y: number) => { + //maxX and maxY will change if the UI/font size changes, but will work for any amount + //of items added to the menu + + this._pageX = x; + this._pageY = y; + this._shouldDisplay = true; + } + + get pageX() { + const x = this._pageX; + if (x < 0) { + return 0; + } + const width = this._width; + if (x + width > window.innerWidth - RadialMenu.buffer) { + return window.innerWidth - RadialMenu.buffer - width; + } + return x; + } + + get pageY() { + const y = this._pageY; + if (y < 0) { + return 0; + } + const height = this._height; + if (y + height > window.innerHeight - RadialMenu.buffer) { + return window.innerHeight - RadialMenu.buffer - height; + } + return y; + } + + @computed get menuItems() { + return this._items.map((item,index) => <RadialMenuItem {...item} key={item.description} closeMenu={this.closeMenu} max={this._items.length} min={index} selected={this._closest} />); + } + + @action + closeMenu = () => { + this.clearItems(); + this._display = false; + this._shouldDisplay = false; + } + + @action + openMenu = () => { + this._shouldDisplay; + this._display = true; + } + + @action + clearItems() { + this._items = []; + } + + render() { + if (!this._display) { + return null; + } + const style = this._yRelativeToTop ? { left: this._mouseX-150, top: this._mouseY-150 } : + { left: this._mouseX-150, bottom: this._mouseY+150 }; + + const contents = ( + <> + {this.menuItems} + </> + ); + // return ( + // <Measure offset onResize={action((r: any) => { this._width = r.offset.width; this._height = r.offset.height; })}> + // {({ measureRef }) => ( + // <div className="radialMenu-cont" style={style} ref={measureRef}> + // {contents} + // </div> + // ) + // } + // </Measure> + // ); + return ( + + <div className="radialMenu-cont" style={style}> + {contents} + </div> + + ); + } + + +}
\ No newline at end of file diff --git a/src/client/views/nodes/RadialMenuItem.tsx b/src/client/views/nodes/RadialMenuItem.tsx new file mode 100644 index 000000000..727d1c3be --- /dev/null +++ b/src/client/views/nodes/RadialMenuItem.tsx @@ -0,0 +1,96 @@ +import React = require("react"); +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { UndoManager } from "../../util/UndoManager"; + +library.add(faAngleRight); + +export interface OriginalMenuProps { + description: string; + event: (stuff?: any) => void; + undoable?: boolean; + icon: IconProp; //maybe should be optional (icon?) + closeMenu?: () => void; + min?: number; + max?:number; + selected:number; +} + + +export type RadialMenuProps = OriginalMenuProps; + +@observer +export class RadialMenuItem extends React.Component<RadialMenuProps & { selected?: boolean }> { + + componentDidMount = () =>{ + this.setcircle(); + } + + componentDidUpdate = () =>{ + this.setcircle(); + } + + handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => { + if ("event" in this.props) { + this.props.closeMenu && this.props.closeMenu(); + let batch: UndoManager.Batch | undefined; + if (this.props.undoable !== false) { + batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`); + } + await this.props.event({ x: e.clientX, y: e.clientY }); + batch && batch.end(); + } + } + + + setcircle(){ + let circlemin=0; + let circlemax=1 + this.props.min? circlemin=this.props.min:null; + this.props.max? circlemax=this.props.max:null; + if (document.getElementById("myCanvas")!==null){ + var c : any= document.getElementById("myCanvas"); + let color = "white" + switch(circlemin%3){ + case 1: + color = "#c2c2c5"; + break; + case 0: + color = "white"; + break; + case 2: + color = "lightgray"; + break; + } + if (circlemax%3===1 && circlemin===circlemax-1){ + color="#c2c2c5"; + } + console.log(this.props.selected,this.props.min) + + if (this.props.selected === this.props.min){ + console.log(this.props.selected,this.props.min) + color="#808080"; + + } + if (c.getContext){ + var ctx = c.getContext("2d"); + ctx.beginPath(); + ctx.arc(150, 150, 150, (circlemin/circlemax)*2*Math.PI, ((circlemin+1)/circlemax) * 2 * Math.PI); + ctx.arc(150, 150, 50, ((circlemin+1)/circlemax)*2*Math.PI, (circlemin/circlemax) * 2 * Math.PI,true); + ctx.fillStyle=color; + ctx.fill() + } + } + } + + render() { + return ( + <div className={"radialMenu-item" + (this.props.selected ? " radialMenu-itemSelected" : "")} onClick={this.handleEvent}> + <canvas id="myCanvas" height="300" width="300"> Your browser does not support the HTML5 canvas tag.</canvas> + </div> + ); + } +}
\ No newline at end of file |