diff --git a/commandLine/src/defines.h b/commandLine/src/defines.h index 3265db2e..208f0c25 100644 --- a/commandLine/src/defines.h +++ b/commandLine/src/defines.h @@ -1,5 +1,5 @@ #define OFPROJECTGENERATOR_MAJOR_VERSION "0" -#define OFPROJECTGENERATOR_MINOR_VERSION "114" +#define OFPROJECTGENERATOR_MINOR_VERSION "115" #define OFPROJECTGENERATOR_PATCH_VERSION "0" #define PG_VERSION (OFPROJECTGENERATOR_MAJOR_VERSION "." OFPROJECTGENERATOR_MINOR_VERSION "." OFPROJECTGENERATOR_PATCH_VERSION) diff --git a/commandLine/src/projects/baseProject.cpp b/commandLine/src/projects/baseProject.cpp index 8d97c6ba..14452258 100644 --- a/commandLine/src/projects/baseProject.cpp +++ b/commandLine/src/projects/baseProject.cpp @@ -404,16 +404,18 @@ bool baseProject::create(const fs::path & _path, string templateName){ } bool baseProject::save(){ - ofLog(OF_LOG_NOTICE) << "saving addons.make"; - - std::ofstream addonsMake(projectDir / "addons.make"); - for (auto & a : addons) { - addonsMake << a.addonMakeName << std::endl; -// if (a.isLocalAddon) { -// addonsMake << fs::path(a.addonPath).generic_string() << std::endl; -// } else { -// addonsMake << a.name << std::endl; -// } + // no addons and no addons.make yet: don't create an empty one (#297). + // an existing one is still rewritten, so removing every addon clears it + if (addons.size() || missingAddons.size() || fs::exists(projectDir / "addons.make")) { + ofLog(OF_LOG_NOTICE) << "saving addons.make"; + std::ofstream addonsMake(projectDir / "addons.make"); + for (auto & a : addons) { + addonsMake << a.addonMakeName << std::endl; + } + // not found, kept so the project still says it needs them (#258) + for (auto & m : missingAddons) { + addonsMake << m << std::endl; + } } //save out params which the PG knows about to config.make @@ -585,7 +587,11 @@ void baseProject::addAddon(const std::string & _addonName){ if(addon.load(_addonName, projectDir, target)){ addonsCache[target][addonName] = addon; }else{ - ofLogVerbose("baseProject::addAddon") << "Ignoring addon that doesn't seem to exist: " << _addonName; + // keep it in addons.make so the project still says it needs it (#258) + ofLogError("baseProject::addAddon") << "addon not found: " << ofTrim(ofSplitString(_addonName, "#")[0]) << " - kept in addons.make, install it or fix the name"; + if (std::find(missingAddons.begin(), missingAddons.end(), _addonName) == missingAddons.end()) { + missingAddons.emplace_back(_addonName); + } return; //if addon does not exist, stop early } } @@ -646,6 +652,15 @@ void baseProject::copyAddonData(ofAddon& addon){ if(fs::exists(from)){ fs::path to { dest / d }; + // bin/data (and any folders in ADDON_DATA) may not exist yet (#619) + fs::path toDir { fs::is_directory(from) ? to : to.parent_path() }; + if (!fs::exists(toDir)) { + try { + fs::create_directories(toDir); + } catch (const std::exception& e) { + ofLogWarning() << "Can not create addon data folder: " << toDir.string() << " :: " << e.what(); + } + } if (fs::is_regular_file(from)){ try { fs::copy_file(from, to, fs::copy_options::overwrite_existing); // this is addon files @@ -655,14 +670,6 @@ void baseProject::copyAddonData(ofAddon& addon){ } } else if (fs::is_directory(from)) { - if (!fs::exists(to)) { - try { - fs::create_directory(to); - } catch (const std::exception& e) { - std::cerr << "Error creating directories: " << e.what() << std::endl; - } - } - try { fs::copy(from, to, fs::copy_options::overwrite_existing | fs::copy_options::recursive); ofLogVerbose() << "adding addon data file: " << d << endl; diff --git a/commandLine/src/projects/baseProject.h b/commandLine/src/projects/baseProject.h index baa4c2df..2df1c72f 100644 --- a/commandLine/src/projects/baseProject.h +++ b/commandLine/src/projects/baseProject.h @@ -184,6 +184,7 @@ class baseProject { std::vector addons; std::vector extSrcPaths; std::vector projectDefines; // also written to config.make PROJECT_DEFINES for make builds + std::vector missingAddons; // not found, still written to addons.make (#258) std::string emscriptenWasmOption; // from the CLI, empty = keep std::string emscriptenWasm; // value for this project: "", "32" or "64" void saveCMakeLists(); diff --git a/commandLine/src/projects/xcodeProject.cpp b/commandLine/src/projects/xcodeProject.cpp index 1cc912fa..cc610b54 100644 --- a/commandLine/src/projects/xcodeProject.cpp +++ b/commandLine/src/projects/xcodeProject.cpp @@ -1100,7 +1100,12 @@ bool xcodeProject::saveProjectFile() { // alert ("emplace back " + cols[3] , 32); // the same define / -D -U -I flag from an addon parsed twice is added once // (not other flags: "-framework A -framework B" are separate values) - string value { cols[3] }; + // the whole value, not just its first word: "src/sub folder" (#303) + string value { c.substr(cols[0].size() + cols[1].size() + cols[2].size() + 3) }; + // Xcode splits build settings on spaces, quote like Xcode does for paths with spaces + if (value.find(' ') != string::npos && value.front() != '"') { + value = "\"" + value + "\""; + } bool bUnique { key == "GCC_PREPROCESSOR_DEFINITIONS" || (bFlags && key != "OTHER_LDFLAGS" && value.size() > 2 && value[0] == '-' && (value[1] == 'D' || value[1] == 'U' || value[1] == 'I')) }; if (!bUnique || std::find(j[p].begin(), j[p].end(), value) == j[p].end()) { j[p].emplace_back(value); diff --git a/frontend/app.js b/frontend/app.js index 8931b8c2..b0f821ab 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -25,6 +25,9 @@ let isOfPathGood = false; let isFirstTimeSierra = false; let bVerbose = false; let localAddons = []; +// addons.make lines whose addon isn't installed: kept on generate / update, shown under the addons (#258) +let missingAddons = []; +let missingAddonCloning = false; let numAddedSrcPaths = 1; @@ -98,6 +101,8 @@ ipcRenderer.on('setTranslations', (event, data) => { //------------------------------------------- ipcRenderer.on('setOfPath', (event, arg) => { + // picked with the folder dialog: keep it over the detected path from now on (#602) + defaultSettings.ofPathManual = true; setOFPath(arg); }); @@ -414,11 +419,14 @@ ipcRenderer.on('selectAddons', (event, arg) => { const neededAddons = []; localAddons = []; + missingAddons = []; //haystack.indexOf(needle) >= 0 for (let i = 0; i < arg.length; i++) { - arg[i] = arg[i].trim(); + // addons.make line, optionally "name #url" + const line = arg[i].trim(); + arg[i] = line.split('#')[0].trim(); // first, check if it's already picked, then do nothing if (addonsAlreadyPicked.indexOf(arg[i]) >= 0){ console.log("already picked"); // alread picked @@ -435,6 +443,8 @@ ipcRenderer.on('selectAddons', (event, arg) => { localAddons.push(arg[i]); } else { neededAddons.push(arg[i]); + const url = ((line.split('#')[1] || '').match(/(https?:\/\/|git@)\S+/) || [''])[0]; + missingAddons.push({ name: arg[i], line, url }); } } } @@ -453,6 +463,7 @@ ipcRenderer.on('selectAddons', (event, arg) => { // $("#generate-mode-section").removeClass("has-missing-addons"); } + renderMissingAddons(); if (localAddons.length > 0){ // $("#generate-mode-section").addClass("has-missing-addons"); @@ -658,7 +669,7 @@ function setup() { $('#ofPathButton').click(() => { disableButtonTemporarily($("#ofPathButton")); - getOFPath(); + getOFPath(true); }); console.log("App is translocated: " + isFirstTimeSierra); @@ -847,6 +858,11 @@ function setup() { } }); + // typed by the user (setOFPath only triggers change) + $("#ofPath").on("input", () => { + defaultSettings.ofPathManual = true; + }); + $("#ofPath").on("change", () => { const ofpath = $("#ofPath").val(); defaultSettings.defaultOfPath = ofpath; @@ -1202,6 +1218,10 @@ function generate() { for(let i = 0; i < localAddons.length; i++) { addonValueArray.push(localAddons[i]); } + // and the missing ones, so they stay in addons.make (with their #url comment) + for (const m of missingAddons) { + addonValueArray.push(m.line.includes(',') ? m.name : m.line); + } // extra source locations const srcExtraArr = []; @@ -1342,6 +1362,8 @@ function switchGenerateMode(mode) { $("#folderButton").show(); $("#missingAddonMessage").hide(); $("#localAddonMessage").hide(); + missingAddons = []; + renderMissingAddons(); $("#nameRandomiser").hide(); $("#revealProjectFiles").show(); $("#extraContainer").hide(); @@ -1366,6 +1388,8 @@ function switchGenerateMode(mode) { $("#folderButton").show(); $("#missingAddonMessage").hide(); $("#localAddonMessage").hide(); + missingAddons = []; + renderMissingAddons(); $("#nameRandomiser").show(); $("#revealProjectFiles").hide(); $("#extraContainer").hide(); @@ -1646,6 +1670,34 @@ function rescanAddons() { ipcRenderer.send('isOFProjectFolder', projectInfo); // <- this forces addon reload } +// one line per missing addon under the addons dropdown: clone it from its addons.make #url, or search for it +function renderMissingAddons() { + const $list = $('#missingAddonsField').empty(); + for (const m of missingAddons) { + const $row = $('
').append( + $(''), + $('').text(m.name), + ' ', $('').text(t('addon.missing')), ' '); + if (m.url) { + $row.append($('').text(t('addon.missingDownload')).attr('title', m.url).on('click', (e) => { + e.preventDefault(); + missingAddonCloning = true; + openConsoleForOperation(); + // package manager style url@commit (an @ before the first / is git@host) + const at = m.url.lastIndexOf('@'); + const [url, ref] = at > m.url.indexOf('/') && m.url.includes('/') ? [m.url.slice(0, at), m.url.slice(at + 1)] : [m.url, '']; + ipcRenderer.send('cloneAddon', { ofPath: $("#ofPath").val(), url, ref }); + }), ' · '); + } + $row.append($('').text(t('addon.missingFind')).on('click', (e) => { + e.preventDefault(); + ipcRenderer.send('openExternal', 'https://github.com/search?type=repositories&q=' + encodeURIComponent(m.name)); + })); + $list.append($row); + } + $list.toggle(missingAddons.length > 0); +} + function cloneAddonFromGit(){ const raw = $("#addonGitUrl").val().trim(); if (raw === '') return; @@ -1658,6 +1710,11 @@ function cloneAddonFromGit(){ ipcRenderer.on('cloneAddonDone', () => { $("#addonGitUrl").prop('disabled', false).attr('placeholder', 'git URL, optionally url#branch-or-commit...'); + // re-import the project so a downloaded missing addon gets picked + if (missingAddonCloning) { + missingAddonCloning = false; + rescanAddons(); + } }); function getRandomSketchName(){ @@ -1985,14 +2042,35 @@ function getOFPlatform() { ipcRenderer.send('getHostType'); } -function getOFPath() { +// the ofPath button re-detects even over a manually set path +let ofPathDetectForced = false; + +function getOFPath(force = false) { console.log('getOFPath:sending'); + ofPathDetectForced = force; ipcRenderer.send('getOFPath'); } +// absolute folder with libs/openFrameworks and addons ("../../.." = the cmdline didn't find one) +function isOFPath(p) { + return !!p && path.isAbsolute(p) + && fs.existsSync(path.join(p, 'libs', 'openFrameworks')) && fs.existsSync(path.join(p, 'addons')); +} ipcRenderer.on('ofPathResult', (event, result) => { + const forced = ofPathDetectForced; + ofPathDetectForced = false; + // a path the user set (dialog / typed) wins over detection on launch and the settings tab (#602) + if (!forced && defaultSettings.ofPathManual && isOFPath($('#ofPath').val())) { + console.log('ofPath: keeping manually set path', $('#ofPath').val()); + return; + } + if (result.success && !forced && !isOFPath(result.message) && isOFPath($('#ofPath').val())) { + console.log('ofPath: detected path is not an openFrameworks folder, keeping', $('#ofPath').val()); + return; + } if (result.success) { + if (forced) defaultSettings.ofPathManual = false; console.log('ofPath:', result.message); document.getElementById('ofPath').textContent = result.message; setOFPath(result.message); diff --git a/frontend/index.html b/frontend/index.html index 3a11faf1..e82f7490 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -190,6 +190,7 @@
+