Skip to content

Commit

Permalink
Isolate transformation editor commands to source within one display. (#…
Browse files Browse the repository at this point in the history
…4623)

* Isolate transformation editor commands to source within one display.

WIP: isolate drag on display.

WIP: Restore single drag handler.

WIP: Drag works.

WIP: isolate drag on display.

Isolate transformation editor commands to sources within one display.

Fix scale and crop editor commands.

Highlight source selector row when working with items in vertical display.

WIP: Restore single drag handler.

WIP: Drag works.

* Prevent user from dragging sources outside of canvas in dual output mode.

* WIP: calc drag boundary.

* Create drag boundaries.
  • Loading branch information
michelinewu committed Jul 19, 2023
1 parent 8a9e876 commit 770f0f7
Show file tree
Hide file tree
Showing 13 changed files with 300 additions and 111 deletions.
30 changes: 9 additions & 21 deletions app/components-react/editor/elements/DualOutputSourceSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ import { Services } from 'components-react/service-provider';
import { useVuex } from 'components-react/hooks';
import { useModule } from 'slap';
import { SourceSelectorModule } from './SourceSelector';
import { Tooltip } from 'antd';
import { $t } from 'services/i18n';

interface IDualOutputSourceSelector {
nodeId: string;
sceneId?: string;
}
export function DualOutputSourceSelector(p: IDualOutputSourceSelector) {
const { toggleVisibility, allSelected } = useModule(SourceSelectorModule);
const { toggleVisibility, makeActive } = useModule(SourceSelectorModule);
const { DualOutputService } = Services;

const v = useVuex(() => ({
Expand All @@ -26,12 +24,6 @@ export function DualOutputSourceSelector(p: IDualOutputSourceSelector) {
verticalActive: DualOutputService.views.activeDisplays.vertical,
}));

const showLinkIcon = useMemo(() => {
return (
allSelected([p.nodeId, v.verticalNodeId]) && v?.isHorizontalVisible && v?.isVerticalVisible
);
}, [allSelected, p.nodeId, v.verticalNodeId, v?.isHorizontalVisible, v?.isVerticalVisible]);

const showHorizontalToggle = useMemo(() => {
return !v?.isLoading && v.horizontalActive;
}, [!v?.isLoading, v.horizontalActive]);
Expand All @@ -42,26 +34,22 @@ export function DualOutputSourceSelector(p: IDualOutputSourceSelector) {

return (
<>
{showLinkIcon && (
<Tooltip
title={$t(
'You currently have the same source on both canvases selected. Please select the source in the canvas to edit it independently.',
)}
placement="bottomRight"
>
<i className="icon-link" style={{ color: 'var(--teal)' }} />
</Tooltip>
)}
{showHorizontalToggle && (
<i
onClick={() => toggleVisibility(p.nodeId)}
onClick={() => {
toggleVisibility(p.nodeId);
makeActive(p.nodeId);
}}
className={v.isHorizontalVisible ? 'icon-desktop' : 'icon-desktop-hide'}
/>
)}

{showVerticalToggle && (
<i
onClick={() => toggleVisibility(v.verticalNodeId)}
onClick={() => {
toggleVisibility(v.verticalNodeId);
makeActive(v.verticalNodeId);
}}
className={v.isVerticalVisible ? 'icon-phone-case' : 'icon-phone-case-hide'}
/>
)}
Expand Down
140 changes: 95 additions & 45 deletions app/components-react/editor/elements/SourceSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ export class SourceSelectorModule {

nodeRefs = {};

/**
* This property handles selection when expanding/collapsing folders
*/
callCameFromInsideTheHouse = false;
/**
* This property handles selection when clicking a dual output icon
*/
callCameFromIcon = false;

getTreeData(nodeData: ISourceMetadata[]) {
// recursive function for transforming SceneNode[] to a Tree format of Antd.Tree
Expand Down Expand Up @@ -159,24 +166,6 @@ export class SourceSelectorModule {
return this.selectionService.state.selectedIds.includes(node.id);
}

/**
* Determine if the nodes are selected in the source selector
* @remark
* In dual output mode, this is used to show the link icon in the source selector
* @param nodeIds -
* @returns
*/
allSelected(nodeIds: string[]): boolean {
const selectedIds = new Set(this.selectionService.views.globalSelection.getIds());

return nodeIds.reduce((selected: boolean, nodeId: string) => {
if (!selectedIds.has(nodeId)) {
selected = false;
}
return selected;
}, true);
}

determineIcon(isLeaf: boolean, sourceId: string) {
if (!isLeaf) {
return this.state.expandedFoldersIds.includes(sourceId)
Expand Down Expand Up @@ -333,36 +322,63 @@ export class SourceSelectorModule {
}
}

makeActive(info: { node: DataNode; nativeEvent: MouseEvent }) {
makeActive(info: { node: DataNode; nativeEvent: MouseEvent } | string) {
this.callCameFromInsideTheHouse = true;
let ids: string[] = [info.node.key as string];

if (info.nativeEvent.ctrlKey) {
ids = this.activeItemIds.concat(ids);
} else if (info.nativeEvent.shiftKey) {
// Logic for multi-select
const idx1 = this.nodeData.findIndex(
i => i.id === this.activeItemIds[this.activeItemIds.length - 1],
);
const idx2 = this.nodeData.findIndex(i => i.id === info.node.key);
const swapIdx = idx1 > idx2;
ids = this.nodeData
.map(i => i.id)
.slice(swapIdx ? idx2 : idx1, swapIdx ? idx1 + 1 : idx2 + 1);

/**
* For calls made from a dual output toggle,
* select only the source from the icon clicked
*/
if (typeof info === 'string') {
this.callCameFromIcon = true;
this.selectionService.views.globalSelection.reset();
this.selectionService.views.globalSelection.select([info]);
return;
}
if (this.dualOutputService.views.hasNodeMap(this.scene.id)) {
const updatedIds = new Set(ids);
ids.forEach(id => {
const dualOutputNodeId = this.dualOutputService.views.getDualOutputNodeId(id);
if (dualOutputNodeId && !updatedIds.has(dualOutputNodeId)) {
updatedIds.add(dualOutputNodeId);
}
});

ids = Array.from(updatedIds);
/**
* Skip multiselect logic when call is made from toggle
*/
if (!this.callCameFromIcon) {
let ids: string[] = [info.node.key as string];

if (info.nativeEvent.ctrlKey) {
ids = this.activeItemIds.concat(ids);
} else if (info.nativeEvent.shiftKey) {
// Logic for multi-select
const idx1 = this.nodeData.findIndex(
i => i.id === this.activeItemIds[this.activeItemIds.length - 1],
);
const idx2 = this.nodeData.findIndex(i => i.id === info.node.key);
const swapIdx = idx1 > idx2;
ids = this.nodeData
.map(i => i.id)
.slice(swapIdx ? idx2 : idx1, swapIdx ? idx1 + 1 : idx2 + 1);
}

/**
* In dual output mode with both displays active,
* clicking on the source selector selects both sources
*/
if (
this.dualOutputService.views.hasNodeMap(this.scene.id) &&
this.dualOutputService.views.activeDisplays.horizontal &&
this.dualOutputService.views.activeDisplays.vertical
) {
const updatedIds = new Set(ids);
ids.forEach(id => {
const dualOutputNodeId = this.dualOutputService.views.getDualOutputNodeId(id);
if (dualOutputNodeId && !updatedIds.has(dualOutputNodeId)) {
updatedIds.add(dualOutputNodeId);
}
});

ids = Array.from(updatedIds);
}
this.selectionService.views.globalSelection.select(ids);
}

this.selectionService.views.globalSelection.select(ids);
this.callCameFromIcon = false;
}

@mutation()
Expand Down Expand Up @@ -398,6 +414,9 @@ export class SourceSelectorModule {
this.nodeRefs[this.lastSelectedId]?.current?.scrollIntoView({ behavior: 'smooth' });
}

/**
* Used for actions initiated from the source selector such as sorting and selecting
*/
get activeItemIds() {
/* Because the source selector only works with either the horizontal
* or vertical node ids at one time, filter them in a dual output scene.
Expand All @@ -416,6 +435,37 @@ export class SourceSelectorModule {
return this.selectionService.state.selectedIds;
}

/**
* Used to highlight selected items in the source selector
*/
get selectionItemIds() {
/**
* When both displays are active, the source selector rows use the horizontal nodes to render.
* To highlight the source in the source selector when interacting with the source
* in the vertical display, convert the vertical node id to the horizontal node id.
*/
if (
this.dualOutputService.views.activeDisplays.horizontal &&
this.dualOutputService.views.activeDisplays.vertical
) {
const selectedIds = new Set(this.selectionService.state.selectedIds);

this.selectionService.state.selectedIds.map(id => {
const horizontalNodeId = this.dualOutputService.views.getHorizontalNodeId(id);
if (horizontalNodeId && !selectedIds.has(horizontalNodeId)) {
selectedIds.add(horizontalNodeId);
}
});

return Array.from(selectedIds);
}
// In all other cases, return all selected ids
return this.selectionService.state.selectedIds;
}

/**
* Used to get all items in the selection
*/
get activeItems() {
return this.selectionService.views.globalSelection.getItems();
}
Expand Down Expand Up @@ -561,7 +611,7 @@ function ItemsTree() {
const {
nodeData,
getTreeData,
activeItemIds,
selectionItemIds,
expandedFoldersIds,
selectiveRecordingEnabled,
showContextMenu,
Expand Down Expand Up @@ -597,7 +647,7 @@ function ItemsTree() {
>
{showTreeMask && <div className={styles.treeMask} data-name="treeMask" />}
<Tree
selectedKeys={activeItemIds}
selectedKeys={selectionItemIds}
expandedKeys={expandedFoldersIds}
onSelect={(selectedKeys, info) => makeActive(info)}
onExpand={(selectedKeys, info) => toggleFolder(info.node.key as string)}
Expand Down
8 changes: 8 additions & 0 deletions app/components-react/root/StudioEditor.m.less
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,11 @@
margin: auto;
text-align: center;
}

.toggle-error {
padding: 0px !important;
text-align: unset !important;
:global(.ant-message-notice-content) {
padding: 4px 16px;
}
}
24 changes: 23 additions & 1 deletion app/components-react/root/StudioEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ERenderingMode } from '../../../obs-api';
import { TDisplayType } from 'services/settings-v2';
import AutoProgressBar from 'components-react/shared/AutoProgressBar';
import { useSubscription } from 'components-react/hooks/useSubscription';
import { message } from 'antd';

export default function StudioEditor() {
const {
Expand All @@ -35,6 +36,7 @@ export default function StudioEditor() {
const studioModeRef = useRef<HTMLDivElement>(null);
const [studioModeStacked, setStudioModeStacked] = useState(false);
const [verticalPlaceholder, setVerticalPlaceholder] = useState(false);
const [messageActive, setMessageActive] = useState(false);
const studioModeTransitionName = useMemo(() => TransitionsService.getStudioTransitionName(), [
v.studioMode,
]);
Expand Down Expand Up @@ -121,7 +123,11 @@ export default function StudioEditor() {
}

moveInFlight = true;
EditorService.actions.return.handleMouseMove(getMouseEvent(event, display)).then(() => {
EditorService.actions.return.handleMouseMove(getMouseEvent(event, display)).then(stopMove => {
if (stopMove && !messageActive) {
console.log('----------------> showmessage');
showOutOfBoundsErrorMessage();
}
moveInFlight = false;

if (lastMoveEvent) {
Expand Down Expand Up @@ -164,6 +170,22 @@ export default function StudioEditor() {
};
}, []);

/**
* Show error message in dual output mode when the user
* attempts to drag a source out of the display.
* Prevent continual calls using the messageActive state variable.
*/
function showOutOfBoundsErrorMessage() {
setMessageActive(true);
message.error({
content: $t('Cannot move source outside canvas in Dual Output Mode.'),
duration: 2,
className: styles.toggleError,
});

setTimeout(() => setMessageActive(false), 2000);
}

return (
<div className={styles.mainContainer} ref={studioModeRef}>
{displayEnabled && (
Expand Down
1 change: 0 additions & 1 deletion app/services/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ export class ClipboardService extends StatefulService<IClipboardState> {
const dualOutputNodeId = this.dualOutputService.views.getDualOutputNodeId(
id,
activeSceneId,
true,
);
if (dualOutputNodeId && !ids.has(dualOutputNodeId)) {
ids.add(dualOutputNodeId);
Expand Down
10 changes: 2 additions & 8 deletions app/services/dual-output/dual-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,8 @@ class DualOutputViews extends ViewHandler<IDualOutputServiceState> {
);
}

getDualOutputNodeId(nodeId: string, sceneId?: string, checkAll: boolean = false) {
if (checkAll) {
return this.getHorizontalNodeId(nodeId, sceneId) ?? this.getVerticalNodeId(nodeId, sceneId);
}

return this.onlyVerticalDisplayActive
? this.getHorizontalNodeId(nodeId, sceneId)
: this.getVerticalNodeId(nodeId, sceneId);
getDualOutputNodeId(nodeId: string, sceneId?: string) {
return this.getHorizontalNodeId(nodeId, sceneId) ?? this.getVerticalNodeId(nodeId, sceneId);
}

getVerticalNodeIds(sceneId: string): string[] {
Expand Down
17 changes: 14 additions & 3 deletions app/services/editor-commands/commands/crop-items.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ModifyTransformCommand } from './modify-transform';
import { Selection } from 'services/selection';
import { $t } from 'services/i18n';
import { TDisplayType } from 'services/settings-v2';

export class CropItemsCommand extends ModifyTransformCommand {
/**
Expand All @@ -9,16 +10,26 @@ export class CropItemsCommand extends ModifyTransformCommand {
* @param crop The crop to apply
* @param position Optionally the items can be moved as well
*/
constructor(selection: Selection, private crop: Partial<ICrop>, private position?: IVec2) {
super(selection);
constructor(
selection: Selection,
private crop: Partial<ICrop>,
private position?: IVec2,
protected display?: TDisplayType,
) {
super(selection, display);
}

get description() {
return $t('Crop %{sourceName}', { sourceName: this.selection.getNodes()[0].name });
}

/**
* Resize items in the editor
* @remark In dual output mode, the selection may have both horizontal and vertical nodes
* but only the nodes in the display where the mouse event originated should be transformed
*/
modifyTransform() {
this.selection.getItems().forEach(item => {
this.selection.getItems(this.display).forEach(item => {
item.setTransform({
position: this.position,
crop: this.crop,
Expand Down
Loading

0 comments on commit 770f0f7

Please sign in to comment.