diff --git a/pxtblocks/compiler/typeChecker.ts b/pxtblocks/compiler/typeChecker.ts index f7e3f378fa58..3156ec208aa4 100644 --- a/pxtblocks/compiler/typeChecker.ts +++ b/pxtblocks/compiler/typeChecker.ts @@ -353,7 +353,7 @@ export function returnType(e: Environment, b: Blockly.Block): Point { return find(b.p); } - if (b.type == "variables_get") + if (b.type == "variables_get" || b.type == "variables_get_reporter") return find(lookup(e, b, b.getField("VAR").getText()).type); if (b.type == "function_call_output") { @@ -493,9 +493,9 @@ function getReturnTypeOfFunctionCall(e: Environment, call: Blockly.Block) { // Basic type unification routine; easy, because there's no structural types. // FIXME: Generics are not supported function unify(t1: string, t2: string) { - if (t1 == null || t1 === "Array" && isArrayType(t2)) + if (t1 == null || isNullishType(t1) || t1 === "Array" && isArrayType(t2)) return t2; - else if (t2 == null || t2 === "Array" && isArrayType(t1)) + else if (t2 == null || isNullishType(t2) || t2 === "Array" && isArrayType(t1)) return t1; else if (t1 == t2) return t1; @@ -503,6 +503,10 @@ function unify(t1: string, t2: string) { throw new Error("cannot mix " + t1 + " with " + t2); } +function isNullishType(type: string) { + return type === "null" || type === "undefined"; +} + function isArrayType(type: string) { return type && (type.indexOf("[]") !== -1 || type == "Array"); } diff --git a/pxtlib/music.ts b/pxtlib/music.ts index 6a3f4045f26c..8e21dd169105 100644 --- a/pxtlib/music.ts +++ b/pxtlib/music.ts @@ -261,8 +261,20 @@ namespace pxt.assets.music { } export function decodeSongFromHex(hex: string) { - const bytes = pxt.U.fromHex(hex); + let isBase64 = false; + for (let i = 0; i < hex.length; i++) { + const c = hex[i]; + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + isBase64 = true; + break; + } + } + if (isBase64) { + return decodeSong(pxt.Util.stringToUint8Array(atob(hex))); + } + + const bytes = pxt.U.fromHex(hex); return decodeSong(bytes); } diff --git a/pxtsim/sound/audioSource.ts b/pxtsim/sound/audioSource.ts index 8fa6c37ba798..dd8b76bc5386 100644 --- a/pxtsim/sound/audioSource.ts +++ b/pxtsim/sound/audioSource.ts @@ -3,10 +3,11 @@ namespace pxsim.AudioContextManager { static activeSources: AudioSource[] = []; static stopAll() { - for (const source of AudioSource.activeSources) { + const activeSources = AudioSource.activeSources; + AudioSource.activeSources = []; + for (const source of activeSources) { source.dispose(); } - AudioSource.activeSources = []; } protected vca: GainNode; diff --git a/tests/blocklycompiler-test/baselines/function_nullish_return.ts b/tests/blocklycompiler-test/baselines/function_nullish_return.ts new file mode 100644 index 000000000000..47002d6ae9bd --- /dev/null +++ b/tests/blocklycompiler-test/baselines/function_nullish_return.ts @@ -0,0 +1,26 @@ +function sprite_first () { + for (let localSprite1 of testTypes.allSprites()) { + localSprite1.setPosition(0, 0) + return localSprite1 + } + return testTypes.undefinedValue() +} +function undefined_first () { + if (true) { + return testTypes.undefinedValue() + } + for (let localSprite2 of testTypes.allSprites()) { + localSprite2.setPosition(0, 0) + return localSprite2 + } +} +let undefinedFirst: Sprite = null +let spriteFirst: Sprite = null +let spriteFirstReferenced = spriteFirst == spriteFirst +let undefinedFirstReferenced = undefinedFirst == undefinedFirst +spriteFirst = sprite_first() +undefinedFirst = undefined_first() +game.onUpdate(function () { + spriteFirst = sprite_first() + undefinedFirst = undefined_first() +}) \ No newline at end of file diff --git a/tests/blocklycompiler-test/cases/function_nullish_return.blocks b/tests/blocklycompiler-test/cases/function_nullish_return.blocks new file mode 100644 index 000000000000..57a247a8ac75 --- /dev/null +++ b/tests/blocklycompiler-test/cases/function_nullish_return.blocks @@ -0,0 +1,209 @@ + + + spriteFirst + undefinedFirst + spriteFirstReferenced + undefinedFirstReferenced + localSprite1 + localSprite2 + + + + + spriteFirstReferenced + + + EQ + + + spriteFirst + + + + + spriteFirst + + + + + + + undefinedFirstReferenced + + + EQ + + + undefinedFirst + + + + + undefinedFirst + + + + + + + spriteFirst + + + + + + + + undefinedFirst + + + + + + + + + + + + + + + + + + spriteFirst + + + + + + + + undefinedFirst + + + + + + + + + + + + + sprite_first + + + + + localSprite1 + + + + + + + + + + localSprite1 + + + + + 0 + + + + + 0 + + + + + + + + localSprite1 + + + + + + + + + + + + + + + + + + + + undefined_first + + + + + TRUE + + + + + + + + + + + + + + + localSprite2 + + + + + + + + + + localSprite2 + + + + + 0 + + + + + 0 + + + + + + + + localSprite2 + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/blocklycompiler-test/test-library/spritekind.ts b/tests/blocklycompiler-test/test-library/spritekind.ts index 4e7b34257f00..91b38135f046 100644 --- a/tests/blocklycompiler-test/test-library/spritekind.ts +++ b/tests/blocklycompiler-test/test-library/spritekind.ts @@ -60,6 +60,18 @@ class Sprite { } } +namespace testTypes { + //% blockId=test_all_sprites block="all sprites" + export function allSprites(): Sprite[] { + return []; + } + + //% blockId=test_undefined_value block="undefined" + export function undefinedValue(): undefined { + return undefined; + } +} + //% blockNamespace="statusbars" //% blockGap=8 class StatusBarSprite extends Sprite { diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts index 34c6a67d5529..63755d02d6c2 100644 --- a/tests/blocklycompiler-test/test.spec.ts +++ b/tests/blocklycompiler-test/test.spec.ts @@ -505,6 +505,10 @@ describe("blockly compiler", function () { blockTestAsync("function_output").then(done, done); }); + it("should ignore nullish types when inferring function return types", (done: () => void) => { + blockTestAsync("function_nullish_return").then(done, done); + }); + it("should output a return type for recursive functions", (done: () => void) => { blockTestAsync("function_recursion").then(done, done); }); diff --git a/theme/music-editor/MusicEditor.less b/theme/music-editor/MusicEditor.less index 92f9a6a23145..6c1a79e3ba17 100644 --- a/theme/music-editor/MusicEditor.less +++ b/theme/music-editor/MusicEditor.less @@ -7,6 +7,7 @@ @import "Track"; @import "TrackSelector"; @import "Workspace"; +@import "gallery"; @music-editor-header-color: #e30fc0; // Matches block color @music-editor-header-border-color: #c10da3; // Matches block color diff --git a/theme/music-editor/gallery.less b/theme/music-editor/gallery.less new file mode 100644 index 000000000000..42c94ab0d4c4 --- /dev/null +++ b/theme/music-editor/gallery.less @@ -0,0 +1,97 @@ +.image-editor-gallery.song { + display: flex; + flex-direction: column; + align-content: center; + justify-content: unset; + padding: 0.5rem; + + gap: 0.5rem; +} + +.song-gallery-item { + display: flex; + flex-direction: row; + align-items: center; + padding: 0.5rem; + width: 100%; + height: 4rem; + + background-color: var(--pxt-neutral-background1); + color: var(--pxt-neutral-foreground1); + border-radius: 0.25rem; + max-width: 500px; + gap: 1rem; + + .song-gallery-item-name { + flex-shrink: 0; + width: 7rem; + font-weight: 600; + font-size: 18px; + + overflow-x: hidden; + overflow-y: hidden; + text-overflow: ellipsis; + + height: 3rem; + line-height: 3rem; + } + + .song-gallery-item-preview { + flex-grow: 1; + flex-shrink: 1; + position: relative; + overflow-x: hidden; + overflow-y: hidden; + height: 3rem; + + img { + image-rendering: pixelated; + width: 100%; + height: 3rem; + object-fit: fill; + } + } + + .common-button.play-button { + flex-shrink: 0; + width: 3rem; + height: 3rem; + padding: 0; + border-radius: 2rem; + background-color: var(--pxt-neutral-foreground2); + color: var(--pxt-neutral-background2); + + .fas.fa-play, .fas.fa-stop { + margin: 0 0 0 0.25rem; + color: var(--pxt-neutral-background2); + } + + .fas.fa-stop { + margin: 0; + } + + &:focus-visible:after { + outline: solid var(--pxt-focus-border) 3px; + outline-offset: 5px; + } + } + + .common-button.song-gallery-item-button { + flex-grow: 1; + height: 3rem; + padding: 0; + background-color: var(--pxt-neutral-background1); + + .common-button-label { + display: flex; + flex-direction: row; + height: 3rem; + align-items: center; + } + + &:focus-visible:after { + outline: solid var(--pxt-focus-border) 3px; + outline-offset: 5px; + } + } +} \ No newline at end of file diff --git a/webapp/src/components/ImageFieldEditor.tsx b/webapp/src/components/ImageFieldEditor.tsx index 1e9b6ff309d5..774e494922d4 100644 --- a/webapp/src/components/ImageFieldEditor.tsx +++ b/webapp/src/components/ImageFieldEditor.tsx @@ -13,6 +13,8 @@ import { MusicFieldEditor } from "./MusicFieldEditor"; import { classList } from "../../../react-common/components/util"; import { FocusTrap, FocusTrapRegion } from "../../../react-common/components/controls/FocusTrap"; import { PianoRollAssetEditor } from "./PianoRollFieldEditor"; +import { SongGallery } from "./SongGallery"; +import { stopPlayback } from "./musicEditor/playback"; export interface ImageFieldEditorProps { singleFrame: boolean; @@ -81,7 +83,7 @@ export class ImageFieldEditor extends React.Component extends React.Component )} -