diff --git a/docs/blog.md b/docs/blog.md
index 477787343a02..5bc0854d954c 100644
--- a/docs/blog.md
+++ b/docs/blog.md
@@ -1,5 +1,13 @@
# Microsoft MakeCode Blog
+## [MakeCode Code Evaluation Tool Beta](/blog/tools/code-eval-tool)
+
+January 14th, 2025 by [Jaqster](https://github.com/jaqster)
+
+The year 2025 is already off to a great start with the Beta release of a new tool for Teachers! **MakeCode Code Evaluation** is an online tool for teachers to help them understand and evaluate student programs.
+
+**[Continue reading this blog post](/blog/tools/code-eval-tool)**
+
## [MakeCode for the micro:bit 2024 Update](/blog/microbit/2024-update)
September 4, 2024 by [Jaqster](https://github.com/jaqster)
diff --git a/docs/blog/SUMMARY.md b/docs/blog/SUMMARY.md
index ce7b3f093d93..c43b671e45a4 100644
--- a/docs/blog/SUMMARY.md
+++ b/docs/blog/SUMMARY.md
@@ -1,6 +1,7 @@
# Microsoft MakeCode Blog
* [Blog](/blog)
+ * [MakeCode Code Evaluation Tool Beta](/blog/tools/code-eval-tool)
* [MakeCode for the micro:bit 2024 Update](/blog/microbit/2024-update)
* [MakeCode Arcade - now more ways to play!](/blog/arcade/arcade-on-microbit-xbox)
* [MakeCode Minecraft 2023 Update](/blog/minecraft/2023-release)
diff --git a/docs/blog/tools/code-eval-tool.md b/docs/blog/tools/code-eval-tool.md
new file mode 100644
index 000000000000..10fe9996e191
--- /dev/null
+++ b/docs/blog/tools/code-eval-tool.md
@@ -0,0 +1,20 @@
+# MakeCode Code Evaluation Tool Beta
+
+**Posted on January 14th, 2025 by [Jaqster](https://github.com/jaqster)**
+
+The year 2025 is already off to a great start with the Beta release of a new tool for Teachers! MakeCode [Code Evaluation](https://makecode.microbit.org/--eval) is an online tool for teachers to help them understand and evaluate student programs. We’ve heard from many teachers over the years that one of the most onerous parts of their job is assessing and evaluating student projects. With many teachers responsible for over 100 students, taking 10 minutes to understand, evaluate and give feedback for each program their students turn in means over 16 hours of review and grading!
+
+The Code Evaluation tool is our first effort to help teachers with this process. We have released it for use with MakeCode for micro:bit projects initially.
+
+Watch this short video to see how to get started:
+
+https://youtu.be/7pA2EbG4QPk
+
+More documentation can be found on the [Code Evaluation Tool]([https://makecode.microbit.org/code-eval-tool) info page.
+
+The Code Evaluation tool has been released as a Beta – meaning that it’s still a work-in-progress. We have ideas on how we could improve it, but we want to hear from you! Please try it out and click on the Give Feedback button to let us know your thoughts - https://makecode.microbit.org/--eval.
+
+Happy Making, Coding, and Evaluating!
+
+
+The MakeCode Team
diff --git a/package.json b/package.json
index 703299575e67..c3c47a472ab2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pxt-core",
- "version": "11.3.10",
+ "version": "11.3.11",
"description": "Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors",
"keywords": [
"TypeScript",
diff --git a/react-common/components/controls/Button.tsx b/react-common/components/controls/Button.tsx
index 19497d70c712..8ab0d2abfb87 100644
--- a/react-common/components/controls/Button.tsx
+++ b/react-common/components/controls/Button.tsx
@@ -28,6 +28,7 @@ export interface ButtonViewProps extends ContainerProps {
export interface ButtonProps extends ButtonViewProps {
onClick: () => void;
+ onRightClick?: () => void;
onBlur?: () => void;
onKeydown?: (e: React.KeyboardEvent) => void;
}
@@ -49,6 +50,7 @@ export const Button = (props: ButtonProps) => {
ariaPressed,
role,
onClick,
+ onRightClick,
onKeydown,
onBlur,
buttonRef,
@@ -78,7 +80,8 @@ export const Button = (props: ButtonProps) => {
);
let clickHandler = (ev: React.MouseEvent) => {
- if (onClick) onClick();
+ if (onRightClick && ev.button !== 0) onRightClick();
+ else if (onClick) onClick();
if (href) window.open(href, target || "_blank", "noopener,noreferrer")
ev.stopPropagation();
ev.preventDefault();
diff --git a/react-common/components/controls/CarouselNav.tsx b/react-common/components/controls/CarouselNav.tsx
new file mode 100644
index 000000000000..4627d073ade6
--- /dev/null
+++ b/react-common/components/controls/CarouselNav.tsx
@@ -0,0 +1,62 @@
+import { classList } from "../util";
+import { Button } from "./Button";
+
+export interface CarouselNavProps {
+ pages: number;
+ selected: number;
+ maxDisplayed?: number;
+ onPageSelected: (page: number) => void;
+}
+
+export const CarouselNav = (props: CarouselNavProps) => {
+ const { pages, selected, maxDisplayed, onPageSelected } = props;
+
+ const displayedPages: number[] = [];
+ let start = 0;
+ let end = pages;
+
+ if (maxDisplayed) {
+ start = Math.min(
+ Math.max(0, selected - (maxDisplayed >> 1)),
+ Math.max(0, start + pages - maxDisplayed)
+ );
+ end = Math.min(start + maxDisplayed, pages);
+ }
+
+ for (let i = start; i < end; i++) {
+ displayedPages.push(i);
+ }
+
+ return (
+
+
onPageSelected(selected - 1)}
+ disabled={selected === 0}
+ />
+
+ {displayedPages.map(page =>
+ onPageSelected(page)}>
+ onPageSelected(page)}
+ label={
+
+ }
+ />
+
+ )}
+
+ onPageSelected(selected + 1)}
+ disabled={selected === pages - 1}
+ />
+
+ )
+}
\ No newline at end of file
diff --git a/react-common/components/controls/FocusTrap.tsx b/react-common/components/controls/FocusTrap.tsx
deleted file mode 100644
index 39d2546c9073..000000000000
--- a/react-common/components/controls/FocusTrap.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import * as React from "react";
-import { classList, nodeListToArray, findNextFocusableElement, focusLastActive } from "../util";
-
-export interface FocusTrapProps extends React.PropsWithChildren<{}> {
- onEscape: () => void;
- id?: string;
- className?: string;
- arrowKeyNavigation?: boolean;
- dontStealFocus?: boolean;
- includeOutsideTabOrder?: boolean;
- dontRestoreFocus?: boolean;
-}
-
-export const FocusTrap = (props: FocusTrapProps) => {
- const {
- children,
- id,
- className,
- onEscape,
- arrowKeyNavigation,
- dontStealFocus,
- includeOutsideTabOrder,
- dontRestoreFocus
- } = props;
-
- let container: HTMLDivElement;
- const previouslyFocused = React.useRef(document.activeElement);
- const [stoleFocus, setStoleFocus] = React.useState(false);
-
- React.useEffect(() => {
- return () => {
- if (!dontRestoreFocus && previouslyFocused.current) {
- focusLastActive(previouslyFocused.current as HTMLElement)
- }
- }
- }, [])
-
- const getElements = () => {
- const all = nodeListToArray(
- includeOutsideTabOrder ? container.querySelectorAll(`[tabindex]`) :
- container.querySelectorAll(`[tabindex]:not([tabindex="-1"])`)
- );
-
- return all as HTMLElement[];
- }
-
- const handleRef = (ref: HTMLDivElement) => {
- if (!ref) return;
- container = ref;
-
- if (!dontStealFocus && !stoleFocus && !ref.contains(document.activeElement) && getElements().length) {
- container.focus();
-
- // Only steal focus once
- setStoleFocus(true);
- }
- }
-
- const onKeyDown = (e: React.KeyboardEvent) => {
- if (!container) return;
-
- const moveFocus = (forward: boolean, goToEnd: boolean) => {
- const focusable = getElements();
-
- if (!focusable.length) return;
-
- const index = focusable.indexOf(e.target as HTMLElement);
-
- if (forward) {
- if (goToEnd) {
- findNextFocusableElement(focusable, index, focusable.length - 1, forward).focus();
- }
- else if (index === focusable.length - 1) {
- findNextFocusableElement(focusable, index, 0, forward).focus();
- }
- else {
- findNextFocusableElement(focusable, index, index + 1, forward).focus();
- }
- }
- else {
- if (goToEnd) {
- findNextFocusableElement(focusable, index, 0, forward).focus();
- }
- else if (index === 0) {
- findNextFocusableElement(focusable, index, focusable.length - 1, forward).focus();
- }
- else {
- findNextFocusableElement(focusable, index, Math.max(index - 1, 0), forward).focus();
- }
- }
-
- e.preventDefault();
- e.stopPropagation();
- }
-
- if (e.key === "Escape") {
- onEscape();
- e.preventDefault();
- e.stopPropagation();
- }
- else if (e.key === "Tab") {
- if (e.shiftKey) moveFocus(false, false);
- else moveFocus(true, false);
- }
- else if (arrowKeyNavigation) {
- if (e.key === "ArrowDown") {
- moveFocus(true, false);
- }
- else if (e.key === "ArrowUp") {
- moveFocus(false, false);
- }
- else if (e.key === "Home") {
- moveFocus(false, true);
- }
- else if (e.key === "End") {
- moveFocus(true, true);
- }
- }
- }
-
- return
- {children}
-
-}
\ No newline at end of file
diff --git a/react-common/components/controls/FocusTrap/FocusTrap.tsx b/react-common/components/controls/FocusTrap/FocusTrap.tsx
new file mode 100644
index 000000000000..79c433a5b11c
--- /dev/null
+++ b/react-common/components/controls/FocusTrap/FocusTrap.tsx
@@ -0,0 +1,240 @@
+import * as React from "react";
+import { classList, nodeListToArray, findNextFocusableElement, focusLastActive } from "../../util";
+import { addRegion, FocusTrapProvider, removeRegion, useFocusTrapDispatch, useFocusTrapState } from "./context";
+import { useId } from "../../../hooks/useId";
+
+export interface FocusTrapProps extends React.PropsWithChildren<{}> {
+ onEscape: () => void;
+ id?: string;
+ className?: string;
+ arrowKeyNavigation?: boolean;
+ dontStealFocus?: boolean;
+ includeOutsideTabOrder?: boolean;
+ dontRestoreFocus?: boolean;
+}
+
+export const FocusTrap = (props: FocusTrapProps) => {
+ return (
+
+
+
+ );
+}
+
+const FocusTrapInner = (props: FocusTrapProps) => {
+ const {
+ children,
+ id,
+ className,
+ onEscape,
+ arrowKeyNavigation,
+ dontStealFocus,
+ includeOutsideTabOrder,
+ dontRestoreFocus
+ } = props;
+
+ let container: HTMLDivElement;
+ const previouslyFocused = React.useRef(document.activeElement);
+ const [stoleFocus, setStoleFocus] = React.useState(false);
+
+ const { regions } = useFocusTrapState();
+
+ React.useEffect(() => {
+ return () => {
+ if (!dontRestoreFocus && previouslyFocused.current) {
+ focusLastActive(previouslyFocused.current as HTMLElement)
+ }
+ }
+ }, [])
+
+ const getElements = React.useCallback(() => {
+ let all = nodeListToArray(
+ includeOutsideTabOrder ? container.querySelectorAll(`[tabindex]`) :
+ container.querySelectorAll(`[tabindex]:not([tabindex="-1"])`)
+ );
+
+ if (regions.length) {
+ const regionElements: pxt.Map = {};
+
+ for (const region of regions) {
+ const el = container.querySelector(`[data-focus-trap-region="${region.id}"]`);
+
+ if (el) {
+ regionElements[region.id] = el;
+ }
+ }
+
+ for (const region of regions) {
+ const regionElement = regionElements[region.id];
+ if (!region.enabled && regionElement) {
+ all = all.filter(el => !regionElement.contains(el));
+ }
+ }
+
+ const initialOrder = all.slice();
+ all.sort((a, b) => {
+ const aRegion = regions.find(r => r.enabled && regionElements[r.id]?.contains(a));
+ const bRegion = regions.find(r => r.enabled && regionElements[r.id]?.contains(b));
+
+ if (aRegion?.order === bRegion?.order) {
+ const aIndex = initialOrder.indexOf(a);
+ const bIndex = initialOrder.indexOf(b);
+ return aIndex - bIndex;
+ }
+ else if (!aRegion) {
+ return 1;
+ }
+ else if (!bRegion) {
+ return -1;
+ }
+ else {
+ return aRegion.order - bRegion.order;
+ }
+ });
+ }
+
+ return all as HTMLElement[];
+ }, [regions, includeOutsideTabOrder]);
+
+ const handleRef = React.useCallback((ref: HTMLDivElement) => {
+ if (!ref) return;
+ container = ref;
+
+ if (!dontStealFocus && !stoleFocus && !ref.contains(document.activeElement) && getElements().length) {
+ container.focus();
+
+ // Only steal focus once
+ setStoleFocus(true);
+ }
+ }, [getElements, dontStealFocus, stoleFocus]);
+
+ const onKeyDown = React.useCallback((e: React.KeyboardEvent) => {
+ if (!container) return;
+
+ const moveFocus = (forward: boolean, goToEnd: boolean) => {
+ const focusable = getElements();
+
+ if (!focusable.length) return;
+
+ const index = focusable.indexOf(e.target as HTMLElement);
+
+ if (forward) {
+ if (goToEnd) {
+ findNextFocusableElement(focusable, index, focusable.length - 1, forward).focus();
+ }
+ else if (index === focusable.length - 1) {
+ findNextFocusableElement(focusable, index, 0, forward).focus();
+ }
+ else {
+ findNextFocusableElement(focusable, index, index + 1, forward).focus();
+ }
+ }
+ else {
+ if (goToEnd) {
+ findNextFocusableElement(focusable, index, 0, forward).focus();
+ }
+ else if (index === 0) {
+ findNextFocusableElement(focusable, index, focusable.length - 1, forward).focus();
+ }
+ else {
+ findNextFocusableElement(focusable, index, Math.max(index - 1, 0), forward).focus();
+ }
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ if (e.key === "Escape") {
+ let foundHandler = false;
+ if (regions.length) {
+ for (const region of regions) {
+ if (!region.onEscape) continue;
+ const regionElement = container.querySelector(`[data-focus-trap-region="${region.id}"]`);
+ if (regionElement?.contains(document.activeElement)) {
+ foundHandler = true;
+ region.onEscape();
+ break;
+ }
+ }
+ }
+ if (!foundHandler) {
+ onEscape();
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ else if (e.key === "Tab") {
+ if (e.shiftKey) moveFocus(false, false);
+ else moveFocus(true, false);
+ }
+ else if (arrowKeyNavigation) {
+ if (e.key === "ArrowDown") {
+ moveFocus(true, false);
+ }
+ else if (e.key === "ArrowUp") {
+ moveFocus(false, false);
+ }
+ else if (e.key === "Home") {
+ moveFocus(false, true);
+ }
+ else if (e.key === "End") {
+ moveFocus(true, true);
+ }
+ }
+ }, [getElements, onEscape, arrowKeyNavigation, regions])
+
+ return(
+
+ {children}
+
+ );
+}
+
+
+
+interface FocusTrapRegionProps extends React.PropsWithChildren<{}> {
+ enabled: boolean;
+ order?: number;
+ onEscape?: () => void;
+ id?: string;
+ className?: string;
+ divRef?: (ref: HTMLDivElement) => void;
+}
+
+export const FocusTrapRegion = (props: FocusTrapRegionProps) => {
+ const {
+ className,
+ id,
+ onEscape,
+ order,
+ enabled,
+ children,
+ divRef
+ } = props;
+
+ const regionId = useId();
+ const dispatch = useFocusTrapDispatch();
+
+ React.useEffect(() => {
+ dispatch(addRegion(regionId, order, enabled, onEscape));
+
+ return () => dispatch(removeRegion(regionId));
+ }, [regionId, enabled, order])
+
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/react-common/components/controls/FocusTrap/context.tsx b/react-common/components/controls/FocusTrap/context.tsx
new file mode 100644
index 000000000000..5f3f47e70404
--- /dev/null
+++ b/react-common/components/controls/FocusTrap/context.tsx
@@ -0,0 +1,103 @@
+import * as React from "react";
+
+interface FocusTrapState {
+ regions: FocusTrapRegionState[];
+}
+
+interface FocusTrapRegionState {
+ id: string;
+ enabled: boolean;
+ order: number;
+ onEscape?: () => void;
+}
+
+const FocustTrapStateContext = React.createContext(null);
+const FocustTrapDispatchContext = React.createContext<(action: Action) => void>(null);
+
+export const FocusTrapProvider = ({
+ children,
+}: React.PropsWithChildren<{}>) => {
+ const [state, dispatch] = React.useReducer(focusTrapReducer, {
+ regions: []
+ });
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+type AddRegion = {
+ type: "ADD_REGION";
+ id: string;
+ order: number;
+ enabled: boolean;
+ onEscape?: () => void;
+};
+
+type RemoveRegion = {
+ type: "REMOVE_REGION";
+ id: string;
+};
+
+type Action = AddRegion | RemoveRegion;
+
+export const addRegion = (id: string, order: number, enabled: boolean, onEscape?: () => void): AddRegion => (
+ {
+ type: "ADD_REGION",
+ id,
+ order,
+ enabled,
+ onEscape
+ }
+);
+
+export const removeRegion = (id: string): RemoveRegion => (
+ {
+ type: "REMOVE_REGION",
+ id
+ }
+);
+
+export function useFocusTrapState() {
+ return React.useContext(FocustTrapStateContext);
+}
+
+export function useFocusTrapDispatch() {
+ return React.useContext(FocustTrapDispatchContext);
+}
+
+function focusTrapReducer(state: FocusTrapState, action: Action): FocusTrapState {
+ let newRegions = state.regions.slice();
+
+ switch (action.type) {
+ case "ADD_REGION":
+ const newRegion = {
+ id: action.id,
+ enabled: action.enabled,
+ order: action.order,
+ onEscape: action.onEscape
+ };
+ const existing = newRegions.findIndex(r => r.id === action.id);
+ if (existing !== -1) {
+ newRegions.splice(existing, 1, newRegion)
+ }
+ else {
+ newRegions.push(newRegion);
+ }
+ break;
+ case "REMOVE_REGION":
+ const toRemove = state.regions.findIndex(r => r.id === action.id);
+ if (toRemove !== -1) {
+ newRegions.splice(toRemove, 1)
+ }
+ break;
+ }
+
+ return {
+ regions: newRegions
+ };
+}
diff --git a/react-common/components/controls/FocusTrap/index.ts b/react-common/components/controls/FocusTrap/index.ts
new file mode 100644
index 000000000000..a51d81ad6b6b
--- /dev/null
+++ b/react-common/components/controls/FocusTrap/index.ts
@@ -0,0 +1 @@
+export * from "./FocusTrap";
\ No newline at end of file
diff --git a/react-common/styles/controls/CarouselNav.less b/react-common/styles/controls/CarouselNav.less
new file mode 100644
index 000000000000..0f720e04ee47
--- /dev/null
+++ b/react-common/styles/controls/CarouselNav.less
@@ -0,0 +1,77 @@
+.common-carousel-nav {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+
+ .common-carousel-nav-arrow {
+ min-height: 1rem;
+ min-width: 1rem;
+ padding: 0;
+ background: none;
+ color: white;
+ width: 1.25rem;
+ height: 1.25rem;
+
+ margin: 0;
+
+ i.fas {
+ width: 1.25rem;
+ }
+
+ &.disabled {
+ opacity: 0.5;
+ }
+ }
+
+ ul {
+ list-style: none;
+ margin: 0;
+ margin-left: 0.125rem;
+ margin-right: 0.125rem;
+ margin-block: 0;
+ padding-inline: 0;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ height: 1.25rem;
+ }
+
+ li {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ height: 1rem;
+ width: 1rem;
+
+ .common-button {
+ width: 1rem;
+ height: 1rem;
+ padding: 0.3rem;
+ background: none;
+
+ .common-carousel-nav-button-handle {
+ background-color: white;
+ border-radius: 100%;
+ height: 0.4rem;
+ width: 0.4rem;
+ display: block;
+ }
+
+ &.selected {
+ padding: 0.125rem;
+
+ .common-carousel-nav-button-handle {
+ height: 0.75rem;
+ width: 0.75rem;
+ }
+ }
+ }
+ }
+
+ .common-button:focus-visible::after {
+ inset: -1px;
+ }
+}
diff --git a/react-common/styles/react-common.less b/react-common/styles/react-common.less
index 87e9debe8bdb..bef6d8cabacc 100644
--- a/react-common/styles/react-common.less
+++ b/react-common/styles/react-common.less
@@ -25,6 +25,7 @@
@import "controls/VerticalResizeContainer.less";
@import "controls/VerticalSlider.less";
@import "controls/Accordion.less";
+@import "controls/CarouselNav.less";
@import "./react-common-variables.less";
@import "fontawesome-free/less/solid.less";
diff --git a/tests/blocks-test/karma.conf.js b/tests/blocks-test/karma.conf.js
index 980c56bb2282..f76cae76583f 100644
--- a/tests/blocks-test/karma.conf.js
+++ b/tests/blocks-test/karma.conf.js
@@ -71,7 +71,7 @@ module.exports = function(config) {
// We don't use the watcher but for some reason this must be set to true for tests to run
autoWatch: true,
- browsers: [process.env.TRAVIS ? 'chromium_travis' : 'ChromeHeadless'],
+ browsers: [process.env.GITHUB_ACTIONS ? 'chromium_githubactions' : 'ChromeHeadless'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
@@ -83,8 +83,8 @@ module.exports = function(config) {
// Launcher for using chromium in Travis
customLaunchers: {
- chromium_travis: {
- base: "Chrome",
+ chromium_githubactions: {
+ base: "ChromeHeadless",
flags: ['--no-sandbox']
}
}
diff --git a/theme/image-editor/button.less b/theme/image-editor/button.less
index 2a04a107c56f..c6061cd5c7ec 100644
--- a/theme/image-editor/button.less
+++ b/theme/image-editor/button.less
@@ -1,20 +1,26 @@
-.image-editor-button {
- text-align: center;
+.common-button.image-editor-button {
line-height: 2rem;
width: 1.75rem;
height: 1.75rem;
border-radius: 0.25rem;
margin: 0.375rem;
+ padding: 0;
+ min-width: unset;
+ min-height: unset;
transition: color 0.1s;
color: var(--sidebar-icon-active-color);
+ background: none;
- cursor: pointer;
- user-select: none;
+ &:focus-visible::after {
+ inset: -1px;
+ outline-color: @inputBorderColorFocus;
+ }
}
-.image-editor-button.toggle, .image-editor-button.disabled {
+.image-editor-button.toggle, .common-button.image-editor-button.disabled {
color: var(--sidebar-icon-inactive-color);
+ background: none;
}
.image-editor-button.disabled {
diff --git a/theme/image-editor/cursorSizes.less b/theme/image-editor/cursorSizes.less
index c906c4050879..62c65491c0ce 100644
--- a/theme/image-editor/cursorSizes.less
+++ b/theme/image-editor/cursorSizes.less
@@ -3,13 +3,21 @@
display: flex;
flex-direction: row;
margin: auto;
+ text-align: center;
}
.cursor-button {
border-radius: 1px;
- background-color: var(--sidebar-icon-inactive-color);
+ background-color: var(--sidebar-icon-active-color);
margin-right: 0.25rem;
transition: background-color 0.1s;
+ vertical-align: middle;
+ display: inline-block;
+ margin-top: -0.25rem;
+}
+
+.toggle .cursor-button {
+ background-color: var(--sidebar-icon-inactive-color);
}
.cursor-button-outer {
@@ -17,24 +25,21 @@
width: 1.5rem;
}
-.cursor-button-outer:hover .cursor-button, .cursor-button-outer.selected .cursor-button {
+.common-button:hover .cursor-button {
background-color: var(--sidebar-icon-active-color);
}
.cursor-button.small {
- margin-top: 0.75rem;
width: 0.5rem;
height: 0.5rem;
}
.cursor-button.medium {
- margin-top: 0.625rem;
width: 0.75rem;
height: 0.75rem;
}
.cursor-button.large {
- margin-top: 0.5rem;
width: 1rem;
height: 1rem;
}
\ No newline at end of file
diff --git a/theme/image-editor/imageCanvas.less b/theme/image-editor/imageCanvas.less
index c3dd8dfe49f5..a9d83e36adaa 100644
--- a/theme/image-editor/imageCanvas.less
+++ b/theme/image-editor/imageCanvas.less
@@ -48,7 +48,7 @@
-ms-interpolation-mode: nearest-neighbor;
}
-.checkerboard {
+.checkerboard, .common-button.image-editor-button.checkerboard {
background-color: #aeaeae;
background-image: linear-gradient(45deg, #dedede 25%, transparent 25%), linear-gradient(-45deg, #dedede 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #dedede 75%), linear-gradient(-45deg, transparent 75%, #dedede 75%);
background-size: 0.75rem 0.75rem;
diff --git a/theme/image-editor/imageEditor.less b/theme/image-editor/imageEditor.less
index becb3be9cd8e..f6be06ae8aa9 100644
--- a/theme/image-editor/imageEditor.less
+++ b/theme/image-editor/imageEditor.less
@@ -90,12 +90,19 @@
overflow: hidden;
}
+.image-editor-region {
+ width: 100%;
+ height: 100%;
+ position: relative;
+}
+
.gallery-editor-header {
height: 3rem;
background-color: #4B7BEC;
border: 2px solid #4067b3;
border-bottom: none;
display: flex;
+ flex-shrink: 0;
}
.image-editor-header-left,
@@ -292,7 +299,7 @@
max-width: 120px;
}
-.image-editor-confirm {
+.common-button.image-editor-confirm {
display: flex;
align-items: center;
padding: 0 2rem;
@@ -300,13 +307,15 @@
color: @white;
cursor: pointer;
user-select: none;
+ margin: 0;
+ border-radius: 0;
}
-.image-editor-confirm:hover {
+.common-button.image-editor-confirm:hover {
background-color: darken(@green, 5%);
}
-.image-editor-confirm:active {
+.common-button.image-editor-confirm:active {
background-color: darken(@green, 10%);
}
diff --git a/theme/image-editor/tilePalette.less b/theme/image-editor/tilePalette.less
index b8c464696841..3e907842310c 100644
--- a/theme/image-editor/tilePalette.less
+++ b/theme/image-editor/tilePalette.less
@@ -105,74 +105,116 @@
.tile-canvas-controls {
width: 100%;
text-align: center;
- margin-top: -0.5rem;
}
-.tile-palette-pages {
- height: 1.75rem;
- display: inline-block;
+.tile-canvas {
+ width: 9.5rem;
+ height: 9.5rem;
+ position: relative;
+
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ grid-template-rows: repeat(4, 1fr);
+ gap: 1px;
+
+ margin: 0.25rem;
+ padding: 2px;
+ background-color: #3d3d3d;
}
-.tile-palette-page-arrow {
- fill: var(--sidebar-icon-inactive-color);
- stroke: var(--sidebar-icon-inactive-color);
- stroke-linejoin: round;
- cursor: pointer;
+.tile-palette-controls {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+ align-content: center;
+ height: 2rem;
+}
- transition: fill 0.1s, stroke 0.1s;
+.tile-palette-controls .image-editor-button {
+ line-height: 1.75rem;
}
-.tile-palette-page-dot {
- fill: var(--sidebar-icon-inactive-color);
- cursor: pointer;
- transition: fill 0.1s;
+.tile-palette-controls-outer {
+ height: 2rem;
}
-.tile-palette-pages:not(.disabled) {
- .tile-palette-page-arrow:hover {
- fill: var(--sidebar-icon-active-color);
- stroke: var(--sidebar-icon-active-color);
+.tile-canvas-controls .common-carousel-nav {
+ .common-carousel-nav-arrow {
+ color: var(--sidebar-icon-inactive-color);
+ transition: color 0.1s;
+
+ &:hover {
+ color: var(--sidebar-icon-active-color);
+ filter: none;
+ }
}
- .tile-palette-page-dot:hover {
- fill: var(--sidebar-icon-active-color);
+ li .common-button {
+ .common-carousel-nav-button-handle {
+ background-color: var(--sidebar-icon-inactive-color);
+ transition: background-color 0.1s;
+ }
+
+ &:hover {
+ filter: none;
+
+ .common-carousel-nav-button-handle {
+ background-color: var(--sidebar-icon-active-color);
+ }
+ }
}
}
-.tile-palette-pages.disabled {
- .tile-palette-page-arrow,
- .tile-palette-page-dot {
- opacity: 0.5;
+.tile-palette-dropdown.common-dropdown {
+ .common-dropdown-button {
+ background: none;
+ color: var(--sidebar-icon-active-color);
+ border: 1px solid var(--sidebar-icon-inactive-color);
+ min-width: unset;
+ width: calc(10rem - 0.5rem);
+ margin: 0 0.25rem;
+ transition: border-color 0.1s;
+
+ &:hover {
+ border-color: var(--sidebar-icon-active-color);
+ filter: none;
+ }
+ }
+
+ .common-menu-dropdown-pane {
+ max-height: 12rem;
+ overflow-y: auto;
+ overflow-x: hidden;
}
}
-.tile-canvas {
- width: 100%;
- padding: 0.25rem;
+.tile-button-outer {
position: relative;
}
-.tile-canvas .paint-surface {
+
+.image-editor-button.common-button.tile-button {
+ margin: 0;
width: 100%;
- background-color: #3d3d3d;
-}
+ height: 100%;
+ background-color: var(--editing-tools-bg-color);
+ border-radius: 0;
-.tile-palette-controls {
- display: flex;
- flex-direction: row;
- justify-content: space-evenly;
- align-content: center;
- height: 2rem;
-}
+ canvas {
+ width: 100%;
+ height: 100%;
-.tile-palette-controls .image-editor-button {
- line-height: 1.75rem;
+ image-rendering: pixelated;
+ }
}
-.tile-palette-controls-outer {
- height: 2rem;
-}
+.image-editor-button.common-button.add-tile-button {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+ margin-top: 0.2rem;
+}
@media screen and (max-height: 720px) {
.image-editor-tilemap-minimap {
diff --git a/theme/image-editor/timeline.less b/theme/image-editor/timeline.less
index fb51866f1ce8..f1f55f30522f 100644
--- a/theme/image-editor/timeline.less
+++ b/theme/image-editor/timeline.less
@@ -7,6 +7,13 @@
flex-direction: column;
}
+.image-editor-timeline-frames {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ padding: 0.4rem 0;
+}
+
.image-editor-timeline-frame {
border: 1px var(--sidebar-icon-inactive-color) solid;
color: var(--sidebar-icon-inactive-color);
@@ -14,7 +21,7 @@
border-radius: 0.25rem;
width: 6rem;
height: 6rem;
- margin: 0.4rem;
+ margin: 0 0.4rem;
transition: border 0.2s, color 0.2s, background-color 0.2s;
overflow: hidden;
@@ -126,6 +133,20 @@
background-color: #aeaeae;
}
+.common-button.image-editor-button.add-frame-button {
+ height: 2rem;
+ width: 6rem;
+ margin: 0 0.4rem;
+
+ border: 1px solid var(--sidebar-icon-inactive-color);
+ transition: color 0.1s, border-color 0.1s;
+
+ &:hover {
+ border-color: var(--sidebar-icon-active-color);
+ filter: none;
+ }
+}
+
/* .timeline-frame-actions */
diff --git a/theme/image-editor/topBar.less b/theme/image-editor/topBar.less
index 0ee9874f5611..5aebbe8e1afc 100644
--- a/theme/image-editor/topBar.less
+++ b/theme/image-editor/topBar.less
@@ -9,6 +9,7 @@
.image-editor-topbar > div .image-editor-button {
margin-top: 0;
margin-left: 0;
+ height: 2rem;
}
.image-editor-topbar > div {
diff --git a/webapp/src/components/ImageEditor/Alert.tsx b/webapp/src/components/ImageEditor/Alert.tsx
index d15bb9e858e6..cd69e5c7ae8b 100644
--- a/webapp/src/components/ImageEditor/Alert.tsx
+++ b/webapp/src/components/ImageEditor/Alert.tsx
@@ -2,7 +2,6 @@ import * as React from 'react';
import { connect } from 'react-redux';
import { ImageEditorStore } from './store/imageReducer';
import { dispatchHideAlert } from './actions/dispatch';
-import { IconButton } from "./Button";
export interface AlertOption {
label: string;
diff --git a/webapp/src/components/ImageEditor/BottomBar.tsx b/webapp/src/components/ImageEditor/BottomBar.tsx
index 8107117e47c1..cb2eb84c7859 100644
--- a/webapp/src/components/ImageEditor/BottomBar.tsx
+++ b/webapp/src/components/ImageEditor/BottomBar.tsx
@@ -3,11 +3,11 @@ import * as React from "react";
import { connect } from 'react-redux';
import { ImageEditorStore, AnimationState, TilemapState } from './store/imageReducer';
import { dispatchChangeImageDimensions, dispatchUndoImageEdit, dispatchRedoImageEdit, dispatchToggleAspectRatioLocked, dispatchChangeZoom, dispatchToggleOnionSkinEnabled, dispatchChangeAssetName } from './actions/dispatch';
-import { IconButton } from "./Button";
import { fireClickOnlyOnEnter } from "./util";
import { isNameTaken } from "../../assets";
import { obtainShortcutLock, releaseShortcutLock } from "./keyboardShortcuts";
import { classList } from "../../../../react-common/components/util";
+import { Button } from "../../../../react-common/components/controls/Button";
export interface BottomBarProps {
dispatchChangeImageDimensions: (dimensions: [number, number]) => void;
@@ -93,12 +93,11 @@ export class BottomBarImpl extends React.Component
-
}
{ !singleFrame &&
-
}
{ !resizeDisabled &&
}
@@ -145,42 +144,44 @@ export class BottomBarImpl extends React.Component
-
-
-
-
- {!hideDoneButton &&
- {lf("Done")}
-
}
+ {!hideDoneButton &&
+
+ }
);
}
diff --git a/webapp/src/components/ImageEditor/CursorSizes.tsx b/webapp/src/components/ImageEditor/CursorSizes.tsx
index dfece1f1702f..87ad80f6ac6c 100644
--- a/webapp/src/components/ImageEditor/CursorSizes.tsx
+++ b/webapp/src/components/ImageEditor/CursorSizes.tsx
@@ -2,6 +2,8 @@ import * as React from 'react';
import { ImageEditorStore, CursorSize } from './store/imageReducer';
import { dispatchChangeCursorSize } from './actions/dispatch';
import { connect } from 'react-redux';
+import { Button } from '../../../../react-common/components/controls/Button';
+import { classList } from '../../../../react-common/components/util';
interface CursorSizesProps {
selected: CursorSize;
@@ -14,17 +16,28 @@ class CursorSizesImpl extends React.Component {
render() {
const { selected } = this.props;
- return
-
-
+ return (
+
+ }
+ onClick={this.clickHandler(CursorSize.One)}
+ />
+ }
+ onClick={this.clickHandler(CursorSize.Three)}
+ />
+ }
+ onClick={this.clickHandler(CursorSize.Five)}
+ />
-
-
-
+ );
}
clickHandler(size: CursorSize) {
diff --git a/webapp/src/components/ImageEditor/Dropdown.tsx b/webapp/src/components/ImageEditor/Dropdown.tsx
deleted file mode 100644
index aa351b69be0e..000000000000
--- a/webapp/src/components/ImageEditor/Dropdown.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import * as React from 'react';
-
-export interface DropdownOption {
- text: string;
- id: string;
-}
-
-export interface DropdownProps {
- options: DropdownOption[];
- selected: number;
- onChange: (selected: DropdownOption, index: number) => void;
-}
-
-export interface DropdownState {
- open: boolean;
-}
-
-export class Dropdown extends React.Component
{
- protected handlers: (() => void)[] = [];
-
- constructor(props: DropdownProps) {
- super(props);
-
- this.state = {
- open: false
- };
- }
-
- componentDidUpdate() {
- this.handlers = [];
- }
-
- componentWillUnmount() {
- this.handlers = null;
- }
-
- render() {
- const { options, selected } = this.props;
- const { open } = this.state;
-
- const selectedOption = options[selected];
-
- return
-
- { selectedOption?.text || "" }
-
-
-
-
- {
- options.map((option, index) =>
-
- {option.text}
-
- )
- }
-
-
- }
-
- protected clickHandler(index: number): () => void {
- if (!this.handlers[index]) {
- const { onChange, options } = this.props;
-
- this.handlers[index] = () => {
- this.setState({ open: false });
- onChange(options[index], index);
- };
- }
-
- return this.handlers[index];
- }
-
- protected handleDropdownClick = () => {
- this.setState({ open: !this.state.open });
- }
-}
\ No newline at end of file
diff --git a/webapp/src/components/ImageEditor/SideBar.tsx b/webapp/src/components/ImageEditor/SideBar.tsx
index 9c3ab4a456a5..640308759cbb 100644
--- a/webapp/src/components/ImageEditor/SideBar.tsx
+++ b/webapp/src/components/ImageEditor/SideBar.tsx
@@ -2,12 +2,13 @@ import * as React from "react";
import { connect } from "react-redux";
import { tools } from "./toolDefinitions";
-import { IconButton } from "./Button";
import { ImageEditorTool, ImageEditorStore } from "./store/imageReducer";
import { dispatchChangeImageTool } from "./actions/dispatch";
import { Palette } from "./sprite/Palette";
import { TilePalette } from "./tilemap/TilePalette";
import { Minimap } from "./tilemap/Minimap";
+import { Button } from "../../../../react-common/components/controls/Button";
+import { classList } from "../../../../react-common/components/util";
interface SideBarProps {
selectedTool: ImageEditorTool;
@@ -30,12 +31,13 @@ export class SideBarImpl extends React.Component {
}
{tools.filter(td => !td.hiddenTool).map(td =>
-
+ onClick={this.clickHandler(td.tool)}
+ />
)}
diff --git a/webapp/src/components/ImageEditor/Timeline.tsx b/webapp/src/components/ImageEditor/Timeline.tsx
index 2d7462d48721..2928bdb81eac 100644
--- a/webapp/src/components/ImageEditor/Timeline.tsx
+++ b/webapp/src/components/ImageEditor/Timeline.tsx
@@ -6,6 +6,7 @@ import { dispatchChangeCurrentFrame, dispatchNewFrame, dispatchDuplicateFrame, d
import { TimelineFrame } from "./TimelineFrame";
import { bindGestureEvents, ClientCoordinates } from "./util";
+import { Button } from "../../../../react-common/components/controls/Button";
interface TimelineProps {
colors: string[];
@@ -81,9 +82,12 @@ export class TimelineImpl extends React.Component
deleteFrame={this.deleteFrame} />
}
-
-
-
+
diff --git a/webapp/src/components/ImageEditor/TimelineFrame.tsx b/webapp/src/components/ImageEditor/TimelineFrame.tsx
index 9d2107b35759..47859dd50c74 100644
--- a/webapp/src/components/ImageEditor/TimelineFrame.tsx
+++ b/webapp/src/components/ImageEditor/TimelineFrame.tsx
@@ -1,5 +1,5 @@
import * as React from "react";
-import { IconButton } from "./Button";
+import { Button } from "../../../../react-common/components/controls/Button";
interface TimelineFrameProps {
frames: pxt.sprite.ImageState[];
@@ -47,13 +47,15 @@ export class TimelineFrame extends React.Component
{showActions &&
-
-
diff --git a/webapp/src/components/ImageEditor/TopBar.tsx b/webapp/src/components/ImageEditor/TopBar.tsx
index 2f36a4d73369..984a9b28c531 100644
--- a/webapp/src/components/ImageEditor/TopBar.tsx
+++ b/webapp/src/components/ImageEditor/TopBar.tsx
@@ -3,10 +3,10 @@ import * as React from "react";
import { connect } from 'react-redux';
import { ImageEditorStore, AnimationState } from './store/imageReducer';
import { dispatchChangeInterval, dispatchChangePreviewAnimating, dispatchChangeOverlayEnabled } from './actions/dispatch';
-import { IconButton } from "./Button";
import { CursorSizes } from "./CursorSizes";
import { Toggle } from "./Toggle";
import { flip, rotate } from "./keyboardShortcuts";
+import { Button } from "../../../../react-common/components/controls/Button";
export interface TopBarProps {
@@ -41,20 +41,40 @@ export class TopBarImpl extends React.Component {
-
-
-
-
+
+
+
+
{ !singleFrame &&
}
{ !singleFrame &&
-
diff --git a/webapp/src/components/ImageEditor/sprite/Palette.tsx b/webapp/src/components/ImageEditor/sprite/Palette.tsx
index 43da36da74f7..606fa1c0a60c 100644
--- a/webapp/src/components/ImageEditor/sprite/Palette.tsx
+++ b/webapp/src/components/ImageEditor/sprite/Palette.tsx
@@ -2,6 +2,8 @@ import * as React from 'react';
import { connect } from 'react-redux';
import { ImageEditorStore, AnimationState } from '../store/imageReducer';
import { dispatchChangeSelectedColor, dispatchChangeBackgroundColor, dispatchSwapBackgroundForeground } from '../actions/dispatch';
+import { Button } from '../../../../../react-common/components/controls/Button';
+import { classList } from '../../../../../react-common/components/util';
export interface PaletteProps {
colors: string[];
@@ -13,8 +15,6 @@ export interface PaletteProps {
}
class PaletteImpl extends React.Component
{
- protected handlers: ((ev: React.MouseEvent) => void)[] = [];
-
render() {
const { colors, selected, backgroundColor, dispatchSwapBackgroundForeground } = this.props;
const SPACER = 1;
@@ -57,33 +57,20 @@ class PaletteImpl extends React.Component {
- {this.props.colors.map((color, index) => {
- return
+
- })}
+ style={index === 0 ? null : { backgroundColor: color }}
+ onClick={() => this.props.dispatchChangeSelectedColor(index)}
+ onRightClick={() => this.props.dispatchChangeBackgroundColor(index)}
+ />
+ )}
;
}
- protected clickHandler(index: number) {
- if (!this.handlers[index]) this.handlers[index] = (ev: React.MouseEvent) => {
- if (ev.button === 0) {
- this.props.dispatchChangeSelectedColor(index);
- }
- else {
- this.props.dispatchChangeBackgroundColor(index);
- ev.preventDefault();
- ev.stopPropagation();
- }
- }
-
- return this.handlers[index];
- }
-
protected preventContextMenu = (ev: React.MouseEvent) => ev.preventDefault();
}
diff --git a/webapp/src/components/ImageEditor/tilemap/TilePalette.tsx b/webapp/src/components/ImageEditor/tilemap/TilePalette.tsx
index 8d1493f584ea..a8746c319dea 100644
--- a/webapp/src/components/ImageEditor/tilemap/TilePalette.tsx
+++ b/webapp/src/components/ImageEditor/tilemap/TilePalette.tsx
@@ -6,12 +6,15 @@ import { dispatchChangeSelectedColor, dispatchChangeBackgroundColor, dispatchSwa
dispatchCreateNewTile, dispatchSetGalleryOpen, dispatchOpenTileEditor, dispatchDeleteTile,
dispatchShowAlert, dispatchHideAlert } from '../actions/dispatch';
import { TimelineFrame } from '../TimelineFrame';
-import { Dropdown, DropdownOption } from '../Dropdown';
import { Pivot, PivotOption } from '../Pivot';
-import { IconButton } from '../Button';
import { AlertOption } from '../Alert';
import { createTile } from '../../../assets';
+import { CarouselNav } from "../../../../../react-common/components/controls/CarouselNav";
+import { Dropdown, DropdownItem } from '../../../../../react-common/components/controls/Dropdown';
+import { Button } from '../../../../../react-common/components/controls/Button';
+import { classList } from '../../../../../react-common/components/util';
+
export interface TilePaletteProps {
colors: string[];
tileset: pxt.TileSet;
@@ -49,7 +52,9 @@ const SCALE = pxt.BrowserUtils.isEdge() ? 25 : 1;
const TILES_PER_PAGE = 16;
-interface Category extends DropdownOption {
+interface Category {
+ id: string;
+ text: string;
tiles: GalleryTile[];
}
@@ -93,7 +98,6 @@ interface UserTile {
type RenderedTile = GalleryTile | UserTile
class TilePaletteImpl extends React.Component {
- protected canvas: HTMLCanvasElement;
protected renderedTiles: RenderedTile[];
protected categoryTiles: RenderedTile[];
protected categories: Category[];
@@ -129,9 +133,7 @@ class TilePaletteImpl extends React.Component {
}
componentDidMount() {
- this.canvas = this.refs["tile-canvas-surface"] as HTMLCanvasElement;
this.updateGalleryTiles();
- this.redrawCanvas();
}
UNSAFE_componentWillReceiveProps(nextProps: TilePaletteProps) {
@@ -145,7 +147,6 @@ class TilePaletteImpl extends React.Component {
componentDidUpdate() {
this.updateGalleryTiles();
- this.redrawCanvas();
}
render() {
@@ -164,6 +165,19 @@ class TilePaletteImpl extends React.Component {
const showCreateTile = !galleryOpen && (totalPages === 1 || page === totalPages - 1);
const controlsDisabled = galleryOpen || !this.renderedTiles.some(t => !isGalleryTile(t) && t.index === selected);
+ const columns = 4;
+ const rows = 4;
+ const startIndex = page * columns * rows;
+
+ const visibleTiles = this.categoryTiles.slice(startIndex, startIndex + columns * rows);
+
+ const dropdownItems: DropdownItem[] = this.categories.filter(c => !!c.tiles.length)
+ .map(cat => ({
+ id: cat.id,
+ title: cat.text,
+ label: cat.text,
+ }));
+
return
@@ -175,10 +189,12 @@ class TilePaletteImpl extends React.Component {
-
+ onClick={this.foregroundBackgroundClickHandler}
+ />
{
- { galleryOpen &&
!!c.tiles.length)} selected={category} /> }
+ { galleryOpen &&
+
+ }
{ !galleryOpen &&
-
-
-
}
@@ -222,20 +246,34 @@ class TilePaletteImpl extends React.Component {
-
+ { visibleTiles.map((tile, index) =>
+
this.handleTileClick(index, false)}
+ onRightClick={() => this.handleTileClick(index, true)}
+ />
+ )}
{ showCreateTile &&
-
-
+
}
- { pageControls(totalPages, page, this.pageHandler) }
+
;
@@ -281,65 +319,8 @@ class TilePaletteImpl extends React.Component
{
}
}
- protected redrawCanvas() {
- const columns = 4;
- const rows = 4;
- const margin = 1;
-
- const { tileset, page, selected } = this.props;
-
- const startIndex = page * columns * rows;
-
- const width = tileset.tileWidth + margin;
-
- this.canvas.width = (width * columns + margin) * SCALE;
- this.canvas.height = (width * rows + margin) * SCALE;
-
- const context = this.canvas.getContext("2d");
-
- for (let r = 0; r < rows; r++) {
- for (let c = 0; c < columns; c++) {
- const tile = this.categoryTiles[startIndex + r * columns + c];
-
- if (tile) {
- if (!isGalleryTile(tile) && tile.index === selected) {
- context.fillStyle = "#ff0000";
- context.fillRect(c * width, r * width, width + 1, width + 1);
- }
-
- context.fillStyle = "#333333";
- context.fillRect(c * width + 1, r * width + 1, width - 1, width - 1);
-
- this.drawBitmap(pxt.sprite.Bitmap.fromData(tile.bitmap), 1 + c * width, 1 + r * width)
- }
- }
- }
-
- this.positionCreateTileButton();
- }
-
- protected drawBitmap(bitmap: pxt.sprite.Bitmap, x0 = 0, y0 = 0, transparent = true, cellWidth = SCALE, target = this.canvas) {
- const { colors } = this.props;
-
- const context = target.getContext("2d");
- context.imageSmoothingEnabled = false;
- for (let x = 0; x < bitmap.width; x++) {
- for (let y = 0; y < bitmap.height; y++) {
- const index = bitmap.get(x, y);
-
- if (index) {
- context.fillStyle = colors[index];
- context.fillRect((x + x0) * cellWidth, (y + y0) * cellWidth, cellWidth, cellWidth);
- }
- else {
- if (!transparent) context.clearRect((x + x0) * cellWidth, (y + y0) * cellWidth, cellWidth, cellWidth);
- }
- }
- }
- }
-
- protected dropdownHandler = (option: DropdownOption, index: number) => {
- this.props.dispatchChangeTilePaletteCategory(index);
+ protected dropdownHandler = (id: string) => {
+ this.props.dispatchChangeTilePaletteCategory(this.categories.filter(c => !!c.tiles.length).findIndex(c => c.id === id));
}
protected pivotHandler = (option: PivotOption, index: number) => {
@@ -401,20 +382,9 @@ class TilePaletteImpl extends React.Component {
}
}
- protected canvasClickHandler = (ev: React.MouseEvent) => {
- this.handleCanvasClickCore(ev.clientX, ev.clientY, ev.button > 0);
- }
-
- protected canvasTouchHandler = (ev: React.TouchEvent) => {
- this.handleCanvasClickCore(ev.changedTouches[0].clientX, ev.changedTouches[0].clientY, false);
- }
-
- protected handleCanvasClickCore(clientX: number, clientY: number, isRightClick: boolean) {
- const bounds = this.canvas.getBoundingClientRect();
- const column = ((clientX - bounds.left) / (bounds.width / 4)) | 0;
- const row = ((clientY - bounds.top) / (bounds.height / 4)) | 0;
+ protected handleTileClick(buttonIndex: number, isRightClick: boolean) {
+ const tile = this.renderedTiles[buttonIndex];
- const tile = this.renderedTiles[row * 4 + column];
if (tile) {
let index: number;
let qname: string;
@@ -456,19 +426,6 @@ class TilePaletteImpl extends React.Component {
}
}
- protected positionCreateTileButton() {
- const button = this.refs["create-tile-ref"] as HTMLDivElement;
-
- if (button) {
- const column = this.categoryTiles.length % 4;
- const row = Math.floor(this.categoryTiles.length / 4) % 4;
-
- button.style.position = "absolute";
- button.style.left = "calc(" + (column / 4) + " * (100% - 0.5rem) + 0.25rem)";
- button.style.top = "calc(" + (row / 4) + " * (100% - 0.5rem) + 0.25rem)";
- }
- }
-
protected foregroundBackgroundClickHandler = () => {
if (this.props.drawingMode != TileDrawingMode.Default) {
this.props.dispatchChangeDrawingMode(TileDrawingMode.Default);
@@ -512,36 +469,56 @@ class TilePaletteImpl extends React.Component {
}
}
+interface TileButtonProps {
+ tile: pxt.sprite.BitmapData;
+ title: string;
+ onClick: () => void;
+ onRightClick: () => void;
+ colors: string[];
+}
+
+const TileButton = (props: TileButtonProps) => {
+ const { tile, title, onClick, onRightClick, colors } = props;
+
+ const canvasRef = React.useRef();
+
+ React.useEffect(() => {
+ const canvas = canvasRef.current;
+
+ canvas.width = tile.width;
+ canvas.height = tile.height;
+
+ const context = canvas.getContext("2d");
+
+ context.clearRect(0, 0, canvas.width, canvas.height);
-function pageControls(pages: number, selected: number, onClick: (index: number) => void) {
- const width = 16 + (pages - 1) * 5;
- const pageMap: boolean[] = [];
- for (let i = 0; i < pages; i++) pageMap[i] = i === selected;
-
- return
- onClick(selected - 1) : undefined} />
- {
- pageMap.map((isSelected, index) =>
- onClick(index) : undefined}/>
- )
+ const bitmap = pxt.sprite.Bitmap.fromData(tile);
+
+ for (let x = 0; x < tile.width; x++) {
+ for (let y = 0; y < tile.height; y++) {
+ const index = bitmap.get(x, y);
+
+ if (index) {
+ context.fillStyle = colors[index];
+ context.fillRect(x, y, 1, 1);
+ }
+ }
}
- onClick(selected + 1) : undefined} />
-
+ }, [tile, colors])
+
+ return (
+
+ }
+ />
+
+ )
}
-
function mapStateToProps({ store: { present }, editor }: ImageEditorStore, ownProps: any) {
let state = (present as TilemapState);
if (!state) return {};
diff --git a/webapp/src/components/ImageFieldEditor.tsx b/webapp/src/components/ImageFieldEditor.tsx
index e1e0adcaa500..dfa4aa3c6e99 100644
--- a/webapp/src/components/ImageFieldEditor.tsx
+++ b/webapp/src/components/ImageFieldEditor.tsx
@@ -8,9 +8,10 @@ import { obtainShortcutLock, releaseShortcutLock } from "./ImageEditor/keyboardS
import { GalleryTile, setTelemetryFunction } from './ImageEditor/store/imageReducer';
import { FilterPanel } from './FilterPanel';
import { fireClickOnEnter } from "../util";
-import { EditorToggle, EditorToggleItem, BasicEditorToggleItem } from "../../../react-common/components/controls/EditorToggle";
+import { EditorToggle } from "../../../react-common/components/controls/EditorToggle";
import { MusicFieldEditor } from "./MusicFieldEditor";
import { classList } from "../../../react-common/components/util";
+import { FocusTrap, FocusTrapRegion } from "../../../react-common/components/controls/FocusTrap";
export interface ImageFieldEditorProps {
singleFrame: boolean;
@@ -20,10 +21,6 @@ export interface ImageFieldEditorProps {
includeSpecialTagsInFilter?: boolean;
}
-interface ToggleOption extends BasicEditorToggleItem {
- view: string;
-}
-
export interface ImageFieldEditorState {
currentView: "editor" | "gallery" | "my-assets";
filterOpen: boolean;
@@ -36,11 +33,6 @@ export interface ImageFieldEditorState {
hideCloseButton?: boolean;
}
-interface ProjectGalleryItem extends pxt.sprite.GalleryItem {
- assetType: pxt.AssetType;
- id: string;
-}
-
export interface AssetEditorCore {
getAsset(): pxt.Asset;
getPersistentData(): any;
@@ -63,6 +55,7 @@ export class ImageFieldEditor extends React.Component extends React.Component
- {showHeader &&
-
-
- i.view === currentView)}
- />
-
-
-
-
-
+ return (
+
+ {showHeader &&
+
+
+ i.view === currentView)}
+ />
+
+
+
-
{lf("Filter")}
+ {!editingTile && !hideCloseButton &&
+
+
}
- {!editingTile && !hideCloseButton &&
-
-
}
-
- }
-
-
- {this.props.isMusicEditor ?
-
:
-
}
+
+
+
+ {this.props.isMusicEditor ?
+ :
+
+ }
+
+
- }
-
-
-
-
-
+
+ );
}
componentDidMount() {
@@ -621,23 +624,44 @@ export class ImageFieldEditor
extends React.Component {
+ if (ref) this.imageEditorRegion = ref;
+ }
+
+ protected onEscapeFromGallery = () => {
+ this.setState({
+ currentView: "editor"
+ }, () => {
+ if (this.imageEditorRegion) {
+ this.imageEditorRegion.focus();
+ }
+ })
+ }
}
interface ImageEditorGalleryProps {
items?: pxt.Asset[];
hidden: boolean;
onAssetSelected: (item: pxt.Asset) => void;
+ onEscape: () => void;
}
class ImageEditorGallery extends React.Component {
render() {
- let { items, hidden } = this.props;
-
- return
- {!hidden && items && items.map((item, index) =>
-
- )}
-
+ let { items, hidden, onEscape } = this.props;
+
+ return (
+
+ {!hidden && items?.map((item, index) =>
+
+ )}
+
+ );
}
clickHandler = (asset: pxt.Asset) => {
diff --git a/webapp/src/components/assetEditor/assetCard.tsx b/webapp/src/components/assetEditor/assetCard.tsx
index 73a1c1c465c3..e0eb892cbd01 100644
--- a/webapp/src/components/assetEditor/assetCard.tsx
+++ b/webapp/src/components/assetEditor/assetCard.tsx
@@ -5,6 +5,7 @@ import { AssetEditorState, isGalleryAsset } from './store/assetEditorReducerStat
import { dispatchChangeSelectedAsset } from './actions/dispatch';
import { AssetPreview } from "./assetPreview";
+import { fireClickOnEnter } from "../../util";
interface AssetCardProps {
@@ -57,17 +58,25 @@ export class AssetCardView extends React.Component {
const inGallery = isGalleryAsset(asset);
const icon = this.getDisplayIconForAsset(asset.type);
const showIcons = icon || !asset.meta?.displayName;
- return
-
- {showIcons &&
- {icon &&
-
+ return (
+
+
+ {showIcons &&
+ {icon &&
+
+
}
+ {!asset.meta?.displayName && !inGallery &&
+
+
}
}
- {!asset.meta?.displayName && !inGallery &&
-
-
}
-
}
-
+
+ );
}
}