How to get the user to define .txt path and name

So, I have this script that works great but it relies on the set path and Part_List.txt name to write the data. I’d like to prompt the user to name the .txt file and then pick the location for saving.

How would I do that?


import bpy
import os

# Get the current user's desktop path
desktop_path = os.path.join(os.path.expanduser("~"), "Desktop")

# Construct the full file path to the .txt file on the desktop
file_path = os.path.join(desktop_path, "Parts_List.txt")

# Open the file for writing
f = open(file_path, "w")

# Create a dictionary to keep track of the object names, object data names, and their counts
obj_data = {}

# Iterate over all visible objects in the scene
for obj in bpy.context.visible_objects:
    # Skip objects that are of type "NoneType"
    if obj is None:
        continue

    # Split the object name and object data name at the decimal point
    obj_name = obj.name.split(".")[0]

    # Skip object data names that are of type "NoneType"
    if obj.data is None:
        continue
    obj_data_name = obj.data.name.split(".")[0]

    # Store the object name, object data name, and count in the dictionary
    if obj_name in obj_data:
        obj_data[obj_name]["count"] += 1
    else:
        obj_data[obj_name] = {"name": obj_data_name, "count": 1}

# Iterate over the dictionary and write the modified object names to the file
for obj_name, data in obj_data.items():
    f.write(f'{data["name"]};{obj_name};{data["count"]}\n')

# Close the file
f.close()

1st I would recommend converting the script to an actual add-on. Any time you are talking about user options I personally feel as though you are suggesting distributing your code. While a script can certainly be functional it can also cause issues with other scripts/add-ons that may use the same variable names or key-mapping etc.

Personally, I like the video series provided by Blender Dr. Stuvel:
Blender Collections | Scripting for Artists [6] - YouTube
and many of the videos provided by Jayanam:
Jayanam - YouTube

In my opinion it is important not only to know how to do something but why certain choices matter.

For example you say you want the user to select a target directory and filename.
Does that mean they make one selection which becomes a default value and is saved as a default value in Blender regardless of opening a new file?
(in which case addon preferences is a good place to store that value)
Or:
Does that mean the user should be able to set a default for a specific blender file and when they open that specific file the file path/name are retained only for that file?
(in which case a simple variable or a preset list can store information easily enough)
Or:
Does that mean every single time the operation is performed the user needs to set a new file location/name?
(in which case the details do not need to be saved at all)

Furthermore when you suggest prompt the user for a target location I would suggest you be more specific (do you mean a pop-up window? would having the file path/name in a panel work? do you just want to force the blender file browser window to be used?)

Please note this is by no means intended to criticize your post but instead to help clarify details so that someone may be able to better assist you in a timely manner.

2 Likes

Thanks for the reply and honesty. I’m a rookie when it comes to writing python code and it’s application in Blender. To this point, it’s been reading and tutorials to put this together.

My wish is for every time the user preforms the operation (presses export), that a popup window appears and they can specify .txt name and location of save.

This is the complete add-on as it stands now. Which sets a predefined .txt name and save location:

bl_info = {
    "name": "Export Parts List",
    "author": "John Doe",
    "version": (1, 0),
    "blender": (2, 80, 0),
    "location": "View3D > N",
    "description": "Exports the VISIBLE objects in the active scene and saves the into a Part_List.txt on your desktop. The .txt file is structured to be imported in the Skyline Estimate Sheet",
    "warning": "",
    "doc_url": "",
    "category": "",
}


import bpy
import os
from bpy.types import (Panel, Operator)

class ButtonOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "partslist.1"
    bl_label = "Simple Object Operator"

    
    def execute(self, context):
        # Get the current user's desktop path
        desktop_path = os.path.join(os.path.expanduser("~"), "Desktop")

        # Construct the full file path to the .txt file on the desktop
        file_path = os.path.join(desktop_path, "Parts_List.txt")

        # Open the file for writing
        f = open(file_path, "w")

        # Create a dictionary to keep track of the object names, object data names, and their counts
        obj_data = {}

        # Iterate over all visible objects in the scene
        for obj in bpy.context.visible_objects:
            # Skip objects that are of type "NoneType"
            if obj is None:
                continue

            # Split the object name and object data name at the decimal point
            obj_name = obj.name.split(".")[0]

            # Skip object data names that are of type "NoneType"
            if obj.data is None:
                continue
            obj_data_name = obj.data.name.split(".")[0]

            # Store the object name, object data name, and count in the dictionary
            if obj_name in obj_data:
                obj_data[obj_name]["count"] += 1
            else:
                obj_data[obj_name] = {"name": obj_data_name, "count": 1}

        # Iterate over the dictionary and write the modified object names to the file
        for obj_name, data in obj_data.items():
            f.write(f'{data["name"]};{obj_name};{data["count"]}\n')

        # Close the file
        f.close()
   
   
        return {'FINISHED'}

class CustomPanel(bpy.types.Panel):
    bl_label = "Parts List"
    bl_idname = "OBJECT_PT_PartList"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Parts List Export"

    def draw(self, context):
        layout = self.layout
        obj = context.object
        row = layout.row()
        row.operator(ButtonOperator.bl_idname, text="Export", icon='IMPORT')




from bpy.utils import register_class, unregister_class

_classes = [    ButtonOperator,    CustomPanel]

def register():
    for cls in _classes:
        register_class(cls)

def unregister():
    for cls in _classes:
        unregister_class(cls)

if __name__ == "__main__":
    register()

Given the updated code you provided I’ll start by going through some of the changes I made and attempt to explain why and provide the full code at the bottom of the post. Please note I am not a programmer just someone that tinkers with Blender.

bl_info:

"category": "File",
"support": "TESTING",

