Consolidate editor export buttons into a menu

This commit is contained in:
Eevee (Evelyn Woods) 2021-04-28 19:25:49 -06:00
parent c1bf88d3dd
commit 58cc6ff61e
4 changed files with 164 additions and 112 deletions

View File

@ -70,6 +70,9 @@
<script>document.body.setAttribute('data-mode', 'loading');</script> <script>document.body.setAttribute('data-mode', 'loading');</script>
<svg id="svg-iconsheet"> <svg id="svg-iconsheet">
<defs> <defs>
<g id="svg-icon-menu-chevron">
<path d="M2,4 l6,6 l6,-6 v3 l-6,6 l-6,-6 z"></path>
</g>
<!-- Actions --> <!-- Actions -->
<g id="svg-icon-up"> <g id="svg-icon-up">
<path d="M0,12 l8,-8 l8,8 z"></path> <path d="M0,12 l8,-8 l8,8 z"></path>
@ -95,11 +98,13 @@
<path d="M 8,13 13,11 8,9 3,11 Z m 0,2 7,-3 V 11 L 8,8 1,11 v 1 z"></path> <path d="M 8,13 13,11 8,9 3,11 Z m 0,2 7,-3 V 11 L 8,8 1,11 v 1 z"></path>
<ellipse cx="5.5" cy="11" rx="0.75" ry="0.5"></ellipse> <ellipse cx="5.5" cy="11" rx="0.75" ry="0.5"></ellipse>
</g> </g>
<!-- Hint background -->
<g id="svg-icon-hint"> <g id="svg-icon-hint">
<path d="M1,8 a7,7 0 1,1 14,0 7,7 0 1,1 -14,0 M2,8 a6,6 0 1,0 12,0 6,6 0 1,0 -12,0"></path> <path d="M1,8 a7,7 0 1,1 14,0 7,7 0 1,1 -14,0 M2,8 a6,6 0 1,0 12,0 6,6 0 1,0 -12,0"></path>
<path d="M5,6 a1,1 0 0,0 2,0 a1,1 0 1,1 1,1 a1,1 0 0,0 -1,1 v1 a1,1 0 1,0 2,0 v-0.17 A3,3 0 1,0 5,6"></path> <path d="M5,6 a1,1 0 0,0 2,0 a1,1 0 1,1 1,1 a1,1 0 0,0 -1,1 v1 a1,1 0 1,0 2,0 v-0.17 A3,3 0 1,0 5,6"></path>
<circle cx="8" cy="12" r="1"></circle> <circle cx="8" cy="12" r="1"></circle>
</g> </g>
<!-- Editor stuff -->
<g id="svg-icon-zoom"> <g id="svg-icon-zoom">
<path d="M1,6 a5,5 0 1,1 10,0 a5,5 0 1,1 -10,0 m2,0 a3,3 0 1,0 6,0 a3,3 0 1,0 -6,0"></path> <path d="M1,6 a5,5 0 1,1 10,0 a5,5 0 1,1 -10,0 m2,0 a3,3 0 1,0 6,0 a3,3 0 1,0 -6,0"></path>
<path d="M14,12 l-2,2 -4,-4 2,-2 4,4"></path> <path d="M14,12 l-2,2 -4,-4 2,-2 4,4"></path>

View File

