UNPKG

pxt-common-packages

Version:
510 lines (435 loc) 17.9 kB
namespace game { export const _KEYBOARD_CHANGE_EVENT = 7339; export const _KEYBOARD_ENTER_EVENT = 7340; export const _KEYBOARD_CANCEL_EVENT = 7341; export interface PromptTheme { colorPrompt: number; colorInput: number; colorInputHighlighted: number; colorInputText: number; colorAlphabet: number; colorCursor: number; colorBackground: number; colorBottomBackground: number; colorBottomText: number; } /** * Ask the player for a string value. * @param message The message to display on the text-entry screen * @param answerLength The maximum number of characters the user can enter (1 - 24) * @param useOnScreenKeyboard Force the simulator to use the on-screen keyboard for text entry */ //% weight=10 help=game/ask-for-string //% blockId=gameaskforstring //% block="ask for string $message || and max length $answerLength use on-screen keyboard $useOnScreenKeyboard" //% message.shadow=text //% message.defl="" //% answerLength.defl="12" //% answerLength.min=1 //% answerLength.max=24 //% group="Prompt" export function askForString(message: any, answerLength = 12, useOnScreenKeyboard = false) { let p = new game.Prompt(); const result = p.show(console.inspect(message), answerLength, useOnScreenKeyboard); return result; } //% whenUsed=true const font = image.font8; // FONT8-TODO //% whenUsed=true const PADDING = 4; //% whenUsed=true const NUM_LETTERS = 26; //% whenUsed=true const ALPHABET_ROW_LENGTH = 12; //% whenUsed=true const NUM_ROWS = Math.ceil(NUM_LETTERS / ALPHABET_ROW_LENGTH); //% whenUsed=true const INPUT_ROWS = 2; //% whenUsed=true const CONTENT_WIDTH = screen.width - PADDING * 2; //% whenUsed=true const CONTENT_HEIGHT = screen.height - PADDING * 2; //% whenUsed=true const CONTENT_TOP = PADDING; // Dimensions of a "cell" that contains a letter //% whenUsed=true const CELL_WIDTH = Math.floor(CONTENT_WIDTH / ALPHABET_ROW_LENGTH); //% whenUsed=true const CELL_HEIGHT = CELL_WIDTH; //% whenUsed=true const LETTER_OFFSET_X = Math.floor((CELL_WIDTH - font.charWidth) / 2); //% whenUsed=true const LETTER_OFFSET_Y = Math.floor((CELL_HEIGHT - font.charHeight) / 2); //% whenUsed=true const BLANK_PADDING = 1; //% whenUsed=true const ROW_LEFT = PADDING + Math.floor((CONTENT_WIDTH - (CELL_WIDTH * ALPHABET_ROW_LENGTH)) / 2); // Dimensions of the bottom bar //% whenUsed=true const BOTTOM_BAR_ALPHABET_MARGIN = 4; //% whenUsed=true const BOTTOM_BAR_HEIGHT = PADDING + BOTTOM_BAR_ALPHABET_MARGIN + CELL_HEIGHT; //% whenUsed=true const BOTTOM_BAR_BUTTON_WIDTH = PADDING * 2 + font.charWidth * 3; //% whenUsed=true const BOTTOM_BAR_TEXT_Y = (BOTTOM_BAR_HEIGHT - font.charHeight) / 2; //% whenUsed=true const BOTTOM_BAR_SHIFT_X = (BOTTOM_BAR_BUTTON_WIDTH - font.charWidth * 3) / 2; //% whenUsed=true const BOTTOM_BAR_CONFIRM_X = (BOTTOM_BAR_BUTTON_WIDTH - font.charWidth * 2) / 2; //% whenUsed=true const CONFIRM_BUTTON_LEFT = screen.width - BOTTOM_BAR_BUTTON_WIDTH; // Dimensions of the alphabet area //% whenUsed=true const ALPHABET_HEIGHT = NUM_ROWS * CELL_HEIGHT; //% whenUsed=true const ALPHABET_TOP = CONTENT_TOP + CONTENT_HEIGHT - ALPHABET_HEIGHT - BOTTOM_BAR_HEIGHT; //% whenUsed=true const ALPHABET_INPUT_MARGIN = 10; // Dimensions of area where text is input //% whenUsed=true const INPUT_HEIGHT = INPUT_ROWS * CELL_HEIGHT; //% whenUsed=true const INPUT_TOP = ALPHABET_TOP - INPUT_HEIGHT - ALPHABET_INPUT_MARGIN; //% whenUsed=true const lowerShiftText = "ABC"; //% whenUsed=true const upperShiftText = "abc"; //% whenUsed=true const digitsUpper = [" ", ",", ".", "?", "!", ":", ";", "\"", "(", ")"]; //% whenUsed=true const confirmText = "OK"; export class Prompt { theme: PromptTheme; message: string; answerLength: number; result: string; protected confirmPressed: boolean; protected cursorRow: number; protected cursorColumn: number; protected upper: boolean; protected useSystemKeyboard: boolean; protected renderable: scene.Renderable; protected selectionStart: number; protected selectionEnd: number; protected keyboardRows: number; protected keyboardColumns: number; private changeTime = 0; constructor(theme?: PromptTheme) { if (theme) { this.theme = theme; } else { this.theme = { colorPrompt: 1, colorInput: 3, colorInputHighlighted: 5, colorInputText: 1, colorAlphabet: 1, colorCursor: 7, colorBackground: 15, colorBottomBackground: 3, colorBottomText: 1, }; } this.cursorRow = 0; this.cursorColumn = 0; this.upper = false; this.result = ""; this.keyboardColumns = ALPHABET_ROW_LENGTH; this.keyboardRows = NUM_ROWS; this.selectionStart = 0; this.selectionEnd = 0; } show(message: string, answerLength: number, useOnScreenKeyboard = false) { this.message = message; this.answerLength = answerLength; controller._setUserEventsEnabled(false); game.pushScene() this.createRenderable(); this.confirmPressed = false; if (!useOnScreenKeyboard && control.deviceDalVersion() === "sim" && helpers._isSystemKeyboardSupported()) { this.useSystemKeyboard = true; helpers._promptForText(this.answerLength, this.numbersOnly()); this.selectionEnd = 0; this.selectionStart = 0; control.onEvent(_KEYBOARD_CHANGE_EVENT, 0, () => { this.result = helpers._getTextPromptString().substr(0, this.answerLength); this.changeTime = game.runtime(); this.selectionStart = helpers._getTextPromptSelectionStart(); this.selectionEnd = helpers._getTextPromptSelectionEnd(); }) let cancelled = false; let finished = false; control.onEvent(_KEYBOARD_CANCEL_EVENT, 0, () => { cancelled = true; }); control.onEvent(_KEYBOARD_ENTER_EVENT, 0, () => { finished = true; }); pauseUntil(() => cancelled || finished); if (cancelled) { this.useSystemKeyboard = false; this.selectionStart = this.result.length; this.selectionEnd = this.selectionStart; this.registerHandlers(); pauseUntil(() => this.confirmPressed); } } else { this.useSystemKeyboard = false; this.registerHandlers(); pauseUntil(() => this.confirmPressed); } game.popScene(); controller._setUserEventsEnabled(true); return this.result; } protected numbersOnly() { return false; } protected createRenderable() { if (this.renderable) { this.renderable.destroy(); } const promptText = new sprites.RenderText(this.message, CONTENT_WIDTH); let systemKeyboardText: sprites.RenderText; this.renderable = scene.createRenderable(-1, () => { promptText.draw(screen, (screen.width >> 1) - (promptText.width >> 1), CONTENT_TOP, this.theme.colorPrompt, 0, 2) this.drawInputArea(); if (!this.useSystemKeyboard) { this.drawKeyboard(); this.drawBottomBar(); return; } if (!systemKeyboardText) { systemKeyboardText = new sprites.RenderText(helpers._getLocalizedInstructions(), CONTENT_WIDTH); } screen.fillRect(0, screen.height - (PADDING << 1) - systemKeyboardText.height, screen.width, screen.height, this.theme.colorBottomBackground); systemKeyboardText.draw(screen, PADDING, screen.height - PADDING - systemKeyboardText.height, this.theme.colorBottomText); }); } protected drawInputArea() { const answerLeft = ROW_LEFT + Math.floor( ((CELL_WIDTH * ALPHABET_ROW_LENGTH) - CELL_WIDTH * Math.min(this.answerLength, ALPHABET_ROW_LENGTH)) / 2); for (let i = 0; i < this.answerLength; i++) { const col = i % ALPHABET_ROW_LENGTH; const row = Math.floor(i / ALPHABET_ROW_LENGTH); if (this.selectionStart !== this.selectionEnd && i >= this.selectionStart && i < this.selectionEnd) { screen.fillRect( answerLeft + col * CELL_WIDTH, INPUT_TOP + row * CELL_HEIGHT, CELL_WIDTH, CELL_HEIGHT, this.theme.colorCursor ); } screen.fillRect( answerLeft + col * CELL_WIDTH + BLANK_PADDING, INPUT_TOP + row * CELL_HEIGHT + CELL_HEIGHT - 1, CELL_WIDTH - BLANK_PADDING * 2, 1, !this.useSystemKeyboard && !this.blink() && i === this.selectionStart ? this.theme.colorInputHighlighted : this.theme.colorInput ); if (i < this.result.length) { const char = this.result.charAt(i); screen.print( char, answerLeft + col * CELL_WIDTH + LETTER_OFFSET_X, INPUT_TOP + row * CELL_HEIGHT + LETTER_OFFSET_Y, this.theme.colorInputText, font ); } } // draw the blinking text cursor if (this.useSystemKeyboard) { if (this.selectionStart === this.selectionEnd && this.selectionStart < this.answerLength) { const col = this.selectionStart % ALPHABET_ROW_LENGTH; const row = Math.floor(this.selectionStart / ALPHABET_ROW_LENGTH); if (!this.blink()) { screen.fillRect( answerLeft + col * CELL_WIDTH, INPUT_TOP + row * CELL_HEIGHT, 1, CELL_HEIGHT, this.theme.colorCursor ); } } } } protected drawKeyboard() { const top = screen.height - BOTTOM_BAR_HEIGHT - this.keyboardRows * CELL_HEIGHT - PADDING; const left = (screen.width >> 1) - ((CELL_WIDTH * this.keyboardColumns) >> 1) for (let j = 0; j < this.keyboardRows * this.keyboardColumns; j++) { const col = j % this.keyboardColumns; const row = Math.idiv(j, this.keyboardColumns); if (col === this.cursorColumn && row === this.cursorRow) { screen.fillRect( left + col * CELL_WIDTH, top + row * CELL_HEIGHT, CELL_WIDTH, CELL_HEIGHT, this.theme.colorCursor ) } screen.print( this.getSymbolForIndex(j), left + col * CELL_WIDTH + LETTER_OFFSET_X, top + row * CELL_HEIGHT + LETTER_OFFSET_Y, this.theme.colorAlphabet ) } } protected drawBottomBar() { this.drawBottomBarBackground(); this.drawShift(this.cursorRow === 3 && !(this.cursorColumn & 1)); this.drawConfirm(this.cursorRow === 3 && !!(this.cursorColumn & 1)); } protected drawBottomBarBackground() { screen.fillRect(0, screen.height - BOTTOM_BAR_HEIGHT, screen.width, BOTTOM_BAR_HEIGHT, this.theme.colorBottomBackground); } protected drawShift(highlighted: boolean) { if (highlighted) { screen.fillRect( 0, screen.height - BOTTOM_BAR_HEIGHT, BOTTOM_BAR_BUTTON_WIDTH, BOTTOM_BAR_HEIGHT, this.theme.colorCursor ); } let shiftText = lowerShiftText; if (this.upper) { shiftText = upperShiftText; } screen.print( shiftText, BOTTOM_BAR_SHIFT_X, screen.height - BOTTOM_BAR_HEIGHT + BOTTOM_BAR_TEXT_Y, this.theme.colorBottomText ) } protected drawConfirm(highlighted: boolean) { if (highlighted) { screen.fillRect( CONFIRM_BUTTON_LEFT, screen.height - BOTTOM_BAR_HEIGHT, BOTTOM_BAR_BUTTON_WIDTH, BOTTOM_BAR_HEIGHT, this.theme.colorCursor ); } screen.print( confirmText, CONFIRM_BUTTON_LEFT + BOTTOM_BAR_CONFIRM_X, screen.height - BOTTOM_BAR_HEIGHT + BOTTOM_BAR_TEXT_Y, this.theme.colorBottomText ) } protected getSymbolForIndex(index: number) { return getCharForIndex(index, this.upper); } private registerHandlers() { controller.up.onEvent(SYSTEM_KEY_DOWN, () => { this.moveVertical(true); }) controller.down.onEvent(SYSTEM_KEY_DOWN, () => { this.moveVertical(false); }) controller.right.onEvent(SYSTEM_KEY_DOWN, () => { this.moveHorizontal(true); }); controller.left.onEvent(SYSTEM_KEY_DOWN, () => { this.moveHorizontal(false); }); controller.A.onEvent(SYSTEM_KEY_DOWN, () => { this.confirm(); }); controller.B.onEvent(SYSTEM_KEY_DOWN, () => { this.delete(); }); } protected moveVertical(up: boolean) { if (up) { if (this.cursorRow === this.keyboardRows) { this.cursorRow = this.keyboardRows - 1; if (this.cursorColumn % 2) { this.cursorColumn = this.keyboardColumns - 1; } else { this.cursorColumn = 0; } } else { this.cursorRow = Math.max(0, this.cursorRow - 1); } } else { this.cursorRow = Math.min(this.keyboardRows, this.cursorRow + 1); if (this.cursorRow === this.keyboardRows) { // Go to closest button this.cursorColumn = this.cursorColumn > 5 ? 1 : 0; } } } protected moveHorizontal(right: boolean) { if (right) { this.cursorColumn = (this.cursorColumn + 1) % this.keyboardColumns; } else { this.cursorColumn = (this.cursorColumn + (this.keyboardColumns - 1)) % this.keyboardColumns; } } protected confirm() { if (this.cursorRow === 3) { if (this.cursorColumn % 2) { this.confirmPressed = true; } else { this.upper = !this.upper; } } else { if (this.selectionStart >= this.answerLength) return; const index = this.cursorColumn + this.cursorRow * this.keyboardColumns const letter = getCharForIndex(index, this.upper); if (!this.result) { this.result = letter; } else { this.result += letter; } this.changeTime = game.runtime(); this.changeInputIndex(1); } } protected delete() { if (this.selectionStart <= 0) return; this.result = this.result.substr(0, this.result.length - 1); this.changeInputIndex(-1); } protected changeInputIndex(delta: number) { this.selectionStart += delta; this.selectionEnd = this.selectionStart; } protected blink() { return Math.idiv(game.runtime() - this.changeTime, 500) & 1; } } function getCharForIndex(index: number, upper: boolean) { if (index < 26) { return String.fromCharCode(index + (upper ? 65 : 97)); } else { if (upper) { return digitsUpper[index - 26]; } else { return "" + (index - 26); } } } }