Providing a category allows the end user a quick filter in the addons for finding types of addons. This is the drop-down menu when enabling an add-on. I chose File since this add-on creates a new file based on information but does not actually export models etc.

The use of "support": "TESTING" is a personal preference that I use so that I can quickly find an addon even if the name is incorrect. If you are happy with your work you can at any time comment out or delete the "support": "TESTING" line and the addon will show in the community tab at the top of the add-ons panel.

importing modules:

Generally it is preferred to maintain all your imports at the top of your add-on.

import bpy
import os
from bpy.utils import register_class, unregister_class

Properties:

While you can add individual properties I prefer to use property groups. This helps to prevent even common naming conventions of variables from interfering with eachother.

class PARTSLIST_PG_props(bpy.types.PropertyGroup):
    op_dir: bpy.props.StringProperty(
        name = "op_dir",
        description = "Name of output directory",
        default = os.path.join(os.path.expanduser("~"), "Desktop"),
        subtype = 'DIR_PATH',)
    op_fname: bpy.props.StringProperty(
        name = "op_fname",
        description = "Name of output file",
        default = "Parts_List.txt",
        subtype = 'FILE_NAME',)

By utilizing subtypes of string properties gain additional implementation controls without additional coding.

dir_subtype

Adding poll method:

class PARTSLIST_OT_export(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "partslist.export"
    bl_label = "Parts List Export"


    @classmethod
    def poll(cls, context):
        props = context.window_manager.partslist_pg
        return os.path.isdir(props.op_dir)

The poll method is used to verify conditions are met prior to allowing the operator to run. In this case I verify that the directory exists prior to attempting to create the file. MS Windows does not like when you try to create a file in a folder that does not exist 1st. I believe the pathlib module handles this better but you already had the os module imported and setup. When the poll condition is not met the operator button becoms greyed out and non functional until the poll condition is satisfied. In this case a pre-existing directory.

poll_condition

Naming:

https://wiki.blender.org/wiki/Reference/Release_Notes/2.80/Python_API/Addons

Try to ensure you use names that are both easy enough to identify intended operation and unique enough to prevent collision with other add-ons.

_classes = [
    PARTSLIST_PG_props,
    PARTSLIST_OT_export,
    PARTSLIST_PT_main_panel,]

Code:

bl_info = {
    "name": "Export Parts List",
    "author": "John Doe",
    "version": (1, 0),
    "blender": (2, 80, 0),
    "location": "View3D > N",
    "description": "Exports the VISIBLE objects in the active scene and saves the into a .txt file structured to be imported in the Skyline Estimate Sheet",
    "warning": "",
    "doc_url": "",
    "category": "File",
    "support": "TESTING",
}


import bpy
import os
from bpy.utils import register_class, unregister_class

class PARTSLIST_PG_props(bpy.types.PropertyGroup):
    op_dir: bpy.props.StringProperty(
        name = "op_dir",
        description = "Name of output directory",
        default = os.path.join(os.path.expanduser("~"), "Desktop"),
        subtype = 'DIR_PATH',)
    op_fname: bpy.props.StringProperty(
        name = "op_fname",
        description = "Name of output file",
        default = "Parts_List.txt",
        subtype = 'FILE_NAME',)

class PARTSLIST_OT_export(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "partslist.export"
    bl_label = "Parts List Export"


    @classmethod
    def poll(cls, context):
        props = context.window_manager.partslist_pg
        return os.path.isdir(props.op_dir)


    def execute(self, context):
        props = context.window_manager.partslist_pg
        file_path = os.path.join(props.op_dir, props.op_fname)
        f = open(file_path, "w")
        obj_data = {}
        for obj in context.visible_objects:
            if obj is None:
                continue
            obj_name = obj.name.split(".")[0]
            if obj.data is None:
                continue
            obj_data_name = obj.data.name.split(".")[0]
            if obj_name in obj_data:
                obj_data[obj_name]["count"] += 1
            else:
                obj_data[obj_name] = {"name": obj_data_name, "count": 1}
        for obj_name, data in obj_data.items():
            f.write(f'{data["name"]};{obj_name};{data["count"]}\n')
        f.close()
        return {'FINISHED'}

class PARTSLIST_PT_main_panel(bpy.types.Panel):
    bl_label = "Parts List"
    bl_idname = "partslist.main_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Parts List Export"


    def draw(self, context):
        props = context.window_manager.partslist_pg
        layout = self.layout
        row = layout.row()
        row.prop(props, "op_dir")
        row = layout.row()
        row.prop(props, "op_fname")
        row = layout.row()
        row.operator(
            PARTSLIST_OT_export.bl_idname,
            text = "Export",
            icon = 'IMPORT')


_classes = [
    PARTSLIST_PG_props,
    PARTSLIST_OT_export,
    PARTSLIST_PT_main_panel,]


def register():
    for cls in _classes:
        register_class(cls)
    bpy.types.WindowManager.partslist_pg = bpy.props.PointerProperty(
        type = PARTSLIST_PG_props)


def unregister():
    for cls in _classes:
        unregister_class(cls)
    del bpy.types.WindowManager.partslist_pg

if __name__ == "__main__":
    register()

3 Likes

Thank you!

This is a perfect breakdown and solution. Exactly what I was looking for. Doing my best to learn as I go and this helps in understanding and making this workable. Blender community is the best for learning and support. Appreciate the help!

1 Like

i like this little addon

but i need one where it would select the local folder and let me select some
data file XYZ.IES in local folder or sub folers and use that name to import and read the selected file

this menu would be in a panel class
and i already have some defined operators to use

i have the part for reading the file so not that part

thanks for any feedback
have a nice day

happy bl