8bitworkshop/src/ui.ts

1221 lines
35 KiB
TypeScript
Raw Normal View History

"use strict";
2016-12-16 01:21:51 +00:00
// 8bitworkshop IDE user interface
2018-07-08 03:10:51 +00:00
import $ = require("jquery");
import * as bootstrap from "bootstrap";
2018-07-08 14:07:19 +00:00
import { CodeProject } from "./project";
import { WorkerResult, SourceFile, WorkerError } from "./workertypes";
2018-07-08 03:10:51 +00:00
import { ProjectWindows } from "./windows";
2018-09-17 20:09:09 +00:00
import { Platform, Preset, DebugSymbols } from "./baseplatform";
import { PLATFORMS } from "./emu";
2018-07-08 03:10:51 +00:00
import * as Views from "./views";
import { createNewPersistentStore } from "./store";
import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap, byteArrayToString, compressLZG } from "./util";
import { StateRecorderImpl } from "./recorder";
2018-07-08 03:10:51 +00:00
// external libs (TODO)
declare var ga, Tour, GIF, saveAs, JSZip, Mousetrap;
// in index.html
declare var exports;
2018-07-08 03:10:51 +00:00
// make sure VCS doesn't start
if (window['Javatari']) window['Javatari'].AUTO_START = false;
var PRESETS : Preset[]; // presets array
export var platform_id : string; // platform ID string
export var platform : Platform; // platform object
2017-01-13 02:21:35 +00:00
var toolbar = $("#controls_top");
export var current_project : CodeProject; // current CodeProject object
2018-07-08 03:10:51 +00:00
var projectWindows : ProjectWindows; // window manager
var stateRecorder : StateRecorderImpl;
// TODO: codemirror multiplex support?
var TOOL_TO_SOURCE_STYLE = {
'dasm': '6502',
'acme': '6502',
'cc65': 'text/x-csrc',
'ca65': '6502',
'z80asm': 'z80',
'sdasz80': 'z80',
'sdcc': 'text/x-csrc',
2017-11-11 19:45:32 +00:00
'verilator': 'verilog',
'jsasm': 'z80',
'zmac': 'z80',
'bataribasic': 'bataribasic',
}
2018-07-08 14:07:19 +00:00
function newWorker() : Worker {
return new Worker("./src/worker/loader.js");
}
2018-06-28 04:57:06 +00:00
2018-07-08 03:10:51 +00:00
var userPaused : boolean; // did user explicitly pause?
2018-06-28 04:57:06 +00:00
2018-07-08 03:10:51 +00:00
var current_output; // current ROM
var current_preset_entry : Preset; // current preset object (if selected)
var main_file_id : string; // main file ID
var store; // persistent store
2017-01-08 15:51:19 +00:00
export var compparams; // received build params from worker
export var lastDebugState; // last debug state (object)
var lastDebugInfo; // last debug info (CPU text)
2018-07-29 20:26:05 +00:00
var debugCategory; // current debug category
2017-04-19 01:18:53 +00:00
2018-07-08 14:07:19 +00:00
function getCurrentPresetTitle() : string {
if (!current_preset_entry)
return main_file_id || "ROM";
2016-12-16 01:21:51 +00:00
else
return current_preset_entry.title || current_preset_entry.name || main_file_id || "ROM";
2016-12-16 01:21:51 +00:00
}
2018-07-08 14:07:19 +00:00
function setLastPreset(id:string) {
if (platform_id != 'base_z80') { // TODO
localStorage.setItem("__lastplatform", platform_id);
localStorage.setItem("__lastid_"+platform_id, id);
}
2016-12-16 01:21:51 +00:00
}
2018-06-29 23:44:04 +00:00
function initProject() {
current_project = new CodeProject(newWorker(), platform_id, platform, store);
projectWindows = new ProjectWindows($("#workspace")[0] as HTMLElement, current_project);
current_project.callbackGetRemote = $.get;
2018-07-26 13:43:49 +00:00
current_project.callbackBuildResult = (result:WorkerResult) => {
setCompileOutput(result);
refreshWindowList();
};
current_project.callbackBuildStatus = (busy:boolean) => {
if (busy) {
toolbar.addClass("is-busy");
} else {
toolbar.removeClass("is-busy");
toolbar.removeClass("has-errors"); // may be added in next callback
2018-07-26 13:43:49 +00:00
projectWindows.setErrors(null);
$("#error_alert").hide();
}
$('#compile_spinner').css('visibility', busy ? 'visible' : 'hidden');
};
2018-06-29 23:44:04 +00:00
}
function refreshWindowList() {
var ul = $("#windowMenuList").empty();
var separate = false;
function addWindowItem(id, name, createfn) {
if (separate) {
ul.append(document.createElement("hr"));
separate = false;
}
var li = document.createElement("li");
var a = document.createElement("a");
a.setAttribute("class", "dropdown-item");
a.setAttribute("href", "#");
if (id == projectWindows.getActiveID())
$(a).addClass("dropdown-item-checked");
a.appendChild(document.createTextNode(name));
li.appendChild(a);
ul.append(li);
if (createfn) {
projectWindows.setCreateFunc(id, createfn);
$(a).click(function(e) {
projectWindows.createOrShow(id);
ul.find('a').removeClass("dropdown-item-checked");
ul.find(e.target).addClass("dropdown-item-checked");
});
}
}
2018-07-08 14:07:19 +00:00
function loadEditor(path:string) {
var tool = platform.getToolForFilename(path);
var mode = tool && TOOL_TO_SOURCE_STYLE[tool];
2018-07-08 03:10:51 +00:00
return new Views.SourceEditor(path, mode);
}
2018-07-04 01:09:58 +00:00
// add main file editor
var id = main_file_id;
addWindowItem(id, getFilenameForPath(id), loadEditor);
2018-07-04 01:09:58 +00:00
// add other source files
separate = true;
current_project.iterateFiles(function(id, text) {
if (text && id != main_file_id)
addWindowItem(id, getFilenameForPath(id), loadEditor);
});
2018-07-04 01:09:58 +00:00
// add listings
// TODO: update listing when recompiling
2018-07-04 01:09:58 +00:00
var listings = current_project.getListings();
if (listings) {
for (var lstfn in listings) {
var lst = listings[lstfn];
// TODO: add assembly listings? (lines, macrolines, sourcefile)
2018-07-04 01:09:58 +00:00
if (lst.assemblyfile) {
addWindowItem(lstfn, getFilenameForPath(lstfn), function(path) {
return new Views.ListingView(lstfn);
2018-07-04 01:09:58 +00:00
});
}
}
}
// add other tools
separate = true;
2018-07-04 01:09:58 +00:00
if (platform.disassemble) {
addWindowItem("#disasm", "Disassembly", function() {
2018-07-08 03:10:51 +00:00
return new Views.DisassemblerView();
});
}
if (platform.readAddress) {
addWindowItem("#memory", "Memory Browser", function() {
2018-07-08 03:10:51 +00:00
return new Views.MemoryView();
});
}
}
2018-06-28 04:57:06 +00:00
// can pass integer or string id
2018-07-08 14:07:19 +00:00
function loadProject(preset_id:string) {
2018-06-28 04:57:06 +00:00
var index = parseInt(preset_id+""); // might fail -1
2016-12-16 01:21:51 +00:00
for (var i=0; i<PRESETS.length; i++)
if (PRESETS[i].id == preset_id)
index = i;
index = (index + PRESETS.length) % PRESETS.length;
if (index >= 0) {
// load the preset
2018-06-29 23:44:04 +00:00
current_preset_entry = PRESETS[index];
preset_id = current_preset_entry.id;
2016-12-16 01:21:51 +00:00
}
// set current file ID
main_file_id = preset_id;
setLastPreset(preset_id);
current_project.mainPath = preset_id;
// load files from storage or web URLs
2018-06-29 23:44:04 +00:00
current_project.loadFiles([preset_id], function(err, result) {
if (err) {
alert(err);
} else if (result && result.length) {
// we need this to build create functions for the editor (TODO?)
refreshWindowList();
// show main file
projectWindows.createOrShow(preset_id);
// build project
current_project.setMainFile(preset_id);
2018-06-29 23:44:04 +00:00
}
});
2016-12-16 01:21:51 +00:00
}
2018-07-08 14:07:19 +00:00
function reloadPresetNamed(id:string) {
qs['platform'] = platform_id;
qs['file'] = id;
gotoNewLocation();
2016-12-16 01:21:51 +00:00
}
2018-07-08 14:07:19 +00:00
function getSkeletonFile(fileid:string, callback) {
2018-06-29 23:44:04 +00:00
var ext = platform.getToolForFilename(fileid);
// TODO: .mame
2018-06-29 23:44:04 +00:00
$.get( "presets/"+platform_id+"/skeleton."+ext, function( text ) {
callback(null, text);
}, 'text')
.fail(function() {
alert("Could not load skeleton for " + platform_id + "/" + ext + "; using blank file");
callback(null, '\n');
2018-06-29 23:44:04 +00:00
});
}
2016-12-16 01:21:51 +00:00
function _createNewFile(e) {
// TODO: support spaces
var filename = prompt("Create New File", "newfile" + platform.getDefaultExtension());
2016-12-16 01:21:51 +00:00
if (filename && filename.length) {
if (filename.indexOf(" ") >= 0) {
alert("No spaces, please.");
return;
}
2016-12-16 01:21:51 +00:00
if (filename.indexOf(".") < 0) {
2017-04-20 00:55:13 +00:00
filename += platform.getDefaultExtension();
2016-12-16 01:21:51 +00:00
}
2018-06-29 23:44:04 +00:00
var path = "local/" + filename;
getSkeletonFile(path, function(err, result) {
if (result) {
store.setItem(path, result, function(err, result) {
if (err)
alert(err+"");
if (result != null)
reloadPresetNamed("local/" + filename);
2018-06-29 23:44:04 +00:00
});
}
});
2016-12-16 01:21:51 +00:00
}
2016-12-30 23:51:15 +00:00
return true;
2016-12-16 01:21:51 +00:00
}
2018-06-26 23:57:03 +00:00
function _uploadNewFile(e) {
$("#uploadFileElem").click();
}
2018-07-08 14:07:19 +00:00
function handleFileUpload(files: File[]) {
2018-06-26 23:57:03 +00:00
console.log(files);
var index = 0;
function uploadNextFile() {
var f = files[index++];
if (!f) {
console.log("Done uploading");
gotoNewLocation();
} else {
var path = "local/" + f.name;
var reader = new FileReader();
reader.onload = function(e) {
var data = (<any>e.target).result;
2018-06-26 23:57:03 +00:00
store.setItem(path, data, function(err, result) {
if (err)
console.log(err);
else {
console.log("Uploaded " + path + " " + data.length + " bytes");
if (index == 1)
qs['file'] = path;
uploadNextFile();
}
});
}
reader.readAsText(f);
}
}
if (files) uploadNextFile();
}
function getCurrentMainFilename() : string {
return getFilenameForPath(main_file_id);
}
function getCurrentEditorFilename() : string {
return getFilenameForPath(projectWindows.getActiveID());
2017-02-02 19:11:52 +00:00
}
function _shareFileAsGist(e) {
loadScript("octokat.js/dist/octokat.js", () => {
if (current_output == null) { // TODO
2016-12-30 23:51:15 +00:00
alert("Please fix errors before sharing.");
return true;
}
var text = projectWindows.getCurrentText();
if (!text) return false;
var github = new exports['Octokat']();
2017-01-25 17:30:05 +00:00
var files = {};
files[getCurrentEditorFilename()] = {"content": text};
2017-01-25 17:30:05 +00:00
var gistdata = {
"description": '8bitworkshop.com {"platform":"' + platform_id + '"}',
"public": true,
"files": files
};
var gist = github.gists.create(gistdata).done(function(val) {
var url = "http://8bitworkshop.com/?sharekey=" + val.id;
window.prompt("Copy link to clipboard (Ctrl+C, Enter)", url);
}).fail(function(err) {
alert("Error sharing file: " + err.message);
2016-12-16 01:21:51 +00:00
});
});
}
function _shareEmbedLink(e) {
if (current_output == null) { // TODO
alert("Please fix errors before sharing.");
return true;
}
loadScript('lib/clipboard.min.js', () => {
var ClipboardJS = exports['ClipboardJS'];
new ClipboardJS(".btn");
});
loadScript('lib/liblzg.js', () => {
// TODO: Module is bad var name (conflicts with MAME)
var lzgrom = compressLZG( window['Module'], current_output );
window['Module'] = null; // so we load it again next time
var lzgb64 = btoa(byteArrayToString(lzgrom));
var embed = {
p: platform_id,
//n: main_file_id,
r: lzgb64
};
var linkqs = $.param(embed);
console.log(linkqs);
var loc = window.location;
var prefix = loc.pathname.replace('index.html','');
var protocol = (loc.host == '8bitworkshop.com') ? 'https:' : loc.protocol;
var fulllink = protocol+'//'+loc.host+prefix+'embed.html?' + linkqs;
var iframelink = '<iframe width=640 height=600 src="' + fulllink + '">';
$("#embedLinkTextarea").text(fulllink);
$("#embedIframeTextarea").text(iframelink);
$("#embedLinkModal").modal('show');
$("#embedAdviceWarnAll").hide();
$("#embedAdviceWarnIE").hide();
if (fulllink.length >= 65536) $("#embedAdviceWarnAll").show();
else if (fulllink.length >= 5120) $("#embedAdviceWarnIE").show();
});
2016-12-30 23:51:15 +00:00
return true;
2016-12-16 01:21:51 +00:00
}
2018-09-25 23:46:24 +00:00
function _downloadCassetteFile(e) {
if (current_output == null) { // TODO
alert("Please fix errors before exporting.");
return true;
}
var addr = compparams && compparams.code_start;
if (addr === undefined) {
alert("Cassette export is not supported on this platform.");
return true;
}
loadScript('lib/c2t.js', () => {
var stdout = '';
var print_fn = function(s) { stdout += s + "\n"; }
var c2t = window['c2t']({
noInitialRun:true,
print:print_fn,
printErr:print_fn
});
var FS = c2t['FS'];
var rompath = getCurrentMainFilename() + ".bin";
var audpath = getCurrentMainFilename() + ".wav";
FS.writeFile(rompath, current_output, {encoding:'binary'});
var args = ["-2bc", rompath+','+addr.toString(16), audpath];
c2t.callMain(args);
var audout = FS.readFile(audpath, {'encoding':'binary'});
if (audout) {
var blob = new Blob([audout], {type: "audio/wav"});
saveAs(blob, audpath);
2018-09-26 01:16:01 +00:00
stdout += "Then connect your audio output to the cassette input, turn up the volume, and play the audio file.";
2018-09-25 23:46:24 +00:00
alert(stdout);
}
});
}
function fixFilename(fn : string) : string {
if (platform_id.startsWith('vcs') && fn.indexOf('.') <= 0)
fn += ".a"; // legacy stuff
return fn;
}
2018-08-21 14:16:47 +00:00
function _revertFile(e) {
var fn = fixFilename(projectWindows.getActiveID());
// TODO: .mame
2018-08-21 14:16:47 +00:00
$.get( "presets/"+platform_id+"/"+fn, function(text) {
if (confirm("Reset '" + fn + "' to default?")) {
projectWindows.getActive().setText(text);
}
}, 'text')
.fail(function() {
alert("Can only revert built-in files.");
});
2016-12-30 23:51:15 +00:00
return true;
2016-12-16 01:21:51 +00:00
}
2017-02-02 19:11:52 +00:00
function _downloadROMImage(e) {
if (current_output == null) {
alert("Please finish compiling with no errors before downloading ROM.");
2017-02-02 19:11:52 +00:00
return true;
}
if (current_output.code) { // TODO
var blob = new Blob([current_output.code], {type: "text/plain"});
saveAs(blob, getCurrentMainFilename()+".js");
} else {
var blob = new Blob([current_output], {type: "application/octet-stream"});
saveAs(blob, getCurrentMainFilename()+".rom");
}
2017-02-02 19:11:52 +00:00
}
function _downloadSourceFile(e) {
var text = projectWindows.getCurrentText();
if (!text) return false;
var blob = new Blob([text], {type: "text/plain;charset=utf-8"});
saveAs(blob, getCurrentEditorFilename());
}
2018-08-25 18:29:51 +00:00
function _downloadProjectZipFile(e) {
loadScript('lib/jszip.min.js', () => {
var zip = new JSZip();
current_project.iterateFiles(function(id, text) {
if (text)
zip.file(getFilenameForPath(id), text);
});
zip.generateAsync({type:"blob"}).then( (content) => {
saveAs(content, getCurrentMainFilename() + ".zip");
});
});
}
function _downloadAllFilesZipFile(e) {
loadScript('lib/jszip.min.js', () => {
var zip = new JSZip();
var count = 0;
store.keys( (err, keys : string[]) => {
if (err) throw err;
keys.forEach((path) => {
store.getItem(path, (err, text) => {
if (text) {
zip.file(fixFilename(getFilenameForPath(path)), text);
}
if (++count == keys.length) {
zip.generateAsync({type:"blob"}).then( (content) => {
saveAs(content, platform_id + "-all.zip");
});
}
});
});
});
});
}
2016-12-16 01:21:51 +00:00
function populateExamples(sel) {
2018-06-26 06:56:36 +00:00
// make sure to use callback so it follows other sections
store.length(function(err, len) {
2018-07-08 03:10:51 +00:00
sel.append($("<option />").text("--------- Examples ---------").attr('disabled','true'));
2018-06-26 06:56:36 +00:00
for (var i=0; i<PRESETS.length; i++) {
var preset = PRESETS[i];
var name = preset.chapter ? (preset.chapter + ". " + preset.name) : preset.name;
2018-07-08 03:10:51 +00:00
sel.append($("<option />").val(preset.id).text(name).attr('selected',(preset.id==main_file_id)?'selected':null));
2018-06-26 06:56:36 +00:00
}
});
2016-12-16 01:21:51 +00:00
}
2018-06-26 06:56:36 +00:00
function populateFiles(sel, category, prefix) {
2018-07-08 03:10:51 +00:00
store.keys(function(err, keys : string[]) {
2018-06-26 06:56:36 +00:00
var foundSelected = false;
var numFound = 0;
if (!keys) keys = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key.startsWith(prefix)) {
if (numFound++ == 0)
2018-07-08 03:10:51 +00:00
sel.append($("<option />").text("------- " + category + " -------").attr('disabled','true'));
2018-06-26 06:56:36 +00:00
var name = key.substring(prefix.length);
2018-07-08 03:10:51 +00:00
sel.append($("<option />").val(key).text(name).attr('selected',(key==main_file_id)?'selected':null));
if (key == main_file_id) foundSelected = true;
2018-06-26 06:56:36 +00:00
}
}
if (!foundSelected && main_file_id && main_file_id.startsWith(prefix)) {
2018-07-08 03:10:51 +00:00
var name = main_file_id.substring(prefix.length);
2018-06-26 06:56:36 +00:00
var key = prefix + name;
2018-07-08 03:10:51 +00:00
sel.append($("<option />").val(key).text(name).attr('selected','true'));
2018-06-26 06:56:36 +00:00
}
});
2016-12-16 01:21:51 +00:00
}
function updateSelector() {
var sel = $("#preset_select").empty();
if (platform_id != 'base_z80') { // TODO
populateFiles(sel, "Local Files", "local/");
populateFiles(sel, "Shared", "shared/");
}
2016-12-16 01:21:51 +00:00
populateExamples(sel);
// set click handlers
sel.off('change').change(function(e) {
2018-06-28 04:57:06 +00:00
reloadPresetNamed($(this).val());
2016-12-16 01:21:51 +00:00
});
}
function showErrorAlert(errors : WorkerError[]) {
var div = $("#error_alert_msg").empty();
for (var err of errors.slice(0,10)) {
var s = '';
if (err.path) s += err.path + ":";
if (err.line) s += err.line + ":";
s += err.msg;
div.append($("<p>").text(s));
}
$("#error_alert").show();
}
2018-07-26 13:43:49 +00:00
function setCompileOutput(data: WorkerResult) {
// errors? mark them in editor
2018-06-25 23:47:40 +00:00
if (data.errors && data.errors.length > 0) {
2018-07-03 04:21:08 +00:00
toolbar.addClass("has-errors");
projectWindows.setErrors(data.errors);
showErrorAlert(data.errors);
2016-12-16 01:21:51 +00:00
} else {
// process symbol map
2018-09-17 20:09:09 +00:00
platform.debugSymbols = new DebugSymbols(data.symbolmap);
compparams = data.params;
2016-12-16 01:21:51 +00:00
// load ROM
var rom = data.output;
if (rom) { // TODO instanceof Uint8Array) {
2016-12-16 01:21:51 +00:00
try {
clearBreakpoint(); // so we can replace memory (TODO: change toolbar btn)
2018-08-23 22:52:56 +00:00
_resetRecording();
2016-12-31 16:05:22 +00:00
platform.loadROM(getCurrentPresetTitle(), rom);
2016-12-16 01:21:51 +00:00
current_output = rom;
if (!userPaused) _resume();
2018-08-02 17:08:37 +00:00
// TODO: reset profiler etc? (Tell views?)
2016-12-16 01:21:51 +00:00
} catch (e) {
console.log(e);
toolbar.addClass("has-errors");
showErrorAlert([{msg:e+"",line:0}]);
2016-12-16 01:21:51 +00:00
current_output = null;
return;
2016-12-16 01:21:51 +00:00
}
}
// update all windows (listings)
2018-08-02 17:08:37 +00:00
projectWindows.refresh(false);
2016-12-16 01:21:51 +00:00
}
}
2018-07-29 20:26:05 +00:00
function showDebugInfo(state?) {
2018-07-29 19:53:50 +00:00
var meminfo = $("#mem_info");
2018-07-29 20:26:05 +00:00
var allcats = platform.getDebugCategories && platform.getDebugCategories();
if (allcats && !debugCategory)
debugCategory = allcats[0];
var s = state && platform.getDebugInfo && platform.getDebugInfo(debugCategory, state);
if (s) {
2016-12-16 01:21:51 +00:00
var hs = lastDebugInfo ? highlightDifferences(lastDebugInfo, s) : s;
2018-07-29 19:53:50 +00:00
meminfo.show().html(hs);
2018-07-29 20:26:05 +00:00
var catspan = $('<span>');
var addCategoryLink = (cat:string) => {
var catlink = $('<a>'+cat+'</a>');
if (cat == debugCategory)
catlink.addClass('selected');
catlink.click((e) => {
debugCategory = cat;
lastDebugInfo = null;
showDebugInfo(lastDebugState);
});
catspan.append(catlink);
catspan.append('<span> </span>');
}
for (var cat of allcats) {
addCategoryLink(cat);
}
meminfo.append('<br>');
meminfo.append(catspan);
2016-12-16 01:21:51 +00:00
lastDebugInfo = s;
} else {
2018-07-29 19:53:50 +00:00
meminfo.hide();
2016-12-16 01:21:51 +00:00
lastDebugInfo = null;
}
}
2018-07-08 14:07:19 +00:00
function setDebugButtonState(btnid:string, btnstate:string) {
$("#debug_bar").find("button").removeClass("btn_active").removeClass("btn_stopped");
$("#dbg_"+btnid).addClass("btn_"+btnstate);
}
2018-09-15 19:27:12 +00:00
function setupDebugCallback(btnid? : string) {
if (platform.setupDebug) platform.setupDebug((state) => {
2017-01-06 14:49:07 +00:00
lastDebugState = state;
2018-07-29 20:26:05 +00:00
showDebugInfo(state);
2018-08-02 17:08:37 +00:00
projectWindows.refresh(true);
2018-09-15 19:27:12 +00:00
setDebugButtonState(btnid||"pause", "stopped");
2016-12-31 16:05:22 +00:00
});
2018-09-15 19:27:12 +00:00
}
function setupBreakpoint(btnid? : string) {
_disableRecording();
setupDebugCallback(btnid);
if (btnid) setDebugButtonState(btnid, "active");
2016-12-16 01:21:51 +00:00
}
2017-11-16 17:08:06 +00:00
function _pause() {
2016-12-31 16:05:22 +00:00
if (platform.isRunning()) {
platform.pause();
console.log("Paused");
2016-12-16 01:21:51 +00:00
}
setDebugButtonState("pause", "stopped");
2016-12-16 01:21:51 +00:00
}
2017-11-16 17:08:06 +00:00
function pause() {
2016-12-16 01:21:51 +00:00
clearBreakpoint();
2017-11-16 17:08:06 +00:00
_pause();
userPaused = true;
2017-11-16 17:08:06 +00:00
}
function _resume() {
if (current_output == null) {
alert("Can't resume emulation until ROM is successfully built.");
return;
}
if (!platform.isRunning()) {
2016-12-31 16:05:22 +00:00
platform.resume();
console.log("Resumed");
2016-12-16 01:21:51 +00:00
}
setDebugButtonState("go", "active");
2017-11-16 17:08:06 +00:00
}
function resume() {
clearBreakpoint();
2018-06-24 17:39:08 +00:00
if (! platform.isRunning() ) {
2018-08-02 17:08:37 +00:00
projectWindows.refresh(false);
2018-06-24 17:39:08 +00:00
}
2017-11-16 17:08:06 +00:00
_resume();
userPaused = false;
2016-12-16 01:21:51 +00:00
}
function togglePause() {
if (platform.isRunning())
pause();
else
resume();
}
2016-12-16 01:21:51 +00:00
function singleStep() {
setupBreakpoint("step");
2016-12-31 16:05:22 +00:00
platform.step();
2016-12-16 01:21:51 +00:00
}
2017-11-24 19:14:22 +00:00
function singleFrameStep() {
setupBreakpoint("tovsync");
2017-11-24 19:14:22 +00:00
platform.runToVsync();
}
2018-07-08 14:07:19 +00:00
function getEditorPC() : number {
var wnd = projectWindows.getActive();
return wnd && wnd.getCursorPC && wnd.getCursorPC();
2017-02-05 04:19:54 +00:00
}
function runToCursor() {
setupBreakpoint("toline");
var pc = getEditorPC();
if (pc >= 0) {
2017-02-05 04:19:54 +00:00
console.log("Run to", pc.toString(16));
if (platform.runToPC) {
platform.runToPC(pc);
} else {
platform.runEval(function(c) {
return c.PC == pc;
});
}
2016-12-16 01:21:51 +00:00
}
}
function runUntilReturn() {
setupBreakpoint("stepout");
platform.runUntilReturn();
}
function runStepBackwards() {
setupBreakpoint("stepback");
platform.stepBack();
}
2016-12-16 01:21:51 +00:00
function clearBreakpoint() {
2017-01-06 14:49:07 +00:00
lastDebugState = null;
2017-11-11 19:45:32 +00:00
if (platform.clearDebug) platform.clearDebug();
2018-09-15 19:27:12 +00:00
setupDebugCallback(); // in case of BRK/trap
2018-07-29 20:26:05 +00:00
showDebugInfo();
2016-12-16 01:21:51 +00:00
}
function resetAndDebug() {
2018-08-23 22:52:56 +00:00
_disableRecording();
if (platform.setupDebug && platform.readAddress) { // TODO??
2017-11-11 19:45:32 +00:00
clearBreakpoint();
2017-11-16 17:08:06 +00:00
_resume();
2017-11-11 19:45:32 +00:00
platform.reset();
setupBreakpoint("reset");
2018-06-22 06:24:52 +00:00
if (platform.runEval)
platform.runEval(function(c) { return true; }); // break immediately
else
; // TODO???
2017-11-11 19:45:32 +00:00
} else {
platform.reset();
}
2017-01-03 01:42:15 +00:00
}
2017-01-06 16:57:28 +00:00
var lastBreakExpr = "c.PC == 0x6000";
function _breakExpression() {
var exprs = window.prompt("Enter break expression", lastBreakExpr);
if (exprs) {
var fn = new Function('c', 'return (' + exprs + ');');
setupBreakpoint();
platform.runEval(fn);
lastBreakExpr = exprs;
}
}
2018-07-08 14:07:19 +00:00
function getSymbolAtAddress(a : number) {
2018-09-17 20:09:09 +00:00
var addr2symbol = platform.debugSymbols && platform.debugSymbols.addr2symbol;
if (addr2symbol) {
if (addr2symbol[a]) return addr2symbol[a];
var i=0;
while (--a >= 0) {
i++;
if (addr2symbol[a]) return addr2symbol[a] + '+' + i;
}
}
return '';
}
2017-04-19 01:18:53 +00:00
function updateDebugWindows() {
if (platform.isRunning()) {
projectWindows.tick();
2017-04-19 01:18:53 +00:00
}
setTimeout(updateDebugWindows, 200);
}
2017-05-20 19:13:23 +00:00
function _recordVideo() {
loadScript("gif.js/dist/gif.js", () => {
var canvas = $("#emulator").find("canvas")[0] as HTMLElement;
2017-05-20 19:13:23 +00:00
if (!canvas) {
alert("Could not find canvas element to record video!");
return;
}
var rotate = 0;
if (canvas.style && canvas.style.transform) {
if (canvas.style.transform.indexOf("rotate(-90deg)") >= 0)
rotate = -1;
else if (canvas.style.transform.indexOf("rotate(90deg)") >= 0)
rotate = 1;
}
// TODO: recording indicator?
var gif = new GIF({
workerScript: 'gif.js/dist/gif.worker.js',
workers: 4,
quality: 10,
rotate: rotate
});
2017-05-20 19:13:23 +00:00
var img = $('#videoPreviewImage');
//img.attr('src', 'https://articulate-heroes.s3.amazonaws.com/uploads/rte/kgrtehja_DancingBannana.gif');
gif.on('finished', function(blob) {
img.attr('src', URL.createObjectURL(blob));
$("#pleaseWaitModal").modal('hide');
2017-11-16 17:08:06 +00:00
_resume();
2017-05-20 19:13:23 +00:00
$("#videoPreviewModal").modal('show');
});
var intervalMsec = 17;
var maxFrames = 420;
2017-05-20 19:13:23 +00:00
var nframes = 0;
console.log("Recording video", canvas);
var f = function() {
if (nframes++ > maxFrames) {
console.log("Rendering video");
$("#pleaseWaitModal").modal('show');
2017-11-16 17:08:06 +00:00
_pause();
2017-05-20 19:13:23 +00:00
gif.render();
} else {
gif.addFrame(canvas, {delay: intervalMsec, copy: true});
2017-05-20 19:13:23 +00:00
setTimeout(f, intervalMsec);
}
};
f();
});
2017-05-20 19:13:23 +00:00
}
2018-07-08 14:07:19 +00:00
function setFrameRateUI(fps:number) {
2018-02-26 23:18:23 +00:00
platform.setFrameRate(fps);
if (fps > 0.01)
$("#fps_label").text(fps.toFixed(2));
else
$("#fps_label").text("1/"+Math.round(1/fps));
}
function _slowerFrameRate() {
var fps = platform.getFrameRate();
fps = fps/2;
if (fps > 0.00001) setFrameRateUI(fps);
}
function _fasterFrameRate() {
var fps = platform.getFrameRate();
fps = Math.min(60, fps*2);
setFrameRateUI(fps);
}
function _slowestFrameRate() {
setFrameRateUI(60/65536);
}
function _fastestFrameRate() {
_resume();
setFrameRateUI(60);
}
2018-06-30 02:06:14 +00:00
function _openBitmapEditor() {
2018-07-04 15:36:32 +00:00
var wnd = projectWindows.getActive();
if (wnd && wnd.openBitmapEditorAtCursor)
2018-07-04 15:36:32 +00:00
wnd.openBitmapEditorAtCursor();
}
function traceTiming() {
2018-08-02 17:08:37 +00:00
projectWindows.refresh(false);
2018-07-04 15:36:32 +00:00
var wnd = projectWindows.getActive();
if (wnd.getSourceFile && wnd.setTimingResult) { // is editor active?
var analyzer = platform.newCodeAnalyzer();
analyzer.showLoopTimingForPC(0);
wnd.setTimingResult(analyzer);
2018-07-04 15:36:32 +00:00
}
2018-06-30 02:06:14 +00:00
}
var recorderActive = false;
function _disableRecording() {
if (recorderActive) {
platform.setRecorder(null);
$("#dbg_record").removeClass("btn_recording");
$("#replaydiv").hide();
recorderActive = false;
}
}
2018-08-23 22:52:56 +00:00
function _resetRecording() {
if (recorderActive) {
stateRecorder.reset();
}
}
function _enableRecording() {
stateRecorder.reset();
platform.setRecorder(stateRecorder);
$("#dbg_record").addClass("btn_recording");
$("#replaydiv").show();
recorderActive = true;
}
function _toggleRecording() {
if (recorderActive) {
_disableRecording();
} else {
_enableRecording();
}
}
function _lookupHelp() {
// TODO
window.open("help/bataribasic/manual.html", "_help");
}
2016-12-16 01:21:51 +00:00
function setupDebugControls(){
2016-12-16 01:21:51 +00:00
$("#dbg_reset").click(resetAndDebug);
$("#dbg_pause").click(pause);
$("#dbg_go").click(resume);
Mousetrap.bindGlobal('ctrl+alt+p', pause);
Mousetrap.bindGlobal('ctrl+alt+r', resume);
Mousetrap.bindGlobal('ctrl+alt+.', resetAndDebug);
2017-11-24 19:14:22 +00:00
if (platform.step) {
2017-11-11 19:45:32 +00:00
$("#dbg_step").click(singleStep).show();
Mousetrap.bindGlobal('ctrl+alt+s', singleStep);
} else
2017-11-11 19:45:32 +00:00
$("#dbg_step").hide();
if (platform.runToVsync) {
2017-11-24 19:14:22 +00:00
$("#dbg_tovsync").click(singleFrameStep).show();
Mousetrap.bindGlobal('ctrl+alt+v', singleFrameStep);
} else
2017-11-24 19:14:22 +00:00
$("#dbg_tovsync").hide();
2018-11-18 17:30:41 +00:00
if ((platform.runEval || platform.runToPC) && !platform_id.startsWith('verilog')) {
2017-11-24 19:14:22 +00:00
$("#dbg_toline").click(runToCursor).show();
Mousetrap.bindGlobal('ctrl+alt+l', runToCursor);
} else
2017-11-11 19:45:32 +00:00
$("#dbg_toline").hide();
if (platform.runUntilReturn) {
2017-11-24 19:14:22 +00:00
$("#dbg_stepout").click(runUntilReturn).show();
Mousetrap.bindGlobal('ctrl+alt+o', runUntilReturn);
} else
2017-11-11 19:45:32 +00:00
$("#dbg_stepout").hide();
if (platform.stepBack) {
2017-11-24 19:14:22 +00:00
$("#dbg_stepback").click(runStepBackwards).show();
Mousetrap.bindGlobal('ctrl+alt+b', runStepBackwards);
} else
2017-11-11 19:45:32 +00:00
$("#dbg_stepback").hide();
2017-11-24 19:14:22 +00:00
if (platform.newCodeAnalyzer) {
$("#dbg_timing").click(traceTiming).show();
}
2017-01-06 14:49:07 +00:00
$("#disassembly").hide();
2018-06-30 02:06:14 +00:00
$("#dbg_bitmap").click(_openBitmapEditor);
2016-12-30 23:51:15 +00:00
$(".dropdown-menu").collapse({toggle: false});
$("#item_new_file").click(_createNewFile);
2018-06-26 23:57:03 +00:00
$("#item_upload_file").click(_uploadNewFile);
$("#item_share_file").click(_shareEmbedLink);
2018-08-21 14:16:47 +00:00
$("#item_reset_file").click(_revertFile);
2017-11-24 19:14:22 +00:00
if (platform.runEval)
$("#item_debug_expr").click(_breakExpression).show();
else
$("#item_debug_expr").hide();
2017-02-02 19:11:52 +00:00
$("#item_download_rom").click(_downloadROMImage);
$("#item_download_file").click(_downloadSourceFile);
2018-08-25 18:29:51 +00:00
$("#item_download_zip").click(_downloadProjectZipFile);
$("#item_download_allzip").click(_downloadAllFilesZipFile);
2017-05-20 19:13:23 +00:00
$("#item_record_video").click(_recordVideo);
2018-09-25 23:46:24 +00:00
if (platform_id == 'apple2')
$("#item_export_cassette").click(_downloadCassetteFile);
else
$("#item_export_cassette").hide();
2018-02-26 23:18:23 +00:00
if (platform.setFrameRate && platform.getFrameRate) {
$("#dbg_slower").click(_slowerFrameRate);
$("#dbg_faster").click(_fasterFrameRate);
$("#dbg_slowest").click(_slowestFrameRate);
$("#dbg_fastest").click(_fastestFrameRate);
2018-02-26 23:18:23 +00:00
}
if (platform.getToolForFilename(main_file_id) == 'bataribasic') {
$("#dbg_help").show().click(_lookupHelp);
}
2017-04-19 01:18:53 +00:00
updateDebugWindows();
// setup replay slider
if (platform.setRecorder && platform.advance) {
setupReplaySlider();
}
}
function setupReplaySlider() {
var replayslider = $("#replayslider");
var replayframeno = $("#replay_frame");
var updateFrameNo = (n) => {
replayframeno.text(n+"");
};
var sliderChanged = (e) => {
_pause();
var frame = (<any>e.target).value;
if (stateRecorder.loadFrame(frame)) {
updateFrameNo(frame);
}
};
var setFrameTo = (frame:number) => {
_pause();
if (stateRecorder.loadFrame(frame)) {
replayslider.val(frame);
updateFrameNo(frame);
console.log('seek to frame',frame);
}
};
stateRecorder.callbackStateChanged = () => {
replayslider.attr('min', 1);
replayslider.attr('max', stateRecorder.numFrames());
2018-08-23 22:52:56 +00:00
replayslider.val(stateRecorder.currentFrame());
updateFrameNo(stateRecorder.currentFrame());
};
replayslider.on('input', sliderChanged);
replayslider.on('change', sliderChanged);
$("#replay_min").click(() => { setFrameTo(1) });
$("#replay_max").click(() => { setFrameTo(stateRecorder.numFrames()); });
$("#replay_back").click(() => { setFrameTo(parseInt(replayslider.val()) - 1); });
$("#replay_fwd").click(() => { setFrameTo(parseInt(replayslider.val()) + 1); });
$("#replay_bar").show();
$("#dbg_record").click(_toggleRecording).show();
2016-12-16 01:21:51 +00:00
}
2016-12-18 20:59:31 +00:00
function showWelcomeMessage() {
2017-05-01 11:37:48 +00:00
if (!localStorage.getItem("8bitworkshop.hello")) {
2016-12-30 23:51:15 +00:00
// Instance the tour
2017-04-20 00:55:13 +00:00
var is_vcs = platform_id == 'vcs';
var steps = [
2016-12-30 23:51:15 +00:00
{
element: "#workspace",
2016-12-30 23:51:15 +00:00
title: "Welcome to 8bitworkshop!",
2017-04-20 00:55:13 +00:00
content: is_vcs ? "Type your 6502 assembly code into the editor, and it'll be assembled in real-time. All changes are saved to browser local storage."
: "Type your source code into the editor, and it'll be compiled in real-time. All changes are saved to browser local storage."
2016-12-30 23:51:15 +00:00
},
{
element: "#emulator",
placement: 'left',
2017-04-20 00:55:13 +00:00
title: "Emulator",
content: "This is an emulator for the \"" + platform_id + "\" platform. We'll load your compiled code into the emulator whenever you make changes."
2016-12-30 23:51:15 +00:00
},
{
element: "#preset_select",
title: "File Selector",
content: "Pick a code example from the book, or access your own files and files shared by others."
},
{
element: "#debug_bar",
placement: 'bottom',
title: "Debug Tools",
2017-04-20 00:55:13 +00:00
content: "Use these buttons to set breakpoints, single step through code, pause/resume, and use debugging tools."
2016-12-30 23:51:15 +00:00
},
{
element: "#dropdownMenuButton",
title: "Main Menu",
2018-10-03 18:17:02 +00:00
content: "Click the menu to download your code, switch between platforms, create new files, or share your work with others."
}];
if (!is_vcs) {
steps.push({
element: "#windowMenuButton",
title: "Window List",
content: "Switch between editor windows, assembly listings, and other tools like disassembler and memory dump."
});
}
2018-07-07 14:55:27 +00:00
steps.push({
element: "#booksMenuButton",
placement: 'left',
title: "Bookstore",
content: "Get some books that explain how to program all of this stuff!"
});
var tour = new Tour({
autoscroll:false,
//storage:false,
steps:steps
});
2016-12-30 23:51:15 +00:00
tour.init();
setTimeout(function() { tour.start(); }, 2000);
2016-12-18 20:59:31 +00:00
}
}
2016-12-16 01:21:51 +00:00
///////////////////////////////////////////////////
2018-07-08 03:10:51 +00:00
var qs = (function (a : string[]) {
if (!a || a.length == 0)
2016-12-16 01:21:51 +00:00
return {};
var b = {};
for (var i = 0; i < a.length; ++i) {
var p = a[i].split('=', 2);
if (p.length == 1)
b[p[0]] = "";
else
b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
}
return b;
})(window.location.search.substr(1).split('&'));
// catch errors
function installErrorHandler() {
if (typeof window.onerror == "object") {
window.onerror = function (msgevent, url, line, col, error) {
console.log(msgevent, url, line, col);
console.log(error);
2018-07-06 06:42:01 +00:00
ga('send', 'exception', {
'exDescription': msgevent + " " + url + " " + " " + line + ":" + col + ", " + error,
'exFatal': true
});
alert(msgevent+"");
_pause();
};
}
}
function uninstallErrorHandler() {
window.onerror = null;
}
function gotoNewLocation() {
uninstallErrorHandler();
2018-07-08 03:10:51 +00:00
window.location.href = "?" + $.param(qs);
}
function replaceURLState() {
if (platform_id) qs['platform'] = platform_id;
history.replaceState({}, "", "?" + $.param(qs));
}
2017-04-26 19:44:55 +00:00
function showBookLink() {
if (platform_id == 'vcs')
$("#booklink_vcs").show();
else if (platform_id == 'mw8080bw' || platform_id == 'vicdual' || platform_id == 'galaxian-scramble' || platform_id == 'vector-z80color' || platform_id == 'williams-z80')
2017-04-26 19:44:55 +00:00
$("#booklink_arcade").show();
}
function addPageFocusHandlers() {
2017-05-02 13:09:53 +00:00
var hidden = false;
document.addEventListener("visibilitychange", function() {
2017-05-02 13:09:53 +00:00
if (document.visibilityState == 'hidden' && platform.isRunning()) {
2017-11-16 17:08:06 +00:00
_pause();
2017-05-02 13:09:53 +00:00
hidden = true;
} else if (document.visibilityState == 'visible' && hidden) {
2017-11-16 17:08:06 +00:00
_resume();
2017-05-02 13:09:53 +00:00
hidden = false;
}
});
$(window).on("focus", function() {
if (hidden) {
2017-11-16 17:08:06 +00:00
_resume();
2017-05-02 13:09:53 +00:00
hidden = false;
}
});
$(window).on("blur", function() {
if (platform.isRunning()) {
2017-11-16 17:08:06 +00:00
_pause();
2017-05-02 13:09:53 +00:00
hidden = true;
}
});
}
2017-01-14 16:14:25 +00:00
function startPlatform() {
2017-01-29 21:06:05 +00:00
if (!PLATFORMS[platform_id]) throw Error("Invalid platform '" + platform_id + "'.");
2017-01-14 16:14:25 +00:00
platform = new PLATFORMS[platform_id]($("#emulator")[0]);
stateRecorder = new StateRecorderImpl(platform);
2017-01-14 16:14:25 +00:00
PRESETS = platform.getPresets();
if (!qs['file']) {
// try to load last file (redirect)
2017-01-14 16:14:25 +00:00
var lastid = localStorage.getItem("__lastid_"+platform_id) || localStorage.getItem("__lastid");
localStorage.removeItem("__lastid");
qs['file'] = lastid || PRESETS[0].id;
replaceURLState();
}
// start platform and load file
platform.start();
initProject();
loadProject(qs['file']);
setupDebugControls();
updateSelector();
showBookLink();
addPageFocusHandlers();
return true;
2017-01-14 16:14:25 +00:00
}
2018-07-08 14:07:19 +00:00
function loadSharedFile(sharekey : string) {
loadScript("octokat.js/dist/octokat.js", () => {
var github = new exports['Octokat']();
2017-01-25 17:30:05 +00:00
var gist = github.gists(sharekey);
gist.fetch().done(function(val) {
var filename;
for (filename in val.files) { break; }
var newid = 'shared/' + filename;
var json = JSON.parse(val.description.slice(val.description.indexOf(' ')+1));
console.log("Fetched " + newid, json);
platform_id = json['platform'];
store = createNewPersistentStore(platform_id, () => {
// runs after migration, if it happens
current_project.updateFile(newid, val.files[filename].content);
reloadPresetNamed(newid);
delete qs['sharekey'];
gotoNewLocation();
});
2017-01-25 17:30:05 +00:00
}).fail(function(err) {
alert("Error loading share file: " + err.message);
});
});
2017-01-25 17:30:05 +00:00
}
2018-08-23 12:49:14 +00:00
function loadScript(scriptfn, onload) {
var script = document.createElement('script');
script.onload = onload;
script.src = scriptfn;
document.getElementsByTagName('head')[0].appendChild(script);
}
2017-01-03 01:42:15 +00:00
// start
export function startUI(loadplatform : boolean) {
installErrorHandler();
2017-01-25 17:30:05 +00:00
// add default platform?
platform_id = qs['platform'] || localStorage.getItem("__lastplatform");
if (!platform_id) {
platform_id = qs['platform'] = "vcs";
}
$("#item_platform_"+platform_id).addClass("dropdown-item-checked");
2017-01-14 16:14:25 +00:00
// parse query string
// is this a share URL?
if (qs['sharekey']) {
2017-01-25 17:30:05 +00:00
loadSharedFile(qs['sharekey']);
} else {
store = createNewPersistentStore(platform_id, null);
2017-01-14 16:14:25 +00:00
// reset file?
if (qs['file'] && qs['reset']) {
store.removeItem(qs['fileview'] || qs['file']);
2017-01-14 16:14:25 +00:00
qs['reset'] = '';
gotoNewLocation();
2017-01-14 16:14:25 +00:00
} else {
// load and start platform object
if (loadplatform) {
var scriptfn = 'gen/platform/' + platform_id.split(/[.-]/)[0] + '.js';
2018-08-23 12:49:14 +00:00
loadScript(scriptfn, () => {
2017-01-14 16:14:25 +00:00
console.log("loaded platform", platform_id);
startPlatform();
showWelcomeMessage();
2018-08-23 12:49:14 +00:00
});
} else {
2017-01-14 16:14:25 +00:00
startPlatform();
showWelcomeMessage();
}
2017-01-14 16:14:25 +00:00
}
2016-12-16 01:21:51 +00:00
}
}