@ -130,6 +130,11 @@ export class Overlay {
// Use capture, which runs before any other event handler // Use capture, which runs before any other event handler
window.addEventListener('keydown', this.keydown_handler, true); window.addEventListener('keydown', this.keydown_handler, true);
// Block mouse movement as well
overlay.addEventListener('mousemove', ev => {
ev.stopPropagation();
});
return overlay; return overlay;
} }
@ -159,6 +164,59 @@ export class TransientOverlay extends Overlay {
} }
} }
export class MenuOverlay extends TransientOverlay {
constructor(conductor, items, make_label, onclick) {
super(conductor, mk('ol.popup-menu'));
for (let [i, item] of items.entries()) {
this.root.append(mk('li', {'data-index': i}, make_label(item)));
}
this.root.addEventListener('click', ev => {
let li = ev.target.closest('li');
if (! li || ! this.root.contains(li))
return;
let i = parseInt(li.getAttribute('data-index'), 10);
let item = items[i];
onclick(item);
this.close();
});
}
open(relto) {
super.open();
let anchor = relto.getBoundingClientRect();
let rect = this.root.getBoundingClientRect();
// Prefer left anchoring, but use right if that would go off the screen
if (anchor.left + rect.width > document.body.clientWidth) {
this.root.style.right = `${document.body.clientWidth - anchor.right}px`;
}
else {
this.root.style.left = `${anchor.left}px`;
}
// Open vertically in whichever direction has more space (with a slight bias towards opening
// downwards). If we would then run off the screen, also set the other anchor to constrain
// the height.
let top_space = anchor.top - 0;
let bottom_space = document.body.clientHeight - anchor.bottom;
if (top_space > bottom_space) {
this.root.style.bottom = `${document.body.clientHeight - anchor.top}px`;
if (rect.height > top_space) {
this.root.style.top = `${0}px`;
}
}
else {
this.root.style.top = `${anchor.bottom}px`;
if (rect.height > bottom_space) {
this.root.style.bottom = `${0}px`;
}
}
}
}
// Overlay styled like a dialog box // Overlay styled like a dialog box
export class DialogOverlay extends Overlay { export class DialogOverlay extends Overlay {
constructor(conductor) { constructor(conductor) {

View File

@ -5,7 +5,7 @@ import { TILES_WITH_PROPS } from './editor-tile-overlays.js';
import * as format_base from './format-base.js'; import * as format_base from './format-base.js';
import * as c2g from './format-c2g.js'; import * as c2g from './format-c2g.js';
import * as dat from './format-dat.js'; import * as dat from './format-dat.js';
import { PrimaryView, TransientOverlay, DialogOverlay, AlertOverlay, flash_button, load_json_from_storage, save_json_to_storage } from './main-base.js'; import { PrimaryView, MenuOverlay, DialogOverlay, AlertOverlay, flash_button, load_json_from_storage, save_json_to_storage } from './main-base.js';
import CanvasRenderer from './renderer-canvas.js'; import CanvasRenderer from './renderer-canvas.js';
import TILE_TYPES from './tiletypes.js'; import TILE_TYPES from './tiletypes.js';
import { SVG_NS, mk, mk_button, mk_svg, string_from_buffer_ascii, bytestring_to_buffer, walk_grid } from './util.js'; import { SVG_NS, mk, mk_button, mk_svg, string_from_buffer_ascii, bytestring_to_buffer, walk_grid } from './util.js';
@ -3622,123 +3622,97 @@ export class Editor extends PrimaryView {
this.save_level(); this.save_level();
}); });
_make_button("Download level as C2M", ev => { let export_items = [
// TODO support getting warnings + errors out of synthesis ["Share this level with a link", () => {
let buf = c2g.synthesize_level(this.stored_level); // FIXME enable this once gliderbot can understand it
let blob = new Blob([buf]); //let data = util.b64encode(fflate.zlibSync(new Uint8Array(c2g.synthesize_level(this.stored_level))));
let url = URL.createObjectURL(blob); let data = util.b64encode(c2g.synthesize_level(this.stored_level));
// To download a file, um, make an <a> and click it. Not kidding let url = new URL(location);
let a = mk('a', { url.searchParams.delete('level');
href: url, url.searchParams.delete('setpath');
download: (this.stored_level.title || 'untitled') + '.c2m', url.searchParams.append('level', data);
}); new EditorShareOverlay(this.conductor, url.toString()).open();
document.body.append(a); }],
a.click(); ["Download level as C2M", () => {
// Absolutely no idea when I'm allowed to revoke this, but surely a minute is safe // TODO support getting warnings + errors out of synthesis
window.setTimeout(() => { let buf = c2g.synthesize_level(this.stored_level);
a.remove(); util.trigger_local_download((this.stored_level.title || 'untitled') + '.c2m', new Blob([buf]));
URL.revokeObjectURL(url); }],
}, 60 * 1000); ["Download level as MSCC DAT/CCL", () => {
}); // TODO support getting warnings out of synthesis?
_make_button("Download level as MSCC DAT", ev => { let buf;
// TODO support getting warnings out of synthesis? try {
let buf; buf = dat.synthesize_level(this.stored_level);
try {
buf = dat.synthesize_level(this.stored_level);
}
catch (errs) {
if (errs instanceof dat.CCLEncodingErrors) {
new EditorExportFailedOverlay(this.conductor, errs.errors).open();
return;
} }
throw errs; catch (errs) {
} if (errs instanceof dat.CCLEncodingErrors) {
new EditorExportFailedOverlay(this.conductor, errs.errors).open();
let blob = new Blob([buf]); return;
let url = URL.createObjectURL(blob); }
// To download a file, um, make an <a> and click it. Not kidding throw errs;
let a = mk('a', {
href: url,
download: (this.stored_level.title || 'untitled') + '.ccl',
});
document.body.append(a);
a.click();
// Absolutely no idea when I'm allowed to revoke this, but surely a minute is safe
window.setTimeout(() => {
a.remove();
URL.revokeObjectURL(url);
}, 60 * 1000);
});
_make_button("Download pack", ev => {
let stored_pack = this.conductor.stored_game;
// This is pretty heckin' best-effort for now; TODO move into format-c2g?
let lines = [];
let safe_title = (stored_pack.title || "untitled").replace(/[""]/g, "'").replace(/[\x00-\x1f]+/g, "_");
lines.push(`game "${safe_title}"`);
let files = {};
let count = stored_pack.level_metadata.length;
let levelnumlen = String(count).length;
for (let [i, meta] of stored_pack.level_metadata.entries()) {
let c2m;
if (i === this.conductor.level_index) {
// Use the current state of the current level even if it's not been saved
c2m = new Uint8Array(c2g.synthesize_level(this.stored_level));
} }
else if (meta.key) { util.trigger_local_download((this.stored_level.title || 'untitled') + '.ccl', new Blob([buf]));
// This is already in localStorage as a c2m }],
c2m = fflate.strToU8(localStorage.getItem(meta.key), true); ["Download pack as C2G", () => {
} let stored_pack = this.conductor.stored_game;
else {
let stored_level = stored_pack.load_level(i); // This is pretty heckin' best-effort for now; TODO move into format-c2g?
c2m = new Uint8Array(c2g.synthesize_level(stored_level)); let lines = [];
let safe_title = (stored_pack.title || "untitled").replace(/[""]/g, "'").replace(/[\x00-\x1f]+/g, "_");
lines.push(`game "${safe_title}"`);
let files = {};
let count = stored_pack.level_metadata.length;
let levelnumlen = String(count).length;
for (let [i, meta] of stored_pack.level_metadata.entries()) {
let c2m;
if (i === this.conductor.level_index) {
// Use the current state of the current level even if it's not been saved
c2m = new Uint8Array(c2g.synthesize_level(this.stored_level));
}
else if (meta.key) {
// This is already in localStorage as a c2m
c2m = fflate.strToU8(localStorage.getItem(meta.key), true);
}
else {
let stored_level = stored_pack.load_level(i);
c2m = new Uint8Array(c2g.synthesize_level(stored_level));
}
let safe_title = meta.title.replace(/[\x00-\x1f<>:""\/\\|?*]+/g, '_');
let dirchunk = i - i % 50;
let dirname = (
String(dirchunk + 1).padStart(levelnumlen, '0') + '-' +
String(Math.min(count, dirchunk + 50)).padStart(levelnumlen, '0'));
let filename = `${dirname}/${i + 1} - ${safe_title}.c2m`;
files[filename] = c2m;
lines.push(`map "${filename}"`);
} }
let safe_title = meta.title.replace(/[\x00-\x1f<>:""\/\\|?*]+/g, '_'); // TODO utf8 encode this
let dirchunk = i - i % 50; safe_title = safe_title.replace(/[\x00-\x1f<>:""\/\\|?*]+/g, '_');
let dirname = ( lines.push("");
String(dirchunk + 1).padStart(levelnumlen, '0') + '-' + files[safe_title + '.c2g'] = fflate.strToU8(lines.join("\n"));
String(Math.min(count, dirchunk + 50)).padStart(levelnumlen, '0')); let u8array = fflate.zipSync(files);
let filename = `${dirname}/${i + 1} - ${safe_title}.c2m`;
files[filename] = c2m;
lines.push(`map "${filename}"`); // TODO support getting warnings + errors out of synthesis
} util.trigger_local_download((stored_pack.title || 'untitled') + '.zip', new Blob([u8array]));
}],
// TODO utf8 encode this ];
safe_title = safe_title.replace(/[\x00-\x1f<>:""\/\\|?*]+/g, '_'); this.export_menu = new MenuOverlay(
lines.push(""); this.conductor,
files[safe_title + '.c2g'] = fflate.strToU8(lines.join("\n")); export_items,
let u8array = fflate.zipSync(files); item => item[0],
item => item[1](),
// TODO also allow download as CCL );
// TODO support getting warnings + errors out of synthesis let export_menu_button = _make_button("Export ", ev => {
let blob = new Blob([u8array]); this.export_menu.open(ev.currentTarget);
let url = URL.createObjectURL(blob);
// To download a file, um, make an <a> and click it. Not kidding
let a = mk('a', {
href: url,
download: (stored_pack.title || 'untitled') + '.zip',
});
document.body.append(a);
a.click();
// Absolutely no idea when I'm allowed to revoke this, but surely a minute is safe
window.setTimeout(() => {
a.remove();
URL.revokeObjectURL(url);
}, 60 * 1000);
});
_make_button("Share", ev => {
// FIXME enable this once gliderbot can understand it
//let data = util.b64encode(fflate.zlibSync(new Uint8Array(c2g.synthesize_level(this.stored_level))));
let data = util.b64encode(c2g.synthesize_level(this.stored_level));
let url = new URL(location);
url.searchParams.delete('level');
url.searchParams.delete('setpath');
url.searchParams.append('level', data);
new EditorShareOverlay(this.conductor, url.toString()).open();
}); });
export_menu_button.append(
mk_svg('svg.svg-icon', {viewBox: '0 0 16 16'},
mk_svg('use', {href: `#svg-icon-menu-chevron`})),
);
//_make_button("Toggle green objects"); //_make_button("Toggle green objects");
// Tile palette // Tile palette
@ -4821,6 +4795,7 @@ export class Editor extends PrimaryView {
overlay.edit_tile(tile, cell); overlay.edit_tile(tile, cell);
overlay.open(); overlay.open();
// Fixed-size balloon positioning
// FIXME move this into TransientOverlay or some other base class // FIXME move this into TransientOverlay or some other base class
let root = overlay.root; let root = overlay.root;
let spacing = 2; let spacing = 2;

View File

@ -240,6 +240,20 @@ svg.svg-icon {
justify-content: start; justify-content: start;
background: none; background: none;
} }
.popup-menu {
position: absolute;
border: 1px solid black;
color: black;
background: hsl(225, 10%, 90%);
box-shadow: 0 1px 3px 1px #0009;
}
.popup-menu > li {
padding: 0.25em 0.5em;
cursor: pointer;
}
.popup-menu > li:hover {
background: hsl(225, 40%, 75%);
}
.dialog { .dialog {
display: flex; display: flex;
flex-direction: column; flex-direction: column;