Skip to content

Commit

Permalink
Update 'New Addon' function to work with 4.2 (#166)
Browse files Browse the repository at this point in the history
* Updated auto_load.py to work when used in an extension as well as in a legacy addon

* Added dialog to ask if generating a legacy addon

* Implemented removing bl_info if not supporting legacy addons

* added blender_manifest.toml template

* Added copying manifest file

* documentation

* Added different formatting for manifest id and name

* reorder dialogues

* Fixed bug where auto_load doesn't recognise classes that don't directly inherit from a Blender class, even if they do so indirectly.

* Ensure addon name is consistent between init and manifest file

* Change choice order

* Resolve manifest path issue

* Remove commented out line
  • Loading branch information
strike-digital authored Jul 16, 2024
1 parent 9ac8e58 commit ac3b1ca
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 22 deletions.
6 changes: 3 additions & 3 deletions pythonFiles/templates/addons/with_auto_load/auto_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def unregister():
#################################################

def get_all_submodules(directory):
return list(iter_submodules(directory, directory.name))
return list(iter_submodules(directory, __package__))

def iter_submodules(path, package_name):
for name in sorted(iter_submodule_names(path)):
Expand Down Expand Up @@ -103,7 +103,7 @@ def get_dependency_from_annotation(value):
return None

def iter_my_deps_from_parent_id(cls, my_classes_by_idname):
if bpy.types.Panel in cls.__bases__:
if issubclass(cls, bpy.types.Panel):
parent_idname = getattr(cls, "bl_parent_id", None)
if parent_idname is not None:
parent_cls = my_classes_by_idname.get(parent_idname)
Expand All @@ -113,7 +113,7 @@ def iter_my_deps_from_parent_id(cls, my_classes_by_idname):
def iter_my_classes(modules):
base_types = get_register_base_types()
for cls in get_classes_in_modules(modules):
if any(base in base_types for base in cls.__bases__):
if any(issubclass(cls, base) for base in base_types):
if not getattr(cls, "is_registered", False):
yield cls

Expand Down
73 changes: 73 additions & 0 deletions pythonFiles/templates/blender_manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
schema_version = "1.0.0"

# Example of manifest file for a Blender extension
# Change the values according to your extension
id = "ADDON_ID"
version = "1.0.0"
name = "ADDON_NAME"
tagline = "This is another extension"
maintainer = "AUTHOR_NAME"
# Supported types: "add-on", "theme"
type = "add-on"

# Optional link to documentation, support, source files, etc
# website = "https://extensions.blender.org/add-ons/my-example-package/"

# Optional list defined by Blender and server, see:
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
tags = ["Animation", "Sequencer"]

blender_version_min = "4.2.0"
# # Optional: Blender version that the extension does not support, earlier versions are supported.
# # This can be omitted and defined later on the extensions platform if an issue is found.
# blender_version_max = "5.1.0"

# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
license = [
"SPDX:GPL-2.0-or-later",
]
# Optional: required by some licenses.
# copyright = [
# "2002-2024 Developer Name",
# "1998 Company Name",
# ]

# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
# platforms = ["windows-x64", "macos-arm64", "linux-x64"]
# Other supported platforms: "windows-arm64", "macos-x64"

# Optional: bundle 3rd party Python modules.
# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
# wheels = [
# "./wheels/hexdump-3.3-py3-none-any.whl",
# "./wheels/jsmin-3.0.1-py3-none-any.whl",
# ]

# Optional: add-ons can list which resources they will require:
# * files (for access of any filesystem operations)
# * network (for internet access)
# * clipboard (to read and/or write the system clipboard)
# * camera (to capture photos and videos)
# * microphone (to capture audio)
#
# If using network, remember to also check `bpy.app.online_access`
# https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
#
# For each permission it is important to also specify the reason why it is required.
# Keep this a single short sentence without a period (.) at the end.
# For longer explanations use the documentation or detail page.
#
# [permissions]
# network = "Need to sync motion-capture data to server"
# files = "Import/export FBX from/to disk"
# clipboard = "Copy and paste bone transforms"

# Optional: build settings.
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
# [build]
# paths_exclude_pattern = [
# "__pycache__/",
# "/.git/",
# "/*.zip",
# ]
69 changes: 51 additions & 18 deletions src/new_addon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ import { letUserPickItem } from './select_utils';
import {
cancel, readTextFile, writeTextFile, getWorkspaceFolders,
addFolderToWorkspace, multiReplaceText, pathExists,
isValidPythonModuleName, renamePath
isValidPythonModuleName, renamePath, toTitleCase
} from './utils';

type AddonBuilder = (path: string, addonName: string, authorName: string) => Promise<string>;
type AddonBuilder = (path: string, addonName: string, authorName: string, supportLegacy: boolean) => Promise<string>;

const addonTemplateDir = path.join(templateFilesDir, 'addons');
const manifestFile = path.join(templateFilesDir, 'blender_manifest.toml')

export async function COMMAND_newAddon() {
let builder = await getNewAddonGenerator();
let [addonName, authorName] = await askUser_SettingsForNewAddon();
let [addonName, authorName, supportLegacy] = await askUser_SettingsForNewAddon();
let folderPath = await getFolderForNewAddon();
folderPath = await fixAddonFolderName(folderPath);
let mainPath = await builder(folderPath, addonName, authorName);
let mainPath = await builder(folderPath, addonName, authorName, supportLegacy);

await vscode.window.showTextDocument(vscode.Uri.file(mainPath));
await vscode.commands.executeCommand('cursorBottom');
addFolderToWorkspace(folderPath);
}

Expand Down Expand Up @@ -131,35 +131,47 @@ async function askUser_SettingsForNewAddon() {
else if (addonName === "") {
return Promise.reject(new Error('Can\'t create an addon without a name.'));
}

let authorName = await vscode.window.showInputBox({ placeHolder: 'Your Name' });
if (authorName === undefined) {
return Promise.reject(cancel());
}
else if (authorName === "") {
return Promise.reject(new Error('Can\'t create an addon without an author name.'));
}

let items = [];
items.push({ label: "Yes", data: true });
items.push({ label: "No", data: false });
let item = await letUserPickItem(items, "Support legacy Blender versions (<4.2)?");
let supportLegacy = item.data;

return [<string>addonName, <string>authorName];
return [<string>addonName, <string>authorName, supportLegacy];
}

async function generateAddon_Simple(folder: string, addonName: string, authorName: string) {
async function generateAddon_Simple(folder: string, addonName: string, authorName: string, supportLegacy: boolean) {
let srcDir = path.join(addonTemplateDir, 'simple');

let initSourcePath = path.join(srcDir, '__init__.py');
let initTargetPath = path.join(folder, '__init__.py');
await copyModifiedInitFile(initSourcePath, initTargetPath, addonName, authorName);
await copyModifiedInitFile(initSourcePath, initTargetPath, addonName, authorName, supportLegacy);

return initTargetPath;
let manifestTargetPath = path.join(folder, 'blender_manifest.toml');
await copyModifiedManifestFile(manifestFile, manifestTargetPath, addonName, authorName);

return manifestTargetPath;
}

async function generateAddon_WithAutoLoad(folder: string, addonName: string, authorName: string) {
async function generateAddon_WithAutoLoad(folder: string, addonName: string, authorName: string, supportLegacy: boolean) {
let srcDir = path.join(addonTemplateDir, 'with_auto_load');

let initSourcePath = path.join(srcDir, '__init__.py');
let initTargetPath = path.join(folder, '__init__.py');
await copyModifiedInitFile(initSourcePath, initTargetPath, addonName, authorName);

await copyModifiedInitFile(initSourcePath, initTargetPath, addonName, authorName, supportLegacy);

let manifestTargetPath = path.join(folder, 'blender_manifest.toml');
await copyModifiedManifestFile(manifestFile, manifestTargetPath, addonName, authorName);

let autoLoadSourcePath = path.join(srcDir, 'auto_load.py');
let autoLoadTargetPath = path.join(folder, 'auto_load.py');
await copyFileWithReplacedText(autoLoadSourcePath, autoLoadTargetPath, {});
Expand All @@ -172,7 +184,7 @@ async function generateAddon_WithAutoLoad(folder: string, addonName: string, aut
return defaultFilePath;
}
catch {
return initTargetPath;
return manifestTargetPath;
}
}

Expand All @@ -185,11 +197,32 @@ async function getDefaultFileName() {
return item.label;
}

async function copyModifiedInitFile(src: string, dst: string, addonName: string, authorName: string) {
await copyFileWithReplacedText(src, dst, {
ADDON_NAME: addonName,
async function copyModifiedInitFile(src: string, dst: string, addonName: string, authorName: string, supportLegacy: boolean) {
let replacements;

// Remove bl_info if not supporting legacy addon system
if (supportLegacy) {
replacements = {
ADDON_NAME: toTitleCase(addonName),
AUTHOR_NAME: authorName,
}
}
else {
// https://regex101.com/r/RmBWrk/1
replacements = {
'bl_info.+=.+{[\\s\\S]*}\\s*': '',
}
}
await copyFileWithReplacedText(src, dst, replacements);
}

async function copyModifiedManifestFile(src: string, dst: string, addonName: string, authorName: string) {
let replacements = {
ADDON_ID: addonName.toLowerCase().replace(/\s/g, '_'),
ADDON_NAME: toTitleCase(addonName),
AUTHOR_NAME: authorName,
});
};
await copyFileWithReplacedText(src, dst, replacements);
}

async function copyFileWithReplacedText(src: string, dst: string, replacements: object) {
Expand Down
10 changes: 9 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ export function startsWithNumber(text: string) {

export function multiReplaceText(text: string, replacements: object) {
for (let old of Object.keys(replacements)) {
text = text.replace(old, <string>(<any>replacements)[old]);
let matcher = RegExp(old, 'g');
text = text.replace(matcher, <string>(<any>replacements)[old]);
}
return text;
}
Expand All @@ -220,3 +221,10 @@ export function isValidPythonModuleName(text: string): boolean {
let match = text.match(/^[_a-z][_0-9a-z]*$/i);
return match !== null;
}

export function toTitleCase(str: string) {
return str.replace(
/\w\S*/g,
text => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase()
);
}

0 comments on commit ac3b1ca

Please sign in to comment.