aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/documents/DocumentTypes.ts4
-rw-r--r--src/client/documents/Documents.ts47
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx164
-rw-r--r--src/client/views/collections/CollectionView.tsx5
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx34
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx4
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx20
7 files changed, 274 insertions, 4 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 1123bcac9..209b34f91 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -62,5 +62,7 @@ export enum CollectionViewType {
Pile = 'pileup',
StackedTimeline = 'stacked timeline',
NoteTaking = 'notetaking',
- Calendar = 'calendar'
+ Calendar = 'calendar',
+ Card = 'card'
+
}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 17cb6fef8..d12c61fd7 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1219,6 +1219,15 @@ export namespace Docs {
);
}
+ export function CardDeckDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
+ return InstanceFromProto(
+ Prototypes.get(DocumentType.COL),
+ new List(documents),
+ { backgroundColor: 'transparent', dropAction: dropActionType.move, _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Card },
+ id
+ );
+ }
+
export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Linear }, id);
}
@@ -1876,6 +1885,44 @@ export namespace DocUtils {
return newCollection;
}
}
+
+ export function spreadCards(docList: Doc[], x: number = 0, y: number = 0, spreadAngle: number = 30, radius: number = 100, create: boolean = true) {
+ const totalCards = docList.length;
+ const halfSpreadAngle = spreadAngle * 0.5;
+ const angleStep = spreadAngle / (totalCards - 1);
+
+ runInAction(() => {
+ docList.forEach((d, i) => {
+ DocUtils.iconify(d);
+ const angle = (-halfSpreadAngle + angleStep * i) * (Math.PI / 180); // Convert degrees to radians
+ d.x = x + Math.cos(angle) * radius;
+ d.y = y + Math.sin(angle) * radius;
+ d.rotation = angle; // Assuming 'd.rotation' sets the rotation, adjust accordingly
+ d._timecodeToShow = undefined;
+ });
+ });
+
+ if (create) {
+ const newCollection = Docs.Create.CardDeckDocument(docList, {
+ title: 'card-spread',
+ _freeform_noZoom: true,
+ x: x - radius,
+ y: y - radius,
+ _width: radius * 2,
+ _height: radius * 2,
+ dragWhenActive: true,
+ _layout_fitWidth: false
+ });
+ // Adjust position based on the collection's dimensions if needed
+ newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - radius;
+ newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - radius;
+ newCollection._width = newCollection._height = radius * 2;
+ return newCollection;
+ }
+ }
+
+
+
export function makeIntoPortal(doc: Doc, layoutDoc: Doc, allLinks: Doc[]) {
const portalLink = allLinks.find(d => d.link_anchor_1 === doc && d.link_relationship === 'portal to:portal from');
if (!portalLink) {
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
new file mode 100644
index 000000000..feb9e61cc
--- /dev/null
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -0,0 +1,164 @@
+import { action, computed, IReactionDisposer, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast } from '../../../fields/Doc';
+import { ScriptField } from '../../../fields/ScriptField';
+import { NumCast, StrCast } from '../../../fields/Types';
+import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { DocUtils } from '../../documents/Documents';
+import { SelectionManager } from '../../util/SelectionManager';
+import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { OpenWhere } from '../nodes/DocumentView';
+import { computePassLayout, computeStarburstLayout, computeCardDeckLayout } from './collectionFreeForm';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import './CollectionPileView.scss';
+import { CollectionSubView } from './CollectionSubView';
+import { dropActionType } from '../../util/DragManager';
+
+@observer
+export class CollectionCardView extends CollectionSubView() {
+ _originalChrome: any = '';
+ _disposers: { [name: string]: IReactionDisposer } = {};
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ }
+
+ componentDidMount() {
+ if (this.layoutEngine() !== computePassLayout.name && this.layoutEngine() !== computeCardDeckLayout.name) {
+ this.Document._freeform_cardEngine = computePassLayout.name;
+ }
+ this._originalChrome = this.layoutDoc._chromeHidden;
+ this.layoutDoc._chromeHidden = true;
+ }
+ componentWillUnmount() {
+ this.layoutDoc._chromeHidden = this._originalChrome;
+ Object.values(this._disposers).forEach(disposer => disposer?.());
+ }
+
+ layoutEngine = () => StrCast(this.Document._freeform_cardEngine);
+
+ @undoBatch
+ addCardDoc = (doc: Doc | Doc[]) => {
+ (doc instanceof Doc ? [doc] : doc).map(d => DocUtils.iconify(d));
+ return this._props.addDocument?.(doc) || false;
+ };
+
+ @undoBatch
+ removeCardDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
+ (doc instanceof Doc ? [doc] : doc).forEach(d => Doc.deiconifyView(d));
+ const ret = this._props.moveDocument?.(doc, targetCollection, addDoc) || false;
+ if (ret && !DocListCast(this.dataDoc[this.fieldKey ?? 'data']).length) this.DocumentView?.()._props.removeDocument?.(this.Document);
+ return ret;
+ };
+
+ @computed get toggleIcon() {
+ return ScriptField.MakeScript('documentView.iconify()', { documentView: 'any' });
+ }
+ @computed get contentEvents() {
+ const isCard = this.layoutEngine() === computeCardDeckLayout.name;
+ return this._props.isContentActive() && isCard ? undefined : 'none';
+ }
+
+ // returns the contents of the cardSpread in a CollectionFreeFormView
+ @computed get contents() {
+ return (
+ <div className="collectionPileView-innards" style={{ pointerEvents: this.contentEvents }}>
+ <CollectionFreeFormView
+ {...this._props} //
+ layoutEngine={this.layoutEngine}
+ addDocument={this.addCardDoc}
+ moveDocument={this.removeCardDoc}
+ // pile children never have their contents active, but will be document active whenever the entire pile is.
+ childContentsActive={returnFalse}
+ childDocumentsActive={this._props.isDocumentActive}
+ childDragAction={dropActionType.move}
+ childClickScript={this.toggleIcon}
+ />
+ </div>
+ );
+ }
+
+ // // toggles the pileup between starburst to compact
+ // toggleStarburst = action(() => {
+ // this.layoutDoc._freeform_scale = undefined;
+ // if (this.layoutEngine() === computeStarburstLayout.name) {
+ // if (NumCast(this.layoutDoc._width) !== NumCast(this.Document._starburstDiameter, 500)) {
+ // this.Document._starburstDiameter = NumCast(this.layoutDoc._width);
+ // }
+ // const defaultSize = 110;
+ // this.Document.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width) / 2 - NumCast(this.layoutDoc._freeform_pileWidth, defaultSize) / 2;
+ // this.Document.y = NumCast(this.Document.y) + NumCast(this.layoutDoc._height) / 2 - NumCast(this.layoutDoc._freeform_pileHeight, defaultSize) / 2;
+ // this.layoutDoc._width = NumCast(this.layoutDoc._freeform_pileWidth, defaultSize);
+ // this.layoutDoc._height = NumCast(this.layoutDoc._freeform_pileHeight, defaultSize);
+ // DocUtils.pileup(this.childDocs, undefined, undefined, NumCast(this.layoutDoc._width) / 2, false);
+ // this.layoutDoc._freeform_panX = 0;
+ // this.layoutDoc._freeform_panY = -10;
+ // this.Document._freeform_pileEngine = computePassLayout.name;
+ // } else {
+ // const defaultSize = NumCast(this.Document._starburstDiameter, 400);
+ // this.Document.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width) / 2 - defaultSize / 2;
+ // this.Document.y = NumCast(this.Document.y) + NumCast(this.layoutDoc._height) / 2 - defaultSize / 2;
+ // this.layoutDoc._freeform_pileWidth = NumCast(this.layoutDoc._width);
+ // this.layoutDoc._freeform_pileHeight = NumCast(this.layoutDoc._height);
+ // this.layoutDoc._freeform_panX = this.layoutDoc._freeform_panY = 0;
+ // this.layoutDoc._width = this.layoutDoc._height = defaultSize;
+ // this.layoutDoc.background;
+ // this.Document._freeform_pileEngine = computeStarburstLayout.name;
+ // }
+ // });
+
+ // for dragging documents out of the pileup view
+ _undoBatch: UndoManager.Batch | undefined;
+ pointerDown = (e: React.PointerEvent) => {
+ let dist = 0;
+ setupMoveUpEvents(
+ this,
+ e,
+ (e: PointerEvent, down: number[], delta: number[]) => {
+ if (this.layoutEngine() === 'pass' && this.childDocs.length && e.shiftKey) {
+ dist += Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1]);
+ if (dist > 100) {
+ if (!this._undoBatch) {
+ this._undoBatch = UndoManager.StartBatch('layout pile');
+ }
+ const doc = this.childDocs[0];
+ doc.x = e.clientX;
+ doc.y = e.clientY;
+ this._props.addDocTab(doc, OpenWhere.inParentFromScreen) && (this._props.removeDocument?.(doc) || false);
+ dist = 0;
+ }
+ }
+ return false;
+ },
+ () => {
+ this._undoBatch?.end();
+ this._undoBatch = undefined;
+ },
+ emptyFunction,
+ e.shiftKey && this.layoutEngine() === computePassLayout.name,
+ this.layoutEngine() === computePassLayout.name && e.shiftKey
+ ); // this sets _doubleTap
+ };
+
+ // onClick for toggling the pileup view
+ // @undoBatch
+ // onClick = (e: React.MouseEvent) => {
+ // if (e.button === 0) {
+ // SelectionManager.DeselectAll();
+ // this.toggleStarburst();
+ // e.stopPropagation();
+ // }
+ // };
+
+ render() {
+ return (
+ <div className={`collectionPileView`}
+ // onClick={this.onClick}
+ onPointerDown={this.pointerDown} style={{ width: this._props.PanelWidth(), height: '100%' }}>
+ {this.contents}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 18eb4dd1f..b7805bf3f 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -33,6 +33,8 @@ import { CollectionLinearView } from './collectionLinear';
import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView';
+import { CollectionCardView } from './CollectionCardDeckView';
+
export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
@@ -134,6 +136,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
case CollectionViewType.Masonry: return <CollectionStackingView key="collview" {...props} />;
case CollectionViewType.Time: return <CollectionTimeView key="collview" {...props} />;
case CollectionViewType.Grid: return <CollectionGridView key="collview" {...props} />;
+ case CollectionViewType.Card: return <CollectionCardView key="collview" {...props} />;
+
+
}
};
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index b8c0967c1..e972f44f1 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -126,6 +126,40 @@ export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc
return normalizeResults(burstDiam, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
}
+export function computeCardDeckLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
+ const docMap = new Map<string, PoolData>();
+ const spreadWidth = Math.min(panelDim[0], childPairs.length * 50); // Total width of the spread
+ const startX = -(spreadWidth / 2); // Starting X position
+ const fanAngle = 5; // Angle in degrees for fanning out cards
+ const baseZIndex = 1000; // Base Z-index to ensure cards are stacked in order
+
+ childPairs.forEach(({ layout, data }, i) => {
+ const aspect = NumCast(layout._height) / NumCast(layout._width);
+ const docSize = Math.min(400, NumCast(layout._width)) * NumCast(pivotDoc._starburstDocScale, 1);
+ const posX = startX + (spreadWidth / childPairs.length) * i;
+ const posY = 0; // Adjust if you want to change the vertical alignment
+ const rotation = (i - (childPairs.length / 2)) * fanAngle; // Calculate rotation for fanning effect
+
+ docMap.set(layout[Id], {
+ x: posX,
+ y: posY,
+ width: docSize,
+ height: docSize * aspect,
+ zIndex: baseZIndex + i, // Increment Z-index for each card to stack them correctly
+ rotation: rotation, // Optional: Add this if you want to rotate elements for a fanned effect
+ pair: { layout, data },
+ replica: '',
+ color: 'white',
+ backgroundColor: 'white',
+ transition: 'all 0.3s',
+ });
+ });
+
+ // This is a placeholder for the divider object and may need to be adjusted based on actual usage
+ const divider = { type: 'div', color: 'transparent', x: -panelDim[0] / 2, y: -panelDim[1] / 2, width: 15, height: 15, payload: undefined };
+ return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
+}
+
export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
const docMap = new Map<string, PoolData>();
const fieldKey = 'data';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 1fd453e96..f9fe306fa 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -49,7 +49,7 @@ import { CollectionSubView } from '../CollectionSubView';
import { TreeViewType } from '../CollectionTreeView';
import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid';
import { CollectionFreeFormInfoUI } from './CollectionFreeFormInfoUI';
-import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
+import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult, computeCardDeckLayout } from './CollectionFreeFormLayoutEngines';
import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannableContents';
import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
import './CollectionFreeFormView.scss';
@@ -1383,6 +1383,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
case computeTimelineLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
case computePivotLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
case computeStarburstLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeStarburstLayout) };
+ case computeCardDeckLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeCardDeckLayout) };
+
}
return { newPool, computedElementData: this.doFreeformLayout(newPool) };
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b913e05ad..d0ac5f6db 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -383,6 +383,18 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
this.hideMarquee();
});
+ @undoBatch
+ spreadCards = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
+ const selected = this.marqueeSelect(false);
+ SelectionManager.DeselectAll();
+ selected.forEach(d => this._props.removeDocument?.(d));
+ const newCollection = DocUtils.spreadCards(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2)!;
+ this._props.addDocument?.(newCollection);
+ this._props.selectDocuments([newCollection]);
+ MarqueeOptionsMenu.Instance.fadeOut(true);
+ this.hideMarquee();
+ });
+
/**
* This triggers the TabDocView.PinDoc method which is the universal method
* used to pin documents to the currently active presentation trail.
@@ -508,6 +520,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
@action
marqueeCommand = (e: KeyboardEvent) => {
+
if (this._commandExecuted || (e as any).propagationIsStopped) {
return;
}
@@ -518,7 +531,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
this.delete(e, e.key === 'h');
e.stopPropagation();
}
- if ('ctsSpg'.indexOf(e.key) !== -1) {
+ if ('ctsSpga'.indexOf(e.key) !== -1) {
+
this._commandExecuted = true;
e.stopPropagation();
e.preventDefault();
@@ -526,7 +540,9 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
if (e.key === 'g') this.collection(e, true);
if (e.key === 'c' || e.key === 't') this.collection(e);
if (e.key === 's' || e.key === 'S') this.summary(e);
- if (e.key === 'p') this.pileup(e);
+ if (e.key === 'p') this.pileup(e)
+ if (e.key === 'a') this.spreadCards(e);
+
this.cleanupInteractions(false);
}
if (e.key === 'r' || e.key === ' ') {