diff --git a/docs/unit_definition_changelog.txt b/docs/unit_definition_changelog.txt index 8c6d3344081204df2d20a4b3511d8012ed99bbd7..944f16f9efe96f629ab4b41810625ea26b85f74d 100644 --- a/docs/unit_definition_changelog.txt +++ b/docs/unit_definition_changelog.txt @@ -132,3 +132,6 @@ iqb-aspect-definition@1.0.0 - new property: styling.firstLineColoringColor - new Element: MathTable - new Element: TextAreaMath + +4.3.0 +- Geometry: new property: trackedVariables diff --git a/projects/common/assets/i18n/de.json b/projects/common/assets/i18n/de.json index def9e7e3a7aafd94cc59cb7b3effff4a1ce0c84a..51e63ebbe39b9adf3998d8feedb9b5405d7abd06 100644 --- a/projects/common/assets/i18n/de.json +++ b/projects/common/assets/i18n/de.json @@ -10,5 +10,6 @@ "continue_command": "Teste 'Continue-Command'", "request_command": "Teste 'Get-Request-Command'", "confirm": "Bestätigen", - "cancel": "Abbrechen" + "cancel": "Abbrechen", + "geometry_reset": "neu anfangen" } diff --git a/projects/common/components/geometry/geometry.component.ts b/projects/common/components/geometry/geometry.component.ts index 351dbcd3472ba4cf5949d4d723feaa3313757a44..84b003d200fec657de27e3c34a5a19abf6f7a50e 100644 --- a/projects/common/components/geometry/geometry.component.ts +++ b/projects/common/components/geometry/geometry.component.ts @@ -1,7 +1,9 @@ import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnDestroy, Output, Renderer2 } from '@angular/core'; -import { debounceTime, Subject, Subscription } from 'rxjs'; +import { + BehaviorSubject, debounceTime, Subject, Subscription +} from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { ElementComponent } from 'common/directives/element-component.directive'; import { GeometryElement } from 'common/models/elements/geometry/geometry'; @@ -19,12 +21,11 @@ declare const GGBApplet: any; [style.width.px]="elementModel.width" [class.center]="this.elementModel.dimensions.isWidthFixed"> <button *ngIf="this.elementModel.showResetIcon" - class="reset-button" - mat-stroked-button + mat-stroked-button class="reset-button" (click)="reset()"> - <mat-icon class="reset-icon">autorenew</mat-icon>neu anfangen + <mat-icon class="reset-icon">autorenew</mat-icon>{{'geometry_reset' | translate }} </button> - <div [id]="elementModel.id" class="geogebra-applet"></div> + <div [id]="elementModel.id" class="geogebra-applet"></div> </div> <aspect-spinner [isLoaded]="isLoaded"></aspect-spinner> `, @@ -38,14 +39,14 @@ declare const GGBApplet: any; }) export class GeometryComponent extends ElementComponent implements AfterViewInit, OnDestroy { @Input() elementModel!: GeometryElement; - @Input() appDefinition!: string; + @Input() appDefinition: string | undefined; @Output() elementValueChanged = new EventEmitter<ValueChangeElement>(); - isLoaded: Subject<boolean> = new Subject(); - geoGebraApi!: any; + isLoaded: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); + geoGebraAPI!: any; private ngUnsubscribe = new Subject<void>(); - private geometryUpdated = new EventEmitter<void>(); + private geometryUpdated = new EventEmitter<void>(); // local subscription to be able to debounce private pageChangeSubscription: Subscription; constructor(public elementRef: ElementRef, @@ -54,19 +55,25 @@ export class GeometryComponent extends ElementComponent implements AfterViewInit private externalResourceService: ExternalResourceService) { super(elementRef); this.externalResourceService.initializeGeoGebra(this.renderer); + this.pageChangeSubscription = pageChangeService.pageChanged - .pipe( - takeUntil(this.ngUnsubscribe) - ).subscribe(() => this.loadApplet()); + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe(() => this.loadApplet()); + this.geometryUpdated - .pipe( - debounceTime(500), - takeUntil(this.ngUnsubscribe) - ).subscribe((): void => this.elementValueChanged - .emit({ - id: this.elementModel.id, - value: this.geoGebraApi.getBase64() - })); + .pipe(debounceTime(500), takeUntil(this.ngUnsubscribe)) + .subscribe(() => this.elementValueChanged.emit({ + id: this.elementModel.id, + value: { + appDefinition: this.geoGebraAPI.getBase64(), + variables: this.elementModel.trackedVariables + .map(variable => ({ id: variable, value: this.getVariableValue(variable) })) + } + })); + } + + private getVariableValue(name: string): string { + return this.geoGebraAPI.getValueString(name); } ngAfterViewInit(): void { @@ -78,8 +85,8 @@ export class GeometryComponent extends ElementComponent implements AfterViewInit this.pageChangeSubscription.unsubscribe(); this.externalResourceService.isGeoGebraLoaded() .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe((isLoaded: boolean) => { - if (isLoaded) this.initApplet(); + .subscribe((isGeoGebraLoaded: boolean) => { + if (isGeoGebraLoaded) this.initApplet(); }); } } @@ -95,11 +102,7 @@ export class GeometryComponent extends ElementComponent implements AfterViewInit } private initApplet(): void { - if (!this.appDefinition) { - console.error('Geogebra Applet definition not found.'); - return; - } - const params: any = { + const params = { id: this.elementModel.id, width: this.elementModel.dimensions.width - 4, // must be smaller than the container, otherwise scroll bars will be displayed height: this.elementModel.dimensions.height - 4, @@ -119,26 +122,26 @@ export class GeometryComponent extends ElementComponent implements AfterViewInit errorDialogsActive: true, showLogging: false, useBrowserForJS: false, - ggbBase64: this.appDefinition, + ggbBase64: this.appDefinition || this.elementModel.appDefinition, appletOnLoad: (geoGebraApi: any) => { - this.geoGebraApi = geoGebraApi; + this.geoGebraAPI = geoGebraApi; this.isLoaded.next(true); - this.geoGebraApi.registerAddListener(() => { + this.geoGebraAPI.registerAddListener(() => { this.geometryUpdated.emit(); }); - this.geoGebraApi.registerRemoveListener(() => { + this.geoGebraAPI.registerRemoveListener(() => { this.geometryUpdated.emit(); }); - this.geoGebraApi.registerUpdateListener(() => { + this.geoGebraAPI.registerUpdateListener(() => { this.geometryUpdated.emit(); }); - this.geoGebraApi.registerRenameListener(() => { + this.geoGebraAPI.registerRenameListener(() => { this.geometryUpdated.emit(); }); - this.geoGebraApi.registerClearListener(() => { + this.geoGebraAPI.registerClearListener(() => { this.geometryUpdated.emit(); }); - this.geoGebraApi.registerClientListener(() => { + this.geoGebraAPI.registerClientListener(() => { this.geometryUpdated.emit(); }); } @@ -148,6 +151,10 @@ export class GeometryComponent extends ElementComponent implements AfterViewInit applet.inject(this.elementModel.id); } + getGeometryObjects(): string[] { + return this.geoGebraAPI.getAllObjectNames(); + } + ngOnDestroy(): void { this.pageChangeSubscription.unsubscribe(); this.ngUnsubscribe.next(); diff --git a/projects/common/models/elements/element.ts b/projects/common/models/elements/element.ts index 9bcdc9cdb21019fe363e8294d85e9f66c1c8886c..5cfb793bd2558d68fae27f08bebfffe20f905255 100644 --- a/projects/common/models/elements/element.ts +++ b/projects/common/models/elements/element.ts @@ -93,7 +93,7 @@ export abstract class UIElement implements UIElementProperties { } } - setProperty(property: string, value: UIElementValue): void { + setProperty(property: string, value: unknown): void { if (Array.isArray(this[property])) { // keep array reference intact (this[property] as UIElementValue[]) .splice(0, (this[property] as UIElementValue[]).length, ...(value as UIElementValue[])); @@ -140,7 +140,7 @@ export abstract class UIElement implements UIElementProperties { abstract getDuplicate(): UIElement; } -export type InputElementValue = TextLabel[] | Hotspot[] | MathTableRow[] | string[] | string | number | boolean[] | +export type InputElementValue = TextLabel[] | Hotspot[] | MathTableRow[] | GeometryValue | string[] | string | number | boolean[] | boolean | null; export interface InputElementProperties extends UIElementProperties { @@ -315,3 +315,8 @@ export interface PlayerElement extends UIElement { } export type TooltipPosition = 'left' | 'right' | 'above' | 'below'; + +interface GeometryValue { + appDefinition: string; + variables: { id: string, value: any }[]; +} diff --git a/projects/common/models/elements/geometry/geometry.ts b/projects/common/models/elements/geometry/geometry.ts index 2af92425796009ac8b5b592eb58ae28fa1809f54..ddf270e82113e85aa6ad037049373ced9b2dfaf2 100644 --- a/projects/common/models/elements/geometry/geometry.ts +++ b/projects/common/models/elements/geometry/geometry.ts @@ -15,6 +15,7 @@ import { InstantiationEror } from 'common/util/errors'; export class GeometryElement extends UIElement implements PositionedUIElement, GeometryProperties { type: UIElementType = 'geometry'; appDefinition: string = ''; + trackedVariables: string[] = []; showResetIcon: boolean = true; enableUndoRedo: boolean = true; showToolbar: boolean = true; @@ -28,6 +29,7 @@ export class GeometryElement extends UIElement implements PositionedUIElement, G super(element); if (element && isValid(element)) { this.appDefinition = element.appDefinition; + this.trackedVariables = [...element.trackedVariables]; this.showResetIcon = element.showResetIcon; this.enableUndoRedo = element.enableUndoRedo; this.showToolbar = element.showToolbar; @@ -41,6 +43,7 @@ export class GeometryElement extends UIElement implements PositionedUIElement, G throw new InstantiationEror('Error at Geometry instantiation', element); } if (element?.appDefinition !== undefined) this.appDefinition = element.appDefinition; + if (element?.trackedVariables !== undefined) this.trackedVariables = [...element.trackedVariables]; if (element?.showResetIcon !== undefined) this.showResetIcon = element.showResetIcon; if (element?.enableUndoRedo !== undefined) this.enableUndoRedo = element.enableUndoRedo; if (element?.showToolbar !== undefined) this.showToolbar = element.showToolbar; @@ -84,6 +87,7 @@ export class GeometryElement extends UIElement implements PositionedUIElement, G export interface GeometryProperties extends UIElementProperties { appDefinition: string; + trackedVariables: string[]; showResetIcon: boolean; enableUndoRedo: boolean; showToolbar: boolean; @@ -97,6 +101,7 @@ export interface GeometryProperties extends UIElementProperties { function isValid(blueprint?: GeometryProperties): boolean { if (!blueprint) return false; return blueprint.appDefinition !== undefined && + blueprint.trackedVariables !== undefined && blueprint.showResetIcon !== undefined && blueprint.enableUndoRedo !== undefined && blueprint.showToolbar !== undefined && diff --git a/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts b/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts index 2d9d7480df1fe52269451706948bd1ed691bdd3d..055a0bb9048bed9e6ffebd7fdb3d713cce7f7748 100644 --- a/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts +++ b/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts @@ -23,7 +23,7 @@ export abstract class CanvasElementOverlay implements OnInit, OnDestroy { @Output() elementSelected = new EventEmitter(); @ViewChild('elementContainer', { read: ViewContainerRef, static: true }) private elementContainer!: ViewContainerRef; isSelected = false; - protected childComponent!: ComponentRef<ElementComponent | CompoundElementComponent>; + childComponent!: ComponentRef<ElementComponent | CompoundElementComponent>; private ngUnsubscribe = new Subject<void>(); temporaryHighlight: boolean = false; @@ -36,21 +36,12 @@ export abstract class CanvasElementOverlay implements OnInit, OnDestroy { this.childComponent = this.elementContainer.createComponent(this.element.getElementComponent()); this.childComponent.instance.elementModel = this.element; - if (this.childComponent.instance instanceof GeometryComponent) { - this.childComponent.instance.appDefinition = (this.element as GeometryElement).appDefinition; - } - this.childComponent.changeDetectorRef.detectChanges(); // this fires onInit, which initializes the FormControl if (this.childComponent.instance instanceof FormElementComponent) { (this.childComponent.instance as FormElementComponent).elementFormControl.setValue(this.element.value); } - // DropList keeps a special viewModel variable, which needs to be updated - // if (this.childComponent.instance instanceof DropListComponent) { - // (this.childComponent.instance as DropListComponent).viewModel = this.element.value as DragNDropValueObject[]; - // } - // Make children not clickable. This way the only relevant events are managed by the overlay. this.childComponent.location.nativeElement.style.pointerEvents = 'none'; diff --git a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts index de5306b9a67e6a6b55b1a35b412fb9236fa07b80..4fb1e41a10e0f08d7fd6b302183d1b12bcfb0c1f 100644 --- a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts +++ b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts @@ -105,8 +105,7 @@ export class ElementPropertiesPanelComponent implements OnInit, OnDestroy { } updateModel(property: string, - value: string | number | boolean | string[] | boolean[] | Hotspot[] | StateVariable | - TextLabel | TextLabel[] | LikertRowElement[] | MathTableRow[] | null, + value: unknown, isInputValid: boolean | null = true): void { if (isInputValid) { this.unitService.updateElementsProperty(this.selectedElements, property, value); diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html index 0cd3ca7f371fcffc43ce7d46e9fe4a3bb624d322..539911abbfd65f6925797f6a8a947e9dc39607b5 100644 --- a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html @@ -206,11 +206,6 @@ </button> </mat-form-field> - <mat-checkbox *ngIf="combinedProperties.showToolbar !== undefined" - [checked]="$any(combinedProperties.showToolbar)" - (change)="updateModel.emit({ property: 'showToolbar', value: $event.checked })"> - {{'propertiesPanel.showToolbar' | translate }} - </mat-checkbox> <mat-checkbox *ngIf="combinedProperties.showResetIcon !== undefined" [checked]="$any(combinedProperties.showResetIcon)" (change)="updateModel.emit({ property: 'showResetIcon', value: $event.checked })"> @@ -237,14 +232,34 @@ {{'propertiesPanel.showFullscreenButton' | translate }} </mat-checkbox> + <mat-checkbox *ngIf="combinedProperties.showToolbar !== undefined" + [checked]="$any(combinedProperties.showToolbar)" + (change)="updateModel.emit({ property: 'showToolbar', value: $event.checked })"> + {{'propertiesPanel.showToolbar' | translate }} + </mat-checkbox> <mat-form-field *ngIf="combinedProperties.customToolbar != null" matTooltip="{{'propertiesPanel.customToolbarHelp' | translate }}" appearance="fill"> <mat-label>{{'propertiesPanel.customToolbar' | translate }}</mat-label> - <input matInput [value]="$any(combinedProperties.customToolbar)" + <input matInput [disabled]="!combinedProperties.showToolbar" + [value]="$any(combinedProperties.customToolbar)" (input)="updateModel.emit({ property: 'customToolbar', value: $any($event.target).value })"> </mat-form-field> + <mat-form-field *ngIf="combinedProperties.trackedVariables !== undefined" + class="wide-form-field" appearance="fill"> + <mat-label>{{'propertiesPanel.trackedVariables' | translate }}</mat-label> + <mat-select multiple [ngModel]="combinedProperties.trackedVariables" + (ngModelChange)="setGeometryVariables($event)"> + <mat-select-trigger> + {{'propertiesPanel.trackedVariables' | translate }} ({{$any(combinedProperties.trackedVariables).length}}) + </mat-select-trigger> + <mat-option *ngFor="let variable of geometryObjects | async" [value]="variable"> + {{variable}} + </mat-option> + </mat-select> + </mat-form-field> + <mat-checkbox *ngIf="combinedProperties.enableModeSwitch !== undefined" [checked]="$any(combinedProperties.enableModeSwitch)" (change)="updateModel.emit({ property: 'enableModeSwitch', value: $event.checked })"> diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts index 565cf19ff7b7447a007139e566b55b862ed260a6..d81d214fffebf5874c3dbf826f71b32cddbe5334 100644 --- a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts @@ -1,16 +1,21 @@ import { - Component, EventEmitter, - Input, Output + ChangeDetectorRef, + Component, ComponentRef, EventEmitter, + Input, OnDestroy, OnInit, Output } from '@angular/core'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { InputElementValue, UIElement } from 'common/models/elements/element'; import { LikertRowElement } from 'common/models/elements/compound-elements/likert/likert-row'; import { FileService } from 'common/services/file.service'; import { CombinedProperties } from 'editor/src/app/components/properties-panel/element-properties-panel.component'; -import { firstValueFrom } from 'rxjs'; +import { + BehaviorSubject, firstValueFrom, of, Subject, switchMap +} from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; import { TextImageLabel, TextLabel } from 'common/models/elements/label-interfaces'; import { Hotspot } from 'common/models/elements/input-elements/hotspot-image'; import { StateVariable } from 'common/models/state-variable'; +import { GeometryComponent } from 'common/components/geometry/geometry.component'; import { UnitService } from '../../../services/unit.service'; import { SelectionService } from '../../../services/selection.service'; import { DialogService } from '../../../services/dialog.service'; @@ -20,7 +25,7 @@ import { DialogService } from '../../../services/dialog.service'; templateUrl: './element-model-properties.component.html', styleUrls: ['./element-model-properties.component.css'] }) -export class ElementModelPropertiesComponent { +export class ElementModelPropertiesComponent implements OnInit, OnDestroy { @Input() combinedProperties!: CombinedProperties; @Input() selectedElements: UIElement[] = []; @Output() updateModel = new EventEmitter<{ @@ -29,9 +34,37 @@ export class ElementModelPropertiesComponent { isInputValid?: boolean | null }>(); + geometryObjects: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]); + private ngUnsubscribe = new Subject<void>(); + constructor(public unitService: UnitService, public selectionService: SelectionService, - public dialogService: DialogService) { } + public dialogService: DialogService, + private cdr: ChangeDetectorRef) { } + + ngOnInit(): void { + this.initGeometryListener(); + } + + initGeometryListener(): void { + this.selectionService.selectedElements.pipe( + switchMap((selectedElements: UIElement[]) => { + if (selectedElements.length !== 1 || + selectedElements[0].type !== 'geometry') { + return of(false); + } + return (this.selectionService.selectedElementComponents[0].childComponent as ComponentRef<GeometryComponent>) + .instance.isLoaded + .pipe(takeUntil(this.ngUnsubscribe)); + })) + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe((isLoaded: boolean) => { + if (!isLoaded) return; + this.geometryObjects.next( + (this.selectionService.selectedElementComponents[0].childComponent as ComponentRef<GeometryComponent>) + .instance.getGeometryObjects()); + }); + } addListValue(property: string, value: string): void { this.updateModel.emit({ @@ -78,4 +111,16 @@ export class ElementModelPropertiesComponent { const appDefinition = await firstValueFrom(this.dialogService.showGeogebraAppDefinitionDialog()); if (appDefinition) this.updateModel.emit({ property: 'appDefinition', value: appDefinition }); } + + setGeometryVariables(variables: string[]) { + this.updateModel.emit({ + property: 'trackedVariables', + value: variables + }); + } + + ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } } diff --git a/projects/editor/src/app/services/unit.service.ts b/projects/editor/src/app/services/unit.service.ts index 229f4af2938cfe16da95c07f06d8d9c4ebf4231f..9f9279467ce27180144c902729dd1231dced890f 100644 --- a/projects/editor/src/app/services/unit.service.ts +++ b/projects/editor/src/app/services/unit.service.ts @@ -325,10 +325,7 @@ export class UnitService { this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); } - updateElementsProperty(elements: UIElement[], - property: string, - value: InputElementValue | LikertRowElement[] | Hotspot[] | StateVariable | - TextLabel | TextLabel[] | MathTableRow[] | ClozeDocument | null): void { + updateElementsProperty(elements: UIElement[], property: string, value: unknown): void { console.log('updateElementProperty', elements, property, value); elements.forEach(element => { if (property === 'id') { @@ -341,7 +338,7 @@ export class UnitService { this.handleClozeDocumentChange(element as ClozeElement, value as ClozeDocument); } else { element.setProperty(property, value); - if (element.type === 'geometry') this.geometryElementPropertyUpdated.next(element.id); + if (element.type === 'geometry' && property !== 'trackedVariables') this.geometryElementPropertyUpdated.next(element.id); if (element.type === 'math-table') this.mathTableElementPropertyUpdated.next(element.id); } }); diff --git a/projects/editor/src/assets/i18n/de.json b/projects/editor/src/assets/i18n/de.json index bfb345dcd8c275b74cb950dbdaa4d78cfbdfe91f..75f2f74a663c184da24e35211decdde1fb452368 100644 --- a/projects/editor/src/assets/i18n/de.json +++ b/projects/editor/src/assets/i18n/de.json @@ -227,7 +227,7 @@ "showResetIcon": "Zurücksetzen-Knopf anzeigen", "enableUndoRedo": "Schritt rückgängig/wiederherstellen", "appDefinition": "GGB-Definition", - "showToolbar": "Werkzeugleiste (muss in GGB aktiviert sein)", + "showToolbar": "Werkzeugleiste anzeigen", "showZoomButtons": "Zoom-Knöpfe anzeigen", "showFullscreenButton": "Vollbild-Knopf anzeigen", "enableShiftDragZoom": "Bewegen und Zoom erlauben",