From 2c7b386347c4f1e7d9622fcc8b7a2c246f3aebe7 Mon Sep 17 00:00:00 2001 From: Trevor SANDY Date: Sun, 21 May 2023 11:41:26 +0200 Subject: [PATCH 01/14] Blender addon - settings lc_profile --- common/lc_profile.cpp | 6 ++++++ common/lc_profile.h | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/common/lc_profile.cpp b/common/lc_profile.cpp index 0e87d478..66847237 100644 --- a/common/lc_profile.cpp +++ b/common/lc_profile.cpp @@ -143,6 +143,12 @@ static lcProfileEntry gProfileEntries[LC_NUM_PROFILE_KEYS] = lcProfileEntry("POVRay", "Width", 1280), // LC_PROFILE_POVRAY_WIDTH lcProfileEntry("POVRay", "Height", 720), // LC_PROFILE_POVRAY_HEIGHT + lcProfileEntry("Blender", "Path", ""), // LC_PROFILE_BLENDER_PATH + lcProfileEntry("Blender", "LDrawConfigPath", ""), // LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH + lcProfileEntry("Blender", "Version", ""), // LC_PROFILE_BLENDER_VERSION + lcProfileEntry("Blender", "AddonVersion", ""), // LC_PROFILE_BLENDER_ADDON_VERSION + lcProfileEntry("Blender", "ImportModule", ""), // LC_PROFILE_BLENDER_IMPORT_MODULE + lcProfileEntry("Settgins", "PreviewViewSphereEnabled", 0), // LC_PROFILE_PREVIEW_VIEW_SPHERE_ENABLED lcProfileEntry("Settings", "PreviewViewSphereSize", 75), // LC_PROFILE_PREVIEW_VIEW_SPHERE_SIZE lcProfileEntry("Settings", "PreviewViewSphereLocation", (int)lcViewSphereLocation::TopRight), // LC_PROFILE_PREVIEW_VIEW_SPHERE_LOCATION diff --git a/common/lc_profile.h b/common/lc_profile.h index 0e96d2ee..aaadc873 100644 --- a/common/lc_profile.h +++ b/common/lc_profile.h @@ -90,6 +90,11 @@ enum LC_PROFILE_KEY LC_PROFILE_POVRAY_LGEO_PATH, LC_PROFILE_POVRAY_WIDTH, LC_PROFILE_POVRAY_HEIGHT, + LC_PROFILE_BLENDER_PATH, + LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH, + LC_PROFILE_BLENDER_VERSION, + LC_PROFILE_BLENDER_ADDON_VERSION, + LC_PROFILE_BLENDER_IMPORT_MODULE, LC_PROFILE_PREVIEW_VIEW_SPHERE_ENABLED, LC_PROFILE_PREVIEW_VIEW_SPHERE_SIZE, From aeb06f138002bc728da24d090b53b02666252db3 Mon Sep 17 00:00:00 2001 From: Trevor SANDY Date: Fri, 2 Jun 2023 00:20:49 +0200 Subject: [PATCH 02/14] Blender addon - settings dialog --- common/lc_blenderpreferences.cpp | 3618 ++++++++++++++++++++++++++++++ common/lc_blenderpreferences.h | 484 ++++ leocad.pro | 2 + 3 files changed, 4104 insertions(+) create mode 100644 common/lc_blenderpreferences.cpp create mode 100644 common/lc_blenderpreferences.h diff --git a/common/lc_blenderpreferences.cpp b/common/lc_blenderpreferences.cpp new file mode 100644 index 00000000..0bf6e660 --- /dev/null +++ b/common/lc_blenderpreferences.cpp @@ -0,0 +1,3618 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lc_blenderpreferences.h" +#include "lc_application.h" +#include "lc_mainwindow.h" +#include "lc_model.h" +#include "lc_colors.h" +#include "lc_colorpicker.h" +#include "lc_profile.h" +#include "lc_http.h" +#include "project.h" + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) +using Qt::SkipEmptyParts; +#else +const auto SkipEmptyParts = QString::SplitBehavior::SkipEmptyParts; +#endif + +const QLatin1String LineEnding("\r\n"); + +#define LC_PRODUCTNAME_STR "LeoCAD" +#define LC_BLENDER_ADDON "ImportLDraw" +#define LC_BLENDER_ADDON_MM "ImportLDrawMM" +#define LC_BLENDER_ADDON_FILE "LDrawBlenderRenderAddons.zip" +#define LC_BLENDER_ADDON_INSTALL_FILE "install_blender_ldraw_addons.py" +#define LC_BLENDER_ADDON_CONFIG_FILE "LDrawRendererPreferences.ini" +#define LC_BLENDER_ADDON_PARAMS_FILE "BlenderLDrawParameters.lst" + +#define LC_BLENDER_ADDON_REPO_STR "https://github.com/trevorsandy" +#define LC_BLENDER_ADDON_REPO_API_STR "https://api.github.com/repos/trevorsandy" +#define LC_BLENDER_ADDON_STR LC_BLENDER_ADDON_REPO_STR "/blenderldrawrender/" +#define LC_BLENDER_ADDON_API_STR LC_BLENDER_ADDON_REPO_API_STR "/blenderldrawrender/" +#define LC_BLENDER_ADDON_LATEST_URL LC_BLENDER_ADDON_API_STR "releases/latest" +#define LC_BLENDER_ADDON_URL LC_BLENDER_ADDON_STR "releases/latest/download/" LC_BLENDER_ADDON_FILE + +#define LC_THEME_DARK_PALETTE_MIDLIGHT "#3E3E3E" // 62, 62, 62, 255 +#define LC_THEME_DEFAULT_PALETTE_LIGHT "#AEADAC" // 174, 173, 172, 255 +#define LC_THEME_DARK_DECORATE_QUOTED_TEXT "#81D4FA" // 129, 212, 250, 255 +#define LC_DISABLED_TEXT "#808080" // 128, 128, 128, 255 + +#define LC_RENDER_IMAGE_MAX_SIZE 32768 // pixels + +static QString WhatsThisDescription = QObject::tr( + " Blender LDraw Addon Settings\n\n" + " You can configure the Blender LDraw addon settings.\n" + " - Blender Executable: set Blender path. On entry,\n" + " %1 will automatically apply the setting and\n" + " attempt the configure the LDraw addon.\n" + " - %1 Blender LDraw Addon: you can update the LDraw\n" + " addon which will downlod the latest addon or apply\n" + " the current addon if the version is the same or newer\n" + " than the online version.\n" + " You can view the standard output log for the update.\n\n" + " - Enabled Addon Modules: check the desired import module.\n" + " The LDraw Import TN import module is the long-standing\n" + " Blender LDraw import addon, while the LDraw Import MM\n" + " addon was recently introduced. The latter addon also\n" + " offer LDraw export functionality.\n" + " The %1 3D Image Render addon is mandatory but if\n" + " no import module is enabled, none of the modules\n" + " will be enabled in Blender so it will not be possible\n" + " to perform an LDraw model import or render.\n\n" + " - LDraw Import Addon Paths: addon paths are specific\n" + " to the enabled addon import module.\n" + " - LDraw Import Addon Settings: addon settings are\n" + " specific to the enabled addon import module.\n" + " - Apply: apply the addon path and setting preferences.\n" + " - Show/Hide Paths: show or hide the addon paths\n" + " display box.\n" + " - Reset: reset the addon path and setting preferences.\n" + " You can select how to reset addon settings.\n" + " The choice is since last apply or system default.\n\n" + " You can see the specific description of each setting\n" + " if you hover over the setting to display its tooltip.\n\n" + " Image Width, Image Height and Render Percentage are\n" + " always updated from the current step model when the\n" + " this dialog is opened. These settngs can be manually\n" + " overridden, Also, when Crop Image is checked the\n" + " current step cropped image width and height is\n" + " calculated and and used\n\n" + " Use the dialogue window scroll bar to access the\n" + " complete selection of addon settings.\n") +.arg(QLatin1String(LC_PRODUCTNAME_STR)); + +lcBlenderPreferences::BlenderPaths lcBlenderPreferences::mBlenderPaths [NUM_PATHS]; +lcBlenderPreferences::BlenderPaths lcBlenderPreferences::mDefaultPaths [NUM_PATHS] = +{ + /* Key: MM Key: Value: Label: Tooltip (Description):*/ + /* 0 PATH_BLENDER */ {"blenderpath", "blenderpath", "", QObject::tr("Blender Path"), QObject::tr("Full file path to Blender application executable")}, + /* 1 PATH_BLENDFILE */ {"blendfile", "blendfile", "", QObject::tr("Blendfile Path"), QObject::tr("Full file path to a supplement .blend file - specify to append additional settings")}, + /* 2. PATH_ENVIRONMENT */ {"environmentfile", "environmentfile", "", QObject::tr("Environment Texture Path"), QObject::tr("Full file path to .exr environment texture file - specify if not using default bundled in addon")}, + /* 3 PATH_LDCONFIG */ {"customldconfigfile", "customldconfigfile", "", QObject::tr("Custom LDConfig Path"), QObject::tr("Full file path to custom LDConfig file - specify if not %1 alternate LDConfig file").arg(LC_PRODUCTNAME_STR)}, + /* 4 PATH_LDRAW */ {"ldrawdirectory", "ldrawpath", "", QObject::tr("LDraw Directory"), QObject::tr("Full directory path to the LDraw parts library (download from https://library.ldraw.org)")}, + /* 5 PATH_LSYNTH */ {"lsynthdirectory", "", "", QObject::tr("LSynth Directory"), QObject::tr("Full directory path to LSynth primitives - specify if not using default bundled in addon")}, + /* 6 PATH_STUD_LOGO */ {"studlogodirectory", "", "", QObject::tr("Stud Logo Directory"), QObject::tr("Full directory path to stud logo primitives - if stud logo enabled, specify if unofficial parts not used or not using default bundled in addon")}, + /* 7 PATH_STUDIO_LDRAW */ {"", "studioldrawpath", "", QObject::tr("Stud.io LDraw Path"), QObject::tr("Full filepath to the Stud.io LDraw Parts Library (download from https://www.bricklink.com/v3/studio/download.page)")} +}; + +lcBlenderPreferences::BlenderSettings lcBlenderPreferences::mBlenderSettings [NUM_SETTINGS]; +lcBlenderPreferences::BlenderSettings lcBlenderPreferences::mDefaultSettings [NUM_SETTINGS] = +{ + /* Key: Value: Label Tooltip (Description)*/ + /* 0 LBL_ADD_ENVIRONMENT */ {"addenvironment", "1", QObject::tr("Add Environment"), QObject::tr("Adds a ground plane and environment texture (affects 'Photo-realistic' look only)")}, + /* 1 LBL_ADD_GAPS */ {"gaps", "0", QObject::tr("Add Part Gap"), QObject::tr("Add a small space between each part")}, + /* 2 LBL_BEVEL_EDGES */ {"beveledges", "1", QObject::tr("Bevel Edges"), QObject::tr("Adds a Bevel modifier for rounding off sharp edges")}, + /* 3 LBL_BLENDFILE_TRUSTED */ {"blendfiletrusted", "0", QObject::tr("Trusted Blend File"), QObject::tr("Specify whether to treat the .blend file as being loaded from a trusted source")}, + /* 4 LBL_CROP_IMAGE */ {"cropimage", "0", QObject::tr("Crop Image"), QObject::tr("Crop the image border at opaque content. Requires transparent background set to True")}, + /* 5 LBL_CURVED_WALLS */ {"curvedwalls", "1", QObject::tr("Curved Walls"), QObject::tr("Makes surfaces look slightly concave")}, + /* 6 LBL_FLATTEN_HIERARCHY */ {"flattenhierarchy", "0", QObject::tr("Flatten Hierarchy"), QObject::tr("In Scene Outline, all parts are placed directly below the root - there's no tree of submodels")}, + /* 7 LBL_IMPORT_CAMERAS */ {"importcameras", "1", QObject::tr("Import Cameras"), QObject::tr("%1 can specify camera definitions within the ldraw data. Choose to load them or ignore them.").arg(LC_PRODUCTNAME_STR)}, + /* 8 LBL_IMPORT_LIGHTS */ {"importlights", "1", QObject::tr("Import Lights"), QObject::tr("%1 can specify point and sunlight definitions within the ldraw data. Choose to load them or ignore them.").arg(LC_PRODUCTNAME_STR)}, + /* 9 LBL_INSTANCE_STUDS */ {"instancestuds", "0", QObject::tr("Instance Studs"), QObject::tr("Creates a Blender Object for each and every stud (WARNING: can be slow to import and edit in Blender if there are lots of studs)")}, + /*10 LBL_KEEP_ASPECT_RATIO */ {"keepaspectratio", "1", QObject::tr("Keep Aspect Ratio"), QObject::tr("Maintain the aspect ratio when resizing the output image - this attribute is not passed to Blender")}, + /*11 LBL_LINK_PARTS */ {"linkparts", "1", QObject::tr("Link Like Parts"), QObject::tr("Identical parts (of the same type and colour) share the same mesh")}, + /*12 LBL_MINIFIG_HIERARCHY */ {"minifighierarchy", "1", QObject::tr("Parent Minifigs"), QObject::tr("Parts of minifigs are automatically parented to each other in a hierarchy")}, + /*13 LBL_NUMBER_NODES */ {"numbernodes", "1", QObject::tr("Number Objects"), QObject::tr("Each object has a five digit prefix eg. 00001_car. This keeps the list in it's proper order")}, + /*14 LBL_OVERWRITE_IMAGE */ {"overwriteimage", "1", QObject::tr("Overwrite Image"), QObject::tr("Specify whether to overwrite an existing rendered image file")}, + /*15 LBL_OVERWRITE_MATERIALS */ {"overwriteexistingmaterials", "0", QObject::tr("Use Existing Material"), QObject::tr("Overwrite existing material with the same name")}, + /*16 LBL_OVERWRITE_MESHES */ {"overwriteexistingmeshes", "0", QObject::tr("Use Existing Mesh"), QObject::tr("Overwrite existing mesh with the same name")}, + /*17 LBL_POSITION_CAMERA */ {"positioncamera", "1", QObject::tr("Position Camera"), QObject::tr("Position the camera to show the whole model")}, + /*18 LBL_REMOVE_DOUBLES */ {"removedoubles", "1", QObject::tr("No Duplicate Vertices"), QObject::tr("Remove duplicate vertices (recommended)")}, + /*19 LBL_RENDER_WINDOW */ {"renderwindow", "1", QObject::tr("Display Render Window"), QObject::tr("Specify whether to display the render window during Blender user interface image file render")}, + /*10 LBL_USE_ARCHIVE_LIBS */ {"usearchivelibrary", "0", QObject::tr("Use Archive Libraries"), QObject::tr("Add any archive (zip) libraries in the LDraw file path to the library search list")}, + /*21 LBL_SEARCH_ADDL_PATHS */ {"searchadditionalpaths", "0", QObject::tr("Search Additional Paths"),QObject::tr("Specify whether to search additional LDraw paths")}, + /*22 LBL_SMOOTH_SHADING */ {"smoothshading", "1", QObject::tr("Smooth Shading"), QObject::tr("Smooth faces and add an edge-split modifier (recommended)")}, + /*23 LBL_TRANSPARENT_BACKGROUND*/ {"transparentbackground", "0", QObject::tr("Transparent Background"), QObject::tr("Specify whether to render a background (affects 'Photo-realistic look only)")}, + /*24 LBL_UNOFFICIAL_PARTS */ {"useunofficialparts", "1", QObject::tr("Use Unofficial Parts"), QObject::tr("Specify whether to use parts from the LDraw unofficial parts library path")}, + /*25 LBL_USE_LOGO_STUDS */ {"uselogostuds", "0", QObject::tr("Use Logo Studs"), QObject::tr("Shows the LEGO logo on each stud (at the expense of some extra geometry and import time)")}, + /*26 LBL_VERBOSE */ {"verbose", "1", QObject::tr("Verbose output"), QObject::tr("Output all messages while working, else only show warnings and errors")}, + + /*27/0 LBL_BEVEL_WIDTH */ {"bevelwidth", "0.5", QObject::tr("Bevel Width"), QObject::tr("Width of the bevelled edges")}, + /*28/1 LBL_CAMERA_BORDER_PERCENT */ {"cameraborderpercentage", "5.0", QObject::tr("Camera Border Percent"), QObject::tr("When positioning the camera, include a (percentage) border around the model in the render")}, + /*29/2 LBL_DEFAULT_COLOUR */ {"defaultcolour", "16", QObject::tr("Default Colour"), QObject::tr("Sets the default part colour using LDraw colour code")}, + /*20/3 LBL_GAPS_SIZE */ {"gapwidth", "0.01", QObject::tr("Gap Width"), QObject::tr("Amount of gap space between each part")}, + /*31/4 LBL_IMAGE_WIDTH */ {"resolutionwidth", "800", QObject::tr("Image Width"), QObject::tr("Sets the rendered image width in pixels - from current step image, label shows config setting.")}, + /*32/5 LBL_IMAGE_HEIGHT */ {"resolutionheight", "600", QObject::tr("Image Height"), QObject::tr("Sets the rendered image height in pixels - from current step image, label shows config setting.")}, + /*33/6 LBL_IMAGE_SCALE */ {"scale", "0.02", QObject::tr("Image Scale"), QObject::tr("Sets the imported model scale (between .01 and 1.0). Scale is 1.0 and is huge and unwieldy in the viewport")}, + /*34/6 LBL_RENDER_PERCENTAGE */ {"renderpercentage", "100", QObject::tr("Render Percentage"), QObject::tr("Sets the rendered image percentage scale for its pixel resolution - updated from current step, label shows config setting.")}, + + /*35/0 LBL_COLOUR_SCHEME */ {"usecolourscheme", "lgeo", QObject::tr("Colour Scheme"), QObject::tr("Colour scheme options - Realistic (lgeo), Original (LDConfig), Alternate (LDCfgalt), Custom (User Defined)")}, + /*36/1 LBL_FLEX_PARTS_SOURCE */ {"uselsynthparts", "1", QObject::tr("Flex Parts Source"), QObject::tr("Source used to create flexible parts - string, hoses etc. (LDCad, LSynth or both")}, + /*27/2 LBL_LOGO_STUD_VERSION */ {"logostudversion", "4", QObject::tr("Logo Version"), QObject::tr("Which version of the logo to use ('3' (flat), '4' (rounded) or '5' (subtle rounded))")}, + /*38/3 LBL_LOOK */ {"uselook", "normal", QObject::tr("Look"), QObject::tr("Photo-realistic or Schematic 'Instruction' look")}, + /*39/4 LBL_POSITION_OBJECT */ {"positionobjectongroundatorigin", "1", QObject::tr("Position Object"), QObject::tr("The object is centred at the origin, and on the ground plane")}, + /*40/5 LBL_RESOLUTION */ {"resolution", "Standard", QObject::tr("Resolution"), QObject::tr("Resolution of part primitives, ie. how much geometry they have")}, + /*41/6 LBL_RESOLVE_NORMALS */ {"resolvenormals", "guess", QObject::tr("Resolve Normals"), QObject::tr("Some older LDraw parts have faces with ambiguous normals, this specifies what do do with them")} +}; + +lcBlenderPreferences::ComboItems lcBlenderPreferences::mComboItems [NUM_COMBO_ITEMS] = +{ + /* FIRST item set as default Data Item: */ + /* 00 LBL_COLOUR_SCHEME */ {"lgeo|ldraw|alt|custom", QObject::tr("Realistic Colours|Original LDraw Colours|Alternate LDraw Colours|Custom Colours")}, + /* 01 LBL_FLEX_PARTS_SOURCE (t/f) */ {"1|0|1", QObject::tr("LSynth|LDCad|LDCad and LSynth")}, + /* 02 LBL_LOGO_STUD_VERSION */ {"4|3|5", QObject::tr("Rounded(4)|Flattened(3)|Subtle Rounded(5)")}, + /* 03 LBL_LOOK */ {"normal|instructions", QObject::tr("Photo Realistic|Lego Instructions")}, + /* 04 LBL_POSITION_OBJECT (t/f) */ {"1|0", QObject::tr("Centered At Origin On Ground|Centered At Origin")}, + /* 05 LBL_RESOLUTION */ {"Standard|High|Low", QObject::tr("Standard Primitives|High Resolution Primitives|Low Resolution Primitives")}, + /* 06 LBL_RESOLVE_NORMALS */ {"guess|double", QObject::tr("Recalculate Normals|Two Faces Back To Back")} +}; + +lcBlenderPreferences::BlenderSettings lcBlenderPreferences::mBlenderSettingsMM [NUM_SETTINGS_MM]; +lcBlenderPreferences::BlenderSettings lcBlenderPreferences::mDefaultSettingsMM [NUM_SETTINGS_MM] = +{ + /* Key: Value: Label Tooltip (Description)*/ + /* 00 LBL_ADD_ENVIRONMENT_MM */ {"addenvironment", "1", QObject::tr("Add Environment"), QObject::tr("Adds a ground plane and environment texture")}, + /* 01 LBL_BEVEL_EDGES_MM */ {"beveledges", "0", QObject::tr("Bevel Edgest"), QObject::tr("Bevel edges. Can cause some parts to render incorrectly")}, + /* 02 LBL_BLEND_FILE_TRUSTED_MM */ {"blendfiletrusted", "0", QObject::tr("Trusted Blend File"), QObject::tr("Specify whether to treat the .blend file as being loaded from a trusted source")}, + /* 03 LBL_CROP_IMAGE_MM */ {"cropimage", "0", QObject::tr("Crop Image"), QObject::tr("Crop the image border at opaque content. Requires transparent background set to True")}, + /* 04 LBL_DISPLAY_LOGO */ {"displaylogo", "0", QObject::tr("Display Logo"), QObject::tr("Display the logo on the stud")}, + /* 05 LBL_IMPORT_CAMERAS_MM */ {"importcameras", "1", QObject::tr("Import Cameras"), QObject::tr("%1 can specify camera definitions within the ldraw data. Choose to load them or ignore them.").arg(LC_PRODUCTNAME_STR)}, + /* 06 LBL_IMPORT_EDGES */ {"importedges", "0", QObject::tr("Import Edges"), QObject::tr("Import LDraw edges as edges")}, + /* 07 LBL_IMPORT_LIGHTS_MM */ {"importlights", "0", QObject::tr("Import Lights"), QObject::tr("%1 can specify point and sunlight definitions within the ldraw data. Choose to load them or ignore them.").arg(LC_PRODUCTNAME_STR)}, + /* 08 LBL_KEEP_ASPECT_RATIO_MM */ {"keepaspectratio", "1", QObject::tr("Keep Aspect Ratio"), QObject::tr("Maintain the aspect ratio when resizing the output image - this attribute is not passed to Blender")}, + /* 09 LBL_MAKE_GAPS */ {"makegaps", "1", QObject::tr("Make Gaps"), QObject::tr("Make small gaps between bricks. A small gap is more realistic")}, + /* 10 LBL_META_BFC */ {"metabfc", "1", QObject::tr("BFC"), QObject::tr("Process LDraw Back Face Culling meta commands")}, + /* 11 LBL_META_CLEAR */ {"metaclear", "0", QObject::tr("CLEAR Command"), QObject::tr("Hides all parts in the timeline up to where this command is encountered")}, + /* 12 LBL_META_GROUP */ {"metagroup", "1", QObject::tr("GROUP Command"), QObject::tr("Process GROUP meta commands")}, + /* 13 LBL_META_PAUSE */ {"metapause", "0", QObject::tr("PAUSE Command"), QObject::tr("Not implemented")}, + /* 14 LBL_META_PRINT_WRITE */ {"metaprintwrite", "0", QObject::tr("PRINT/WRITE Command"), QObject::tr("Prints PRINT/WRITE META commands to the system console.")}, + /* 15 LBL_META_SAVE */ {"metasave", "0", QObject::tr("SAVE Command"), QObject::tr("Not implemented")}, + /* 16 LBL_META_STEP */ {"metastep", "0", QObject::tr("STEP Command"), QObject::tr("Adds a keyframe that shows the part at the moment in the timeline")}, + /* 17 LBL_META_STEP_GROUPS */ {"metastepgroups", "0", QObject::tr("STEP Groups"), QObject::tr("Create collection for individual steps")}, + /* 18 LBL_META_TEXMAP */ {"metatexmap", "1", QObject::tr("TEXMAP and DATA Command"), QObject::tr("Process TEXMAP and DATA meta commands")}, + /* 19 LBL_NO_STUDS */ {"nostuds", "0", QObject::tr("No Studs"), QObject::tr("Don't import studs")}, + /* 20 LBL_OVERWRITE_IMAGE_MM */ {"overwriteimage", "1", QObject::tr("Overwrite Image"), QObject::tr("Specify whether to overwrite an existing rendered image file")}, + /* 21 LBL_POSITION_CAMERA_MM */ {"positioncamera", "1", QObject::tr("Position Camera"), QObject::tr("Position the camera to show the whole model")}, + /* 22 LBL_PARENT_TO_EMPTY */ {"parenttoempty", "1", QObject::tr("Parent To Empty"), QObject::tr("Parent the model to an empty")}, + /* 23 LBL_PREFER_STUDIO */ {"preferstudio", "0", QObject::tr("Prefer Stud.io Library"), QObject::tr("Search for parts in Stud.io library first")}, + /* 24 LBL_PREFER_UNOFFICIAL */ {"preferunofficial", "0", QObject::tr("Prefer Unofficial Parts"), QObject::tr("Search for unofficial parts first")}, + /* 25 LBL_PRESERVE_HIERARCHY */ {"preservehierarchy", "0", QObject::tr("Preserve File Structure"), QObject::tr("Don't merge the constituent subparts and primitives into the top level part. Some parts may not render properly")}, + /* 26 LBL_PROFILE */ {"profile", "0", QObject::tr("Profile"), QObject::tr("Profile import performance")}, + /* 27 LBL_RECALCULATE_NORMALS */ {"recalculatenormals", "0", QObject::tr("Recalculate Normals"), QObject::tr("Recalculate normals. Not recommended if BFC processing is active")}, + /* 28 LBL_REMOVE_DOUBLES_MM */ {"removedoubles", "1", QObject::tr("No Duplicate Vertices"), QObject::tr("Merge vertices that are within a certain distance.")}, + /* 29 LBL_RENDER_WINDOW_MM */ {"renderwindow", "1", QObject::tr("Display Render Window"), QObject::tr("Specify whether to display the render window during Blender user interface image file render")}, + /* 30 LBL_SEARCH_ADDL_PATHS_MM */ {"searchadditionalpaths", "0", QObject::tr("Search Additional Paths"), QObject::tr("Specify whether to search additional LDraw paths")}, + /* 31 LBL_SETEND_FRAME */ {"setendframe", "1", QObject::tr("Set Step End Frame"), QObject::tr("Set the end frame to the last step")}, + /* 32 LBL_SET_TIMELINE_MARKERS */ {"settimelinemarkers", "0", QObject::tr("Set Timeline Markers"), QObject::tr("Set timeline markers for meta commands")}, + /* 33 LBL_SHADE_SMOOTH */ {"shadesmooth", "1", QObject::tr("Shade Smooth"), QObject::tr("Use flat or smooth shading for part faces")}, + /* 34 LBL_TRANSPARENT_BACKGROUND_MM */ {"transparentbackground", "0", QObject::tr("Transparent Background"), QObject::tr("Specify whether to render a background")}, + /* 35 LBL_TREAT_MODELS_WITH_SUBPARTS_AS_PARTS */ {"treatmodelswithsubpartsasparts","1", QObject::tr("Treat Subparts As Parts"), QObject::tr("Treat subpart like a part by merging its constituent parts into one object")}, + /* 36 LBL_TREAT_SHORTCUT_AS_MODEL */ {"treatshortcutasmodel", "0", QObject::tr("Treat Shortcuts As Models"),QObject::tr("Split shortcut parts into their constituent pieces as if they were models")}, + /* 37 LBL_TRIANGULATE */ {"triangulate", "0", QObject::tr("Triangulate Faces"), QObject::tr("Triangulate all faces")}, + /* 38 LBL_USE_ARCHIVE_LIBRARY_MM */ {"usearchivelibrary", "0", QObject::tr("Use Archive Libraries"), QObject::tr("Add any archive (zip) libraries in the LDraw file path to the library search list")}, + /* 39 LBL_USE_FREESTYLE_EDGES */ {"usefreestyleedges", "0", QObject::tr("Use Freestyle Edges"), QObject::tr("Render LDraw edges using freestyle")}, + /* 40 LBL_VERBOSE_MM */ {"verbose", "1", QObject::tr("Verbose output"), QObject::tr("Output all messages while working, else only show warnings and errors")}, + + /* 41/00 LBL_BEVEL_SEGMENTS */ {"bevelsegments", "4", QObject::tr("Bevel Segments"), QObject::tr("Bevel segments")}, + /* 42/01 LBL_BEVEL_WEIGHT */ {"bevelweight", "0.3", QObject::tr("Bevel Weight"), QObject::tr("Bevel weight")}, + /* 43/02 LBL_BEVEL_WIDTH_MM */ {"bevelwidth", "0.3", QObject::tr("Bevel Width"), QObject::tr("Width of the bevelled edges")}, + /* 44/03 LBL_CAMERA_BORDER_PERCENT_MM */ {"cameraborderpercent", "5", QObject::tr("Camera Border Percent"), QObject::tr("When positioning the camera, include a (percentage) border around the model in the render")}, + /* 45/04 LBL_FRAMES_PER_STEP */ {"framesperstep", "3", QObject::tr("Frames Per Step"), QObject::tr("Frames per step")}, + /* 46/05 LBL_GAP_SCALE */ {"gapscale", "0.997", QObject::tr("Gap Scale"), QObject::tr("Scale individual parts by this much to create the gap")}, + /* 47/06 LBL_IMPORT_SCALE */ {"importscale", "0.02", QObject::tr("Import Scale"), QObject::tr("What scale to import at. Full scale is 1.0 and is so huge that it is unwieldy in the viewport")}, + /* 48/07 LBL_MERGE_DISTANCE */ {"mergedistance", "0.05", QObject::tr("Merge Distance"), QObject::tr("Maximum distance between elements to merge")}, + /* 49/08 LBL_RENDER_PERCENTAGE_MM */ {"renderpercentage", "100", QObject::tr("Render Percentage"), QObject::tr("Sets the rendered image percentage scale for its pixel resolution - updated from current step, label shows config setting.")}, + /* 50/09 LBL_RESOLUTION_WIDTH */ {"resolutionwidth", "800", QObject::tr("Image Width"), QObject::tr("Sets the rendered image width in pixels - from current step image, label shows config setting.")}, + /* 51/10 LBL_RESOLUTION_HEIGHT */ {"resolutionheight", "600", QObject::tr("Image Height"), QObject::tr("Sets the rendered image height in pixels - from current step image, label shows config setting.")}, + /* 52/11 LBL_STARTING_STEP_FRAME */ {"startingstepframe", "1", QObject::tr("Starting Step Frame"), QObject::tr("Frame to add the first STEP meta command")}, + + /* 53/00 LBL_CHOSEN_LOGO */ {"chosenlogo", "logo3", QObject::tr("Chosen Logo"), QObject::tr("Which logo to display. logo and logo2 aren't used and are only included for completeness")}, + /* 54/01 LBL_COLOUR_SCHEME_MM */ {"usecolourscheme", "lgeo", QObject::tr("Colour Scheme"), QObject::tr("Colour scheme options - Realistic (lgeo), Original (LDConfig), Alternate (LDCfgalt), Custom (User Defined)")}, + /* 55/02 LBL_GAP_SCALE_STRATEGY */ {"gapscalestrategy", "constraint", QObject::tr("Gap Strategy"), QObject::tr("Apply gap to object directly or scale and empty to adjust to gaps between parts")}, + /* 56/03 LBL_GAP_TARGET */ {"gaptarget", "object", QObject::tr("Gap Target"), QObject::tr("Whether to scale the object data or mesh data")}, + /* 57/04 LBL_RESOLUTION_MM */ {"resolution", "Standard", QObject::tr("Resolution"), QObject::tr("Resolution of part primitives, ie. how much geometry they have")}, + /* 58/05 LBL_SMOOTH_TYPE */ {"smoothtype", "edge_split", QObject::tr("Smooth Type"), QObject::tr("Use either autosmooth or an edge split modifier to smooth part faces")} +}; + +lcBlenderPreferences::ComboItems lcBlenderPreferences::mComboItemsMM [NUM_COMBO_ITEMS_MM] = +{ + /* FIRST item set as default Data Item: */ + /* 00 LBL_CHOSEN_LOGO */ {"logo|logo2|logo3", QObject::tr("Logo (Not Used)|Logo2 (Not Used)|Flattened(3)")}, + /* 01 LBL_COLOUR_SCHEME_MM */ {"lgeo|ldraw|alt|custom", QObject::tr("Realistic Colours|Original LDraw Colours|Alternate LDraw Colours|Custom Colours")}, + /* 02 LBL_GAP_SCALE_STRATEGY */ {"object|constraint", QObject::tr("Gap applied directly to object|Gap scaled to adjust to gaps between parts")}, + /* 03 LBL_GAP_TARGET */ {"object|mesh", QObject::tr("Scale object data|Scale mesh data")}, + /* 04 LBL_RESOLUTION_MM */ {"Low|Standard|High", QObject::tr("Low Resolution Primitives|Standard Primitives|High Resolution Primitives")}, + /* 05 LBL_SMOOTH_TYPE */ {"edge_split|auto_smooth|bmesh_split", QObject::tr("Smooth part faces with edge split modifier|Auto-smooth part faces|Split during initial mesh processing")} +}; + +lcBlenderPreferences* gAddonPreferences; + +lcBlenderPreferencesDialog::lcBlenderPreferencesDialog(int Width, int Height, double Scale, QWidget* Parent) + : QDialog(Parent) +{ + setWindowTitle(tr("Blender LDraw Addon Settings")); + + QVBoxLayout* Layout = new QVBoxLayout(this); + setLayout(Layout); + + QGroupBox* Box = new QGroupBox(this); + Layout->addWidget(Box); + + mPreferences = new lcBlenderPreferences(Width,Height,Scale,Box); + + QDialogButtonBox* ButtonBox; + ButtonBox = new QDialogButtonBox(this); + mApplyButton = new QPushButton(tr("Apply"), ButtonBox); + mApplyButton->setToolTip(tr("Apply addon paths and settings preferences")); + mApplyButton->setEnabled(false); + ButtonBox->addButton(mApplyButton, QDialogButtonBox::ActionRole); + connect(mApplyButton,SIGNAL(clicked()), this, SLOT(accept())); + + mPathsButton = new QPushButton(tr("Hide Paths"), ButtonBox); + mPathsButton->setToolTip(tr("Hide addon path preferences dialog")); + ButtonBox->addButton(mPathsButton,QDialogButtonBox::ActionRole); + connect(mPathsButton,SIGNAL(clicked()), this, SLOT(ShowPathsGroup())); + + mResetButton = new QPushButton(tr("Reset"), ButtonBox); + mResetButton->setEnabled(false); + mResetButton->setToolTip(tr("Reset addon paths and settings preferences")); + ButtonBox->addButton(mResetButton,QDialogButtonBox::ActionRole); + connect(mResetButton,SIGNAL(clicked()), this, SLOT(ResetSettings())); + + ButtonBox->addButton(QDialogButtonBox::Cancel); + connect(ButtonBox,SIGNAL(rejected()), this, SLOT(reject())); + + if (!QFileInfo(lcGetProfileString(LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH)).isReadable() && !lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE).isEmpty()) + mApplyButton->setEnabled(true); + + connect(mPreferences,SIGNAL(SettingChangedSig(bool)), this, SLOT(EnableButton(bool))); + + Layout->addWidget(ButtonBox); + + setMinimumSize(Box->sizeHint().width() + 50, 500); + + setSizeGripEnabled(true); + + setModal(true); +} + +lcBlenderPreferencesDialog::~lcBlenderPreferencesDialog() +{ +} + +bool lcBlenderPreferencesDialog::GetBlenderPreferences(int& Width, int& Height, double & Scale, QWidget* Parent) +{ + lcBlenderPreferencesDialog* Dialog = new lcBlenderPreferencesDialog(Width,Height,Scale,Parent); + + bool Ok = Dialog->exec() == QDialog::Accepted; + + if (Ok) + { + Dialog->mPreferences->Apply(Ok); + Width = Dialog->mPreferences->mImageWidth; + Height = Dialog->mPreferences->mImageHeight; + Scale = Dialog->mPreferences->mScale; + } + + return Ok; +} + +void lcBlenderPreferencesDialog::ShowPathsGroup() +{ + QString const Display = mPathsButton->text().startsWith("Hide") ? tr("Show") : tr("Hide"); + mPathsButton->setText(tr("%1 Paths").arg(Display)); + mPathsButton->setToolTip(tr("%1 addon path preferences dialog").arg(Display)); + mPreferences->ShowPathsGroup(); +} + +void lcBlenderPreferencesDialog::EnableButton(bool Change) +{ + mApplyButton->setEnabled(Change); + mResetButton->setEnabled(Change); + mPathsButton->setText(tr("Hide Paths")); + mPathsButton->setToolTip(tr("Hide addon path preferences dialog")); +} + +void lcBlenderPreferencesDialog::ResetSettings() +{ + mPreferences->ResetSettings(); + mApplyButton->setEnabled(false); +} + +void lcBlenderPreferencesDialog::accept() +{ + if (mPreferences->SettingsModified()) + { + mApplyButton->setEnabled(false); + mPreferences->SaveSettings(); + QDialog::accept(); + } else + QDialog::reject(); +} + +void lcBlenderPreferencesDialog::reject() +{ + if (mPreferences->PromptCancel()) + QDialog::reject(); +} + +lcBlenderPreferences::lcBlenderPreferences(int Width, int Height, double Scale, QWidget* Parent) + : QWidget(Parent) +{ + gAddonPreferences = this; + +#ifndef QT_NO_PROCESS + mProcess = nullptr; +#endif + + mImageWidth = Width; + mImageHeight = Height; + mScale = Scale; + + mDialogCancelled = false; + + QVBoxLayout* Layout = new QVBoxLayout(Parent); + + if (Parent) + { + Parent->setLayout(Layout); + Parent->setWhatsThis(WhatsThisDescription); + } + else + { + setWindowTitle(tr("Blender LDraw Addon Settings")); + setLayout(Layout); + setWhatsThis(WhatsThisDescription); + } + + mContent = new QWidget(); + + mForm = new QFormLayout(mContent); + + mContent->setLayout(mForm); + + QPalette ReadOnlyPalette = QApplication::palette(); + const lcPreferences& Preferences = lcGetPreferences(); + if (Preferences.mColorTheme == lcColorTheme::Dark) + ReadOnlyPalette.setColor(QPalette::Base,QColor(LC_THEME_DARK_PALETTE_MIDLIGHT)); + else + ReadOnlyPalette.setColor(QPalette::Base,QColor(LC_THEME_DEFAULT_PALETTE_LIGHT)); + ReadOnlyPalette.setColor(QPalette::Text,QColor(LC_DISABLED_TEXT)); + + QGroupBox* BlenderExeBox = new QGroupBox(tr("Blender Executable"),mContent); + mForm->addRow(BlenderExeBox); + + mExeGridLayout = new QGridLayout(BlenderExeBox); + BlenderExeBox->setLayout(mExeGridLayout); + + mBlenderVersionLabel = new QLabel(BlenderExeBox); + mExeGridLayout->addWidget(mBlenderVersionLabel,0,0); + + mBlenderVersionEdit = new QLineEdit(BlenderExeBox); + mBlenderVersionEdit->setPalette(ReadOnlyPalette); + mBlenderVersionEdit->setReadOnly(true); + mExeGridLayout->addWidget(mBlenderVersionEdit,0,1,1,2); + + QLabel* PathLabel = new QLabel(BlenderExeBox); + mExeGridLayout->addWidget(PathLabel,1,0); + + QLineEdit* PathLineEdit = new QLineEdit(BlenderExeBox); + mExeGridLayout->addWidget(PathLineEdit,1,1); + mPathLineEditList << PathLineEdit; + connect(PathLineEdit, SIGNAL(editingFinished()), this, SLOT(ConfigureBlenderAddon())); + + QPushButton* PathBrowseButton = new QPushButton(tr("Browse..."), BlenderExeBox); + mExeGridLayout->addWidget(PathBrowseButton,1,2); + mPathBrowseButtonList << PathBrowseButton; + connect(PathBrowseButton, SIGNAL(clicked(bool)), this, SLOT(BrowseBlender(bool))); + + QGroupBox* BlenderAddonVersionBox = new QGroupBox(tr("%1 Blender LDraw Addon").arg(LC_PRODUCTNAME_STR),mContent); + mForm->addRow(BlenderAddonVersionBox); + + mAddonGridLayout = new QGridLayout(BlenderAddonVersionBox); + BlenderAddonVersionBox->setLayout(mAddonGridLayout); + + mAddonVersionLabel = new QLabel(BlenderAddonVersionBox); + mAddonGridLayout->addWidget(mAddonVersionLabel,0,0); + + mAddonVersionEdit = new QLineEdit(BlenderAddonVersionBox); + mAddonVersionEdit->setToolTip(tr("%1 Blender LDraw import and image renderer addon").arg(LC_PRODUCTNAME_STR)); + mAddonVersionEdit->setPalette(ReadOnlyPalette); + mAddonVersionEdit->setReadOnly(true); + mAddonGridLayout->addWidget(mAddonVersionEdit,0,1); + mAddonGridLayout->setColumnStretch(1,1/*1 is greater than 0 (default)*/); + + mAddonUpdateButton = new QPushButton(tr("Update"), BlenderAddonVersionBox); + mAddonUpdateButton->setToolTip(tr("Update %1 Blender LDraw addon").arg(LC_PRODUCTNAME_STR)); + mAddonGridLayout->addWidget(mAddonUpdateButton,0,2); + connect(mAddonUpdateButton, SIGNAL(clicked(bool)), this, SLOT(UpdateBlenderAddon())); + + mAddonStdOutButton = new QPushButton(tr("Output..."), BlenderAddonVersionBox); + mAddonStdOutButton->setToolTip(tr("Open the standrd output log")); + mAddonStdOutButton->setEnabled(false); + mAddonGridLayout->addWidget(mAddonStdOutButton,0,3); + connect(mAddonStdOutButton, SIGNAL(clicked(bool)), this, SLOT(GetStandardOutput())); + + mModulesBox = new QGroupBox(tr("Enabled Addon Modules"),mContent); + QHBoxLayout* ModulesLayout = new QHBoxLayout(mModulesBox); + mModulesBox->setLayout(ModulesLayout); + mAddonGridLayout->addWidget(mModulesBox,1,0,1,4); + + mImportActBox = new QCheckBox(tr("LDraw Import TN"),mModulesBox); + mImportActBox->setToolTip(tr("Enable addon import module (adapted from LDraw Import by Toby Nelson) in Blender")); + ModulesLayout->addWidget(mImportActBox); + connect(mImportActBox, SIGNAL(clicked(bool)), this, SLOT(EnableImportModule())); + + mImportMMActBox = new QCheckBox(tr("LDraw Import MM"),mModulesBox); + mImportMMActBox->setToolTip(tr("Enable addon import module (adapted from LDraw Import by Matthew Morrison) in Blender")); + ModulesLayout->addWidget(mImportMMActBox); + connect(mImportMMActBox, SIGNAL(clicked(bool)), this, SLOT(EnableImportModule())); + + mRenderActBox = new QCheckBox(tr("%1 Image Render").arg(LC_PRODUCTNAME_STR), mModulesBox); + mRenderActBox->setToolTip(tr("Addon image render module in Blender")); + mRenderActBox->setEnabled(false); + ModulesLayout->addWidget(mRenderActBox); + + LoadSettings(); + + mConfigured = !lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE).isEmpty(); + + const int CtlIdx = PATH_BLENDER; + PathLabel->setText(mBlenderPaths[CtlIdx].label); + PathLabel->setToolTip(mBlenderPaths[CtlIdx].tooltip); + + PathLineEdit->setText(mBlenderPaths[CtlIdx].value); + PathLineEdit->setToolTip(mBlenderPaths[CtlIdx].tooltip); + + if (mAddonVersion.isEmpty()) + { + mModulesBox->setEnabled(false); + mAddonUpdateButton->setEnabled(false); + mImportActBox->setChecked(true); // default addon module + } + else + { + mAddonVersionEdit->setText(mAddonVersion); + mRenderActBox->setChecked(true); + mImportActBox->setChecked( lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE) == QLatin1String("TN")); + mImportMMActBox->setChecked(lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE) == QLatin1String("MM")); + } + + QString VersionTextColour = QApplication::palette().text().color().name(),AddonTextColour; + QString VersionText = tr("Blender"), AddonText = tr("Blender Addon"); + if (mConfigured) + { + AddonTextColour = VersionTextColour; + mBlenderVersionEdit->setText(mBlenderVersion); + mBlenderVersionEdit->setVisible(true); + } + else + { + QString const TextColour = Preferences.mColorTheme == lcColorTheme::Dark + ? QLatin1String(LC_THEME_DARK_DECORATE_QUOTED_TEXT) + : QLatin1String("blue"); + + bool BlenderConfigured = !lcGetProfileString(LC_PROFILE_BLENDER_PATH).isEmpty(); + if (!BlenderConfigured) + { + VersionText = tr("Blender not configured."); + VersionTextColour = TextColour; + } + else + mBlenderVersionEdit->setText(mBlenderVersion); + + if (QFileInfo(QString("%1/Blender/%2").arg(mDataDir).arg(LC_BLENDER_ADDON_FILE)).isReadable()) + { + mModulesBox->setEnabled(false); + mImportActBox->setChecked(true); + mAddonUpdateButton->setEnabled(true); + if (BlenderConfigured) + AddonText = tr("Addon not configured - Update."); + else + AddonText = tr("Addon not configured."); + AddonTextColour = TextColour; + } + } + + mAddonVersionEdit->setVisible(!mAddonVersion.isEmpty()); + mBlenderVersionLabel->setStyleSheet(QString("QLabel { color : %1; }").arg(VersionTextColour)); + mAddonVersionLabel->setStyleSheet(QString("QLabel { color : %1; }").arg(AddonTextColour)); + mBlenderVersionLabel->setText(VersionText); + mAddonVersionLabel->setText(AddonText); + + if (mImportMMActBox->isChecked()) + InitPathsAndSettingsMM(); + else + InitPathsAndSettings(); + + QScrollArea* ScrollArea = new QScrollArea(this); + ScrollArea->setWidgetResizable(true); + ScrollArea->setWidget(mContent); + Layout->addWidget(ScrollArea); +} + +lcBlenderPreferences::~lcBlenderPreferences() +{ + gAddonPreferences = nullptr; +} + +void lcBlenderPreferences::ClearGroupBox(QGroupBox* GroupBox) +{ + int Row = -1; + QFormLayout::ItemRole ItemRole = QFormLayout::SpanningRole; + + mForm->getWidgetPosition(GroupBox,& Row,& ItemRole); + + if(Row == -1 || ItemRole != QFormLayout::SpanningRole) return; + + QLayoutItem* GroupBoxIitem = mForm->itemAt ( Row, ItemRole ); + + mForm->removeItem(GroupBoxIitem); + + QWidget* WidgetItem = GroupBoxIitem->widget(); + if(WidgetItem) + { + if (GroupBox == mPathsBox) + { + delete mPathsBox; + mPathsBox = nullptr; + } + else if (GroupBox == mSettingsBox) + { + delete mSettingsBox; + mSettingsBox = nullptr; + } + } + + delete GroupBoxIitem; +} + +void lcBlenderPreferences::InitPathsAndSettings() +{ + if (!NumSettings()) + LoadSettings(); + + // Paths + ClearGroupBox(mPathsBox); + mPathsBox = new QGroupBox(mContent); + mPathsBox->setTitle(tr("LDraw Import TN Addon Paths")); + mPathsGridLayout = new QGridLayout(mPathsBox); + mPathsBox->setLayout(mPathsGridLayout); + mForm->addRow(mPathsBox); + + for (int CtlIdx = 1/*skip blender executable*/; CtlIdx < NumPaths(); ++CtlIdx) + { + int ColumnIdx = CtlIdx - 1; // adjust for skipping first item - blender executable + bool IsVisible = CtlIdx != PATH_STUDIO_LDRAW; + QLabel* PathLabel = new QLabel(mBlenderPaths[CtlIdx].label, mPathsBox); + PathLabel->setToolTip(mBlenderPaths[CtlIdx].tooltip); + mPathsGridLayout->addWidget(PathLabel,ColumnIdx,0); + PathLabel->setVisible(IsVisible); + + QLineEdit* PathLineEdit = new QLineEdit(mPathsBox); + PathLineEdit->setProperty("ControlID",QVariant(CtlIdx)); + PathLineEdit->setText(mBlenderPaths[CtlIdx].value); + PathLineEdit->setToolTip(mBlenderPaths[CtlIdx].tooltip); + if (mPathLineEditList.size() > CtlIdx) + mPathLineEditList.replace(CtlIdx, PathLineEdit); + else + mPathLineEditList << PathLineEdit; + mPathsGridLayout->addWidget(PathLineEdit,ColumnIdx,1); + PathLineEdit->setVisible(IsVisible); + + QPushButton* PathBrowseButton = new QPushButton(tr("Browse..."), mPathsBox); + if (mPathBrowseButtonList.size() > CtlIdx) + mPathBrowseButtonList.replace(CtlIdx, PathBrowseButton); + else + mPathBrowseButtonList << PathBrowseButton; + mPathsGridLayout->addWidget(PathBrowseButton,ColumnIdx,2); + PathBrowseButton->setVisible(IsVisible); + + if (IsVisible) + { + connect(PathBrowseButton, SIGNAL(clicked(bool)), this, SLOT (BrowseBlender(bool))); + connect(PathLineEdit, SIGNAL(editingFinished()), this, SLOT (PathChanged())); + } + } + + mPathsBox->setEnabled(mConfigured); + + // Settings + ClearGroupBox(mSettingsBox); + mSettingsBox = new QGroupBox(mContent); + mSettingsSubform = new QFormLayout(mSettingsBox); + mSettingsBox->setLayout(mSettingsSubform); + mForm->addRow(mSettingsBox); + + mSettingsBox->setTitle(tr("LDraw Import TN Addon Settings")); + mSettingLabelList.clear(); + mCheckBoxList.clear(); + mLineEditList.clear(); + mComboBoxList.clear(); + + int ComboBoxItemsIndex = 0; + + for (int LblIdx = 0; LblIdx < NumSettings(); LblIdx++) + { + QLabel* Label = new QLabel(mSettingsBox); + Label->setText(mBlenderSettings[LblIdx].label); + Label->setToolTip(mBlenderSettings[LblIdx].tooltip); + mSettingLabelList << Label; + + if (LblIdx < LBL_BEVEL_WIDTH) + { // QCheckBoxes + QCheckBox* CheckBox = new QCheckBox(mSettingsBox); + CheckBox->setProperty("ControlID",QVariant(LblIdx)); + CheckBox->setChecked(mBlenderSettings[LblIdx].value.toInt()); + CheckBox->setToolTip(mBlenderSettings[LblIdx].tooltip); + if (LblIdx == LBL_CROP_IMAGE) + connect(CheckBox, SIGNAL(toggled(bool)), this, SLOT (SetModelSize(bool))); + else + connect(CheckBox, SIGNAL(clicked()), this, SLOT (SettingChanged())); + mCheckBoxList << CheckBox; + mSettingsSubform->addRow(Label,CheckBox); + } + else if (LblIdx < LBL_COLOUR_SCHEME) + { // QLineEdits + QLineEdit* LineEdit = new QLineEdit(mSettingsBox); + LineEdit->setProperty("ControlID",QVariant(LblIdx)); + if (LblIdx == LBL_IMAGE_WIDTH || LblIdx == LBL_IMAGE_HEIGHT) + { + connect(LineEdit, SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + LineEdit->setValidator(new QIntValidator(16, LC_RENDER_IMAGE_MAX_SIZE)); + } + else if(LblIdx == LBL_DEFAULT_COLOUR) + { + LineEdit->setReadOnly(true); + LineEdit->setStyleSheet("Text-align:left"); + mDefaultColourEditAction = LineEdit->addAction(QIcon(), QLineEdit::TrailingPosition); + connect(mDefaultColourEditAction, SIGNAL(triggered(bool)), this, SLOT (ColorButtonClicked(bool))); + } + else + { + LineEdit->setText(mBlenderSettings[LblIdx].value); + if (LblIdx == LBL_IMAGE_SCALE) + LineEdit->setValidator(new QDoubleValidator(0.01,10.0,2)); + else if (LblIdx == LBL_RENDER_PERCENTAGE || LblIdx == LBL_CAMERA_BORDER_PERCENT) + LineEdit->setValidator(new QIntValidator(1,1000)); + else + LineEdit->setValidator(new QDoubleValidator(0.01,100.0,2)); + connect(LineEdit, SIGNAL(textEdited(const QString&)), this, SLOT (SettingChanged(const QString&))); + } + LineEdit->setToolTip(mBlenderSettings[LblIdx].tooltip); + mLineEditList << LineEdit; + if (LblIdx == LBL_DEFAULT_COLOUR) + SetDefaultColor(lcGetColorIndex(mBlenderSettings[LBL_DEFAULT_COLOUR].value.toInt())); + mSettingsSubform->addRow(Label,LineEdit); + } + else + { // QComboBoxes + QComboBox* ComboBox = new QComboBox(mSettingsBox); + ComboBox->setProperty("ControlID",QVariant(LblIdx)); + QString const Value = mBlenderSettings[LblIdx].value; + QStringList const DataList = mComboItems[ComboBoxItemsIndex].dataList.split("|"); + QStringList const ItemList = mComboItems[ComboBoxItemsIndex].itemList.split("|"); + ComboBox->addItems(ItemList); + for (int CtlIdx = 0; CtlIdx < ComboBox->count(); CtlIdx++) + ComboBox->setItemData(CtlIdx, DataList.at(CtlIdx)); + ComboBox->setToolTip(mBlenderSettings[LblIdx].tooltip); + int CurrentIndex = int(ComboBox->findData(QVariant::fromValue(Value))); + ComboBox->setCurrentIndex(CurrentIndex); + if (LblIdx == LBL_COLOUR_SCHEME) + connect(ComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT (ValidateColourScheme(int))); + else + connect(ComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT (SettingChanged(int))); + mComboBoxList << ComboBox; + ComboBoxItemsIndex++; + mSettingsSubform->addRow(Label,ComboBox); + } + } + + SetModelSize(); + + if (!mSettingsSubform->rowCount()) + mSettingsSubform = nullptr; + + mSettingsBox->setEnabled(mConfigured); +} + +void lcBlenderPreferences::InitPathsAndSettingsMM() +{ + if (!NumSettingsMM()) + LoadSettings(); + + // Paths + ClearGroupBox(mPathsBox); + mPathsBox = new QGroupBox(mContent); + mPathsBox->setTitle(tr("LDraw Import MM Addon Paths")); + mPathsGridLayout = new QGridLayout(mPathsBox); + mPathsBox->setLayout(mPathsGridLayout); + mForm->addRow(mPathsBox); + + for (int CtlIdx = 1/*skip blender executable*/; CtlIdx < NumPaths(); ++CtlIdx) + { + int ColumnIdx = CtlIdx - 1; // adjust for skipping first item - blender executable + bool IsVisible = CtlIdx != PATH_LSYNTH && CtlIdx != PATH_STUD_LOGO; + QLabel* PathLabel = new QLabel(mBlenderPaths[CtlIdx].label, mPathsBox); + PathLabel->setToolTip(mBlenderPaths[CtlIdx].tooltip); + mPathsGridLayout->addWidget(PathLabel,ColumnIdx,0); + PathLabel->setVisible(IsVisible); + + QLineEdit* PathLineEdit = new QLineEdit(mPathsBox); + PathLineEdit->setProperty("ControlID",QVariant(CtlIdx)); + PathLineEdit->setText(mBlenderPaths[CtlIdx].value); + PathLineEdit->setToolTip(mBlenderPaths[CtlIdx].tooltip); + if (mPathLineEditList.size() > CtlIdx) + mPathLineEditList.replace(CtlIdx, PathLineEdit); + else + mPathLineEditList << PathLineEdit; + mPathsGridLayout->addWidget(PathLineEdit,ColumnIdx,1); + PathLineEdit->setVisible(IsVisible); + + QPushButton* PathBrowseButton = new QPushButton(tr("Browse..."), mPathsBox); + if (mPathBrowseButtonList.size() > CtlIdx) + mPathBrowseButtonList.replace(CtlIdx, PathBrowseButton); + else + mPathBrowseButtonList << PathBrowseButton; + mPathsGridLayout->addWidget(PathBrowseButton,ColumnIdx,2); + PathBrowseButton->setVisible(IsVisible); + + if (IsVisible) + { + connect(PathBrowseButton, SIGNAL(clicked(bool)), this, SLOT (BrowseBlender(bool))); + connect(PathLineEdit, SIGNAL(editingFinished()), this, SLOT (PathChanged())); + } + } + + mPathsBox->setEnabled(mConfigured); + + // Settings + ClearGroupBox(mSettingsBox); + mSettingsBox = new QGroupBox(mContent); + mSettingsSubform = new QFormLayout(mSettingsBox); + mSettingsBox->setLayout(mSettingsSubform); + mForm->addRow(mSettingsBox); + + mSettingsBox->setTitle(tr("LDraw Import MM Addon Settings")); + mSettingLabelList.clear(); + mCheckBoxList.clear(); + mLineEditList.clear(); + mComboBoxList.clear(); + + int ComboBoxItemsIndex = 0; + + for (int LblIdx = 0; LblIdx < NumSettingsMM(); LblIdx++) + { + QLabel* Label = new QLabel(mSettingsBox); + Label->setText(mBlenderSettingsMM[LblIdx].label); + Label->setToolTip(mBlenderSettingsMM[LblIdx].tooltip); + mSettingLabelList << Label; + + if (LblIdx < LBL_BEVEL_SEGMENTS) + { // QCheckBoxes + QCheckBox* CheckBox = new QCheckBox(mSettingsBox); + CheckBox->setProperty("ControlID",QVariant(LblIdx)); + CheckBox->setChecked(mBlenderSettingsMM[LblIdx].value.toInt()); + CheckBox->setToolTip(mBlenderSettingsMM[LblIdx].tooltip); + if (LblIdx == LBL_CROP_IMAGE_MM) + connect(CheckBox, SIGNAL(toggled(bool)), this, SLOT (SetModelSize(bool))); + else + connect(CheckBox, SIGNAL(clicked()), this, SLOT (SettingChanged())); + mCheckBoxList << CheckBox; + mSettingsSubform->addRow(Label,CheckBox); + } + else if (LblIdx < LBL_CHOSEN_LOGO) + { // QLineEdits + QLineEdit* LineEdit = new QLineEdit(mSettingsBox); + LineEdit->setProperty("ControlID",QVariant(LblIdx)); + if (LblIdx == LBL_RESOLUTION_WIDTH || LblIdx == LBL_RESOLUTION_HEIGHT) + { + connect(LineEdit, SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + LineEdit->setValidator(new QIntValidator(16, LC_RENDER_IMAGE_MAX_SIZE)); + } + else + { + LineEdit->setText(mBlenderSettingsMM[LblIdx].value); + if (LblIdx == LBL_IMPORT_SCALE) + LineEdit->setValidator(new QDoubleValidator(0.01,10.0,2)); + else if (LblIdx == LBL_RENDER_PERCENTAGE_MM || LblIdx == LBL_CAMERA_BORDER_PERCENT_MM) + LineEdit->setValidator(new QIntValidator(1,1000)); + else if (LblIdx == LBL_GAP_SCALE || LblIdx == LBL_MERGE_DISTANCE) + LineEdit->setValidator(new QDoubleValidator(0.001,100.0,3)); + else if (LblIdx == LBL_BEVEL_WEIGHT || LblIdx == LBL_BEVEL_WIDTH_MM) + LineEdit->setValidator(new QDoubleValidator(0.0,10.0,1)); + else + LineEdit->setValidator(new QIntValidator(1, LC_RENDER_IMAGE_MAX_SIZE)); + connect(LineEdit, SIGNAL(textEdited(const QString&)), this, SLOT (SettingChanged(const QString&))); + } + LineEdit->setToolTip(mBlenderSettingsMM[LblIdx].tooltip); + mLineEditList << LineEdit; + mSettingsSubform->addRow(Label,LineEdit); + } + else + { // QComboBoxes + QComboBox* ComboBox = new QComboBox(mSettingsBox); + ComboBox->setProperty("ControlID",QVariant(LblIdx)); + QString const Value = mBlenderSettingsMM[LblIdx].value; + QStringList const DataList = mComboItemsMM[ComboBoxItemsIndex].dataList.split("|"); + QStringList const ItemList = mComboItemsMM[ComboBoxItemsIndex].itemList.split("|"); + ComboBox->addItems(ItemList); + for (int CtlIdx = 0; CtlIdx < ComboBox->count(); CtlIdx++) + ComboBox->setItemData(CtlIdx, DataList.at(CtlIdx)); + ComboBox->setToolTip(mBlenderSettingsMM[LblIdx].tooltip); + int CurrentIndex = int(ComboBox->findData(QVariant::fromValue(Value))); + ComboBox->setCurrentIndex(CurrentIndex); + if (LblIdx == LBL_COLOUR_SCHEME_MM) + connect(ComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT (ValidateColourScheme(int))); + else + connect(ComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT (SettingChanged(int))); + mComboBoxList << ComboBox; + ComboBoxItemsIndex++; + mSettingsSubform->addRow(Label,ComboBox); + } + } + + SetModelSize(); + + if (!mSettingsSubform->rowCount()) + mSettingsSubform = nullptr; + + mSettingsBox->setEnabled(mConfigured); +} + +void lcBlenderPreferences::UpdateBlenderAddon() +{ + mAddonUpdateButton->setEnabled(false); + + disconnect(mPathLineEditList[PATH_BLENDER], SIGNAL(editingFinished()), this, SLOT (ConfigureBlenderAddon())); + + ConfigureBlenderAddon(sender() == mPathBrowseButtonList[PATH_BLENDER], + sender() == mAddonUpdateButton); + + connect(mPathLineEditList[PATH_BLENDER], SIGNAL(editingFinished()), this, SLOT (ConfigureBlenderAddon())); +} + +void lcBlenderPreferences::ConfigureBlenderAddon(bool TestBlender, bool AddonUpdate, bool ModuleChange) +{ + mProgressBar = nullptr; + + QString const BlenderExe = QDir::toNativeSeparators(mPathLineEditList[PATH_BLENDER]->text()); + + if (BlenderExe.isEmpty()) + { + mBlenderVersion.clear(); + mConfigured = false; + StatusUpdate(false, true); + mAddonUpdateButton->setEnabled(mConfigured); + mPathsBox->setEnabled(mConfigured); + mSettingsBox->setEnabled(mConfigured); + return; + } + + if (QFileInfo(BlenderExe).isReadable()) + { + enum ProcEnc + { PR_OK, PR_FAIL, PR_WAIT, PR_INSTALL, PR_TEST }; + QString const BlenderDir = QDir::toNativeSeparators(QString("%1/Blender").arg(mDataDir)); + QString const BlenderConfigDir = QString("%1/setup/addon_setup/config").arg(BlenderDir); + QString const BlenderAddonDir = QDir::toNativeSeparators(QString("%1/addons").arg(BlenderDir)); + QString const BlenderExeCompare = QDir::toNativeSeparators(lcGetProfileString(LC_PROFILE_BLENDER_PATH)).toLower(); + QString const BlenderInstallFile = QDir::toNativeSeparators(QString("%1/%2").arg(BlenderDir).arg(LC_BLENDER_ADDON_INSTALL_FILE)); + QString const BlenderTestString = QLatin1String("###TEST_BLENDER###"); + QByteArray AddonPathsAndModuleNames; + QString Message, ShellProgram; + QStringList Arguments; + ProcEnc Result = PR_OK; + QFile Script; + + bool NewBlenderExe = BlenderExeCompare != BlenderExe.toLower(); + + if (mConfigured && !AddonUpdate && !ModuleChange && !NewBlenderExe) + return; + + auto ProcessCommand = [&](ProcEnc Action) + { + mProcess = new QProcess(); + + QString ProcessAction = tr("addon install"); + if (Action == PR_INSTALL) + { + connect(mProcess, SIGNAL(readyReadStandardOutput()), this, SLOT(ReadStdOut())); + QString const& LdrawLibPath = QFileInfo(lcGetProfileString(LC_PROFILE_PARTS_LIBRARY)).absolutePath(); + QStringList SystemEnvironment = QProcess::systemEnvironment(); + SystemEnvironment.prepend("LDRAW_DIRECTORY=" + LdrawLibPath); + SystemEnvironment.prepend("ADDONS_TO_LOAD=" + AddonPathsAndModuleNames); + mProcess->setEnvironment(SystemEnvironment); + } + else + { + ProcessAction = tr("test"); + disconnect(&mUpdateTimer, SIGNAL(timeout()), this, SLOT(Update())); + } + + mProcess->setWorkingDirectory(BlenderDir); + + mProcess->setStandardErrorFile(QString("%1/stderr-blender-addon-install").arg(BlenderDir)); + + if (Action == PR_INSTALL) + { + mProcess->start(BlenderExe, Arguments); + } + else + { +#ifdef Q_OS_WIN + mProcess->start(ShellProgram, QStringList() << "/C" << Script.fileName()); +#else + mProcess->start(ShellProgram, QStringList() << Script.fileName()); +#endif + } + + if (!mProcess->waitForStarted()) + { + Message = tr("Cannot start Blender %1 mProcess.\n%2") + .arg(ProcessAction) + .arg(QString(mProcess->readAllStandardError())); + delete mProcess; + mProcess = nullptr; + return PR_WAIT; + } + else + { + if (mProcess->exitStatus() != QProcess::NormalExit || mProcess->exitCode() != 0) + { + Message = tr("Failed to execute Blender %1.\n%2") + .arg(ProcessAction) + .arg(QString(mProcess->readAllStandardError())); + return PR_FAIL; + } + } + + if (Action == PR_TEST) + { + while (mProcess && mProcess->state() != QProcess::NotRunning) + { + QTime Waiting = QTime::currentTime().addMSecs(500); + while (QTime::currentTime() < Waiting) + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + connect(&mUpdateTimer, SIGNAL(timeout()), this, SLOT(Update())); + QString const StdOut = QString(mProcess->readAllStandardOutput()); + if (!StdOut.contains(BlenderTestString)) + { + Message = tr("A simple check to test if the selected file is Blender failed." + "Please create an %1 GitHub ticket if you are sure the file is Blender 2.8 or newer." + "The ticket should contain the full path to the Blender executable.").arg(LC_PRODUCTNAME_STR); + return PR_FAIL; + } + else + { + QStringList Items = StdOut.split('\n',SkipEmptyParts).last().split(" "); + if (Items.count() > 6 && Items.at(0) == QLatin1String("Blender")) + { + Items.takeLast(); + mBlenderVersion.clear(); + for (int LblIdx = 1; LblIdx < Items.size(); LblIdx++) + mBlenderVersion.append(Items.at(LblIdx)+" "); + mBlenderVersionFound = !mBlenderVersion.isEmpty(); + if (mBlenderVersionFound) + { + mBlenderVersion = mBlenderVersion.trimmed().prepend("v").append(")"); + mBlenderVersionEdit->setText(mBlenderVersion); + // set default LDraw import module if not configured + if (!mImportActBox->isChecked() && !mImportMMActBox->isChecked()) + mImportActBox->setChecked(true); + Message = tr("Blender %1 mProcess completed. Version: %2 validated.").arg(ProcessAction).arg(mBlenderVersion); + } + } + } + } + + return PR_OK; + }; + + connect(&mUpdateTimer, SIGNAL(timeout()), this, SLOT(Update())); + mUpdateTimer.start(500); + + if (!ModuleChange) + { + mProgressBar = new QProgressBar(mContent); + mProgressBar->setMaximum(0); + mProgressBar->setMinimum(0); + mProgressBar->setValue(1); + + TestBlender |= sender() == mPathLineEditList[PATH_BLENDER]; + if (TestBlender) + { + mExeGridLayout->replaceWidget(mBlenderVersionEdit, mProgressBar); + mProgressBar->show(); + + Arguments << QString("--factory-startup"); + Arguments << QString("-b"); + Arguments << QString("--python-expr"); + Arguments << QString("\"import sys;print('%1');sys.stdout.flush();sys.exit()\"").arg(BlenderTestString); + + bool Error = false; + + QString ScriptName, ScriptCommand; + +#ifdef Q_OS_WIN + ScriptName = QLatin1String("blender_test.bat"); +#else + ScriptName = QLatin1String("blender_test.sh"); +#endif + ScriptCommand = QString("%1 %2").arg(BlenderExe).arg(Arguments.join(" ")); + + Script.setFileName(QString("%1/%2").arg(QDir::tempPath()).arg(ScriptName)); + if(Script.open(QIODevice::WriteOnly | QIODevice::Text)) + { + QTextStream Stream(&Script); +#ifdef Q_OS_WIN + Stream << QLatin1String("@ECHO OFF& SETLOCAL") << LineEnding; +#else + Stream << QLatin1String("#!/bin/bash") << LineEnding; +#endif + Stream << ScriptCommand << LineEnding; + Script.close(); + } + else + { + Message = tr("Cannot write Blender render script file [%1] %2.") + .arg(Script.fileName()) + .arg(Script.errorString()); + Error = true; + } + + if (Error) + { + StatusUpdate(false); + return; + } + + QThread::sleep(1); + +#ifdef Q_OS_WIN + ShellProgram = QLatin1String(LC_WINDOWS_SHELL); +#else + ShellProgram = QLatin1String(LC_UNIX_SHELL); +#endif + Result = ProcessCommand(PR_TEST); + bool TestOk = Result != PR_FAIL; + QString const statusLabel = TestOk ? "" : tr("Blender test failed."); + StatusUpdate(false, TestOk, statusLabel); + if (TestOk) + { + lcSetProfileString(LC_PROFILE_BLENDER_VERSION, mBlenderVersion); + lcSetProfileString(LC_PROFILE_BLENDER_PATH, BlenderExe); + } + else + { + QString const& Title = tr ("%1 Blender LDraw Addon").arg(LC_PRODUCTNAME_STR); + QString const& Header = tr ("Blender test failed."); + ShowMessage(Header, Title, Message); + return; + } + } // Test Blender + + if (!mBlenderVersion.isEmpty() && !mImportMMActBox->isChecked() && !mImportActBox->isChecked()) + { + QString const& Title = tr ("%1 Blender LDraw Addon Modules").arg(LC_PRODUCTNAME_STR); + QString const& Header = tr ("No import module enabled." + "If you continue, the default import module (Import TN) will be used.
" + "If you select No, all addon modules will be disabled."); + QString const& Body = tr ("Continue with the default import module ?"); + int Exec = ShowMessage(Header, Title, Body, QString(), MBB_YES_NO, QMessageBox::NoIcon); + if (Exec != QMessageBox::Yes) + { + mRenderActBox->setChecked(false); + if (Exec == QMessageBox::Cancel) + return; + } + + mImportActBox->setChecked(true); + } + + if (TestBlender) + { + QString const preferredImportModule = + mImportActBox->isChecked() + ? QString("TN") + : mImportMMActBox->isChecked() + ? QString("MM") + : QString(); // disable all import modules + lcSetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE, preferredImportModule); + } + else + mBlenderVersionEdit->setVisible(AddonUpdate); + + if (mProgressBar) + { + mProgressBar->setMaximum(0); + mProgressBar->setMinimum(0); + mProgressBar->setValue(1); + mAddonGridLayout->replaceWidget(mAddonVersionEdit, mProgressBar); + mProgressBar->show(); + } + + if (!ExtractBlenderAddon(BlenderDir)) + { + if (AddonUpdate) + { + mConfigured = true; + mBlenderVersionLabel->setText(tr("Blender")); + mBlenderVersionLabel->setStyleSheet(QString("QLabel { color : %1; }").arg(QApplication::palette().text().color().name())); + mBlenderVersionEdit->setText(mBlenderVersion); + mBlenderVersionEdit->setToolTip(tr("Display the Blender and %1 Render addon version").arg(LC_PRODUCTNAME_STR)); + mBlenderVersionEdit->setVisible(mConfigured); + if (!mAddonVersion.isEmpty()) + { + mModulesBox->setEnabled(true); + mAddonVersionEdit->setText(mAddonVersion); + } + mAddonUpdateButton->setEnabled(mConfigured); + if (mProgressBar) + { + mExeGridLayout->replaceWidget(mProgressBar, mAddonVersionEdit); + mProgressBar->close(); + } + } + return; + } + + if (!QFileInfo(BlenderInstallFile).exists()) + { + ShowMessage(tr ("Could not find addon install file: %1").arg(BlenderInstallFile)); + StatusUpdate(true, true, tr("Addon file not found.")); + return; + } + + StatusUpdate(true, false, tr("Install addon...")); + + QDir ConfigDir(BlenderConfigDir); + if(!QDir(ConfigDir).exists()) + ConfigDir.mkpath("."); + } + + SaveSettings(); + + Arguments.clear(); + Arguments << QString("--background"); + Arguments << QString("--python"); + Arguments << BlenderInstallFile; + Arguments << "--"; + if (!mRenderActBox->isChecked() && !mImportActBox->isChecked() && !mImportMMActBox->isChecked()) + Arguments << QString("--disable_ldraw_addons"); + else if (!mImportActBox->isChecked()) + Arguments << QString("--disable_ldraw_import"); + else if (!mImportMMActBox->isChecked()) + Arguments << QString("--disable_ldraw_import_mm"); + else if (!mRenderActBox->isChecked()) + Arguments << QString("--disable_ldraw_render"); + Arguments << QString("--leocad"); + + Message = tr("Blender Addon Install Arguments: %1 %2").arg(BlenderExe).arg(Arguments.join(" ")); + + if (!TestBlender) + mBlenderVersionFound = false; + + if (QDir(BlenderAddonDir).entryInfoList(QDir::Dirs|QDir::NoSymLinks).count() > 0) + { + QJsonArray JsonArray; + QStringList AddonDirs = QDir(BlenderAddonDir).entryList(QDir::NoDotAndDotDot | QDir::Dirs, QDir::SortByMask); + for (QString const& Addon : AddonDirs) + { + QDir Dir(QString("%1/%2").arg(BlenderAddonDir).arg(Addon)); + Dir.setFilter(QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks); + QFileInfoList List = Dir.entryInfoList(); + for (int LblIdx = 0; LblIdx < List.size(); LblIdx++) + { + if (List.at(LblIdx).fileName() == QLatin1String("__init__.py")) + { + QFile File(QFileInfo(List.at(LblIdx)).absoluteFilePath()); + if (!File.open(QFile::ReadOnly | QFile::Text)) + { + ShowMessage(tr("Cannot read addon file %1
%2").arg(List.at(LblIdx).fileName()).arg(File.errorString())); + break; + } + else + { + bool FoundModule = false; + QTextStream In(&File); + while ( ! In.atEnd()) + { + if (QString(In.readLine(0)).startsWith("bl_info")) + { + FoundModule = true; + break; + } + } + File.close(); + if (FoundModule) + { + QJsonObject JsonItemObj; + JsonItemObj["load_dir"] = QDir::toNativeSeparators(Dir.absolutePath()); + JsonItemObj["module_name"] = Dir.dirName(); + JsonArray.append(JsonItemObj); + } + } + } + } + } + QJsonDocument JsonDoc; + JsonDoc.setArray(JsonArray); + AddonPathsAndModuleNames = JsonDoc.toJson(QJsonDocument::Compact); + } + + Result = ProcessCommand(PR_INSTALL); + + if (Result != PR_OK) + StatusUpdate(true, true, tr("Addon install failed.")); + } + else + ShowMessage(tr("Blender executable not found at [%1]").arg(BlenderExe), tr("Addon install failed.")); +} + +bool lcBlenderPreferences::ExtractBlenderAddon(const QString& BlenderDir) +{ + bool Proceed = true; + + QDir Dir(BlenderDir); + if (!Dir.exists()) + Dir.mkdir(BlenderDir); + + if (GetBlenderAddon(BlenderDir)) + { + gAddonPreferences->StatusUpdate(true, false, tr("Extract addon...")); + QString const BlenderAddonFile = QDir::toNativeSeparators(QString("%1/%2").arg(BlenderDir).arg(LC_BLENDER_ADDON_FILE)); + // TODO - Replace QuaZip call to unzip addon + /* QStringList addonList = JlCompress::extractDir(blenderAddonFile, blenderDir); + if (addonList.isEmpty()) + { + showMessage(tr("Failed to extract %1 to %2") + .arg(blenderAddonFile).arg(blenderDir), tr("Extract addon")); + proceed = false; + } + */ + } + + if (!Proceed) + gAddonPreferences->StatusUpdate(true, true,tr("Extract addon failed.")); + + return Proceed; +} + +bool lcBlenderPreferences::GetBlenderAddon(const QString& BlenderDir) +{ + enum AddonEnc + { + ADDON_FAIL = -1, + ADDON_NOT_FOUND, + ADDON_DOWNLOAD = ADDON_NOT_FOUND, + ADDON_ARCHIVE, + ADDON_EXTRACTED, + ADDON_RELOAD, + ADDON_CANCEL + }; + + QString const BlenderAddonDir = QDir::toNativeSeparators(QString("%1/addons").arg(BlenderDir)); + QString const BlenderAddonFile = QDir::toNativeSeparators(QString("%1/%2").arg(BlenderDir).arg(LC_BLENDER_ADDON_FILE)); + QString const AddonVersionFile = QDir::toNativeSeparators(QString("%1/%2/__version__.py").arg(BlenderAddonDir).arg(LC_BLENDER_ADDON_FOLDER_STR)); + bool ExtractedAddon = QFileInfo(AddonVersionFile).isReadable(); + bool BlenderAddonExists = ExtractedAddon || QFileInfo(BlenderAddonFile).isReadable(); + QString AddonStatus = tr("Installing Blender addon..."); + AddonEnc AddonAction = ADDON_DOWNLOAD; + QString LocalVersion, OnlineVersion; + + using namespace std; + auto VersionStringCompare = [](string V1, string V2) + { // Returns 1 if V2 is smaller, -1 if V1 is smaller, 0 if equal + int Vnum1 = 0, Vnum2 = 0; + for (quint32 i = 0, j = 0; (i < V1.length() || j < V2.length());) + { + while (i < V1.length() && V1[i] != '.') + { + Vnum1 = Vnum1 * 10 + (V1[i] - '0'); + i++; + } + while (j < V2.length() && V2[j] != '.') + { + Vnum2 = Vnum2 * 10 + (V2[j] - '0'); + j++; + } + if (Vnum1 > Vnum2) + return 1; + if (Vnum2 > Vnum1) + return -1; + Vnum1 = Vnum2 = 0; + i++; + j++; + } + + return 0; + }; + + auto GetBlenderAddonVersionMatch = [&]() + { + QByteArray Ba; + if (!ExtractedAddon) + { + // TODO - Replace QuaZip archive file check + /* + QuaZip Zip(BlenderAddonFile); + if (!Zip.open(QuaZip::mdUnzip)) + { + QString const Result = tr("Could not open archive to check content. Return code %1.
" + "Archive file %2 may be open in another program.") + .arg(Zip.getZipError()).arg(QFileInfo(BlenderAddonFile).fileName()); + emit gui->messageSig(LOG_WARNING, Result); + return false; // Download new archive + } + + QString const AddonVersionFile = QString("addons/%1/__version__.py").arg(LC_BLENDER_ADDON_FOLDER_STR); + Zip.setCurrentFile(AddonVersionFile); + QuaZipFile File(&Zip); + if (!File.open(QIODevice::ReadOnly)) + { + ShowMessage(tr("Cannot read addon archive version file: [%1]
%2.") + .arg(AddonVersionFile).arg(File.errorString())); + return false; // Download new archive + } + Ba = File.readAll(); + File.close(); + Zip.close(); + if (Zip.getZipError() != UNZ_OK) + showMessage(tr("Archive close error. Return code %1.").arg(Zip.getZipError())); + */ + ShowMessage(tr("Check addon archive is not supported. Manually extract addon and continue.")); + } + else + { + QFile File(AddonVersionFile); + if (!File.open(QIODevice::ReadOnly)) + { + ShowMessage(tr("Cannot read addon version file: [%1]
%2.") + .arg(AddonVersionFile).arg(File.errorString())); + return false; // Download new archive + } + Ba = File.readAll(); + File.close(); + } + + QTextStream Content(Ba.data()); + while (!Content.atEnd()) + { + QString Token; + Content >> Token; + if (Token == QLatin1String("version")) + { + Content >> Token; + LocalVersion = Content.readAll().trimmed().replace("(","v").replace(",",".").replace(" ","").replace(")",""); + } + } + + // TODO - Refactor, use LeoCAD mHttpReply/HttpManager + DownloadFile(LC_BLENDER_ADDON_LATEST_URL, tr("Latest Addon"),false/*promptRedirect*/,false/*showProgress*/); + QByteArray const &Response_Data = GetDownloadedFile(); + if (!Response_Data.isEmpty()) + { + QJsonDocument Json = QJsonDocument::fromJson(Response_Data); + OnlineVersion = Json.object()["tag_name"].toString(); + } + else + ShowMessage(tr("Check latest addon version failed."), tr("Latest Addon"), QString(), QString(), MBB_OK, QMessageBox::Warning); + + if (!LocalVersion.isEmpty() && !OnlineVersion.isEmpty()) + { + // LocalVersion is smaller than OnlineVersion so prompt to download new archive + if (VersionStringCompare(LocalVersion.toStdString(), OnlineVersion.toStdString()) < 0) + return false; + } + + return true; // Reload existing archive + }; + + if (BlenderAddonExists) + { + if (GetBlenderAddonVersionMatch()) + { + AddonAction = ADDON_RELOAD; + } + else if (gMainWindow) + { + if (LocalVersion.isEmpty()) + LocalVersion = gAddonPreferences->mAddonVersion; + QString const& Title = tr ("%1 Blender LDraw Addon").arg(LC_PRODUCTNAME_STR); + QString const& Header = tr ("Detected %1 Blender LDraw addon %2. A newer version %3 exists.") + .arg(LC_PRODUCTNAME_STR) + .arg(LocalVersion).arg(OnlineVersion); + QString const& Body = tr ("Do you want to download version %1 ?").arg(OnlineVersion); + int Exec = ShowMessage(Header, Title, Body, QString(), MBB_YES, QMessageBox::NoIcon); + if (Exec == QMessageBox::Cancel) + { + AddonStatus = tr("Blender addon setup cancelled"); + AddonAction = ADDON_CANCEL; + } + else if (Exec == QMessageBox::No) + { + AddonAction = ADDON_RELOAD; + } + } + + if (AddonAction == ADDON_DOWNLOAD) + AddonStatus = tr("Download addon..."); + + gAddonPreferences->StatusUpdate(true, false, AddonStatus); + + if (AddonAction == ADDON_CANCEL) + { + gAddonPreferences->mDialogCancelled = true; + return false; + } + } + + if (QFileInfo(BlenderAddonDir).exists()) + { + bool Result = true; + QDir Dir(BlenderAddonDir); + for (QFileInfo const& FileInfo : Dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks, QDir::DirsFirst)) + { + if (FileInfo.isDir()) + Result &= QDir(FileInfo.absoluteFilePath()).removeRecursively(); + else + Result &= QFile::remove(FileInfo.absoluteFilePath()); + } + + Result &= Dir.rmdir(BlenderAddonDir); + if (!Result) + ShowMessage(tr("Failed to remove Blender addon: %1").arg(BlenderAddonDir), + tr("Remove Existing Addon"), QString(), QString(), MBB_OK, QMessageBox::Warning); + } + + if (AddonAction == ADDON_DOWNLOAD) + { + BlenderAddonExists = false; + // TODO - Refactor, use LeoCAD mHttpReply/HttpManager + DownloadFile(LC_BLENDER_ADDON_URL, tr("Blender Addon"),false/*promptRedirect*/,false/*showProgress*/); + QByteArray const &Buffer = GetDownloadedFile(); + if (!Buffer.isEmpty()) + { + if (QFileInfo(BlenderAddonFile).exists()) + { + QDir Dir(BlenderDir); + if (!Dir.remove(BlenderAddonFile)) + ShowMessage(tr("Failed to remove Blender addon archive:
%1").arg(BlenderAddonFile)); + } + QFile File(BlenderAddonFile); + if (File.open(QIODevice::WriteOnly)) + { + File.write(Buffer); + File.close(); + BlenderAddonExists = true; + } + else + ShowMessage(tr("Failed to open Blender addon file:
%1:
%2") + .arg(BlenderAddonFile).arg(File.errorString())); + } + else + ShowMessage(tr("Failed to download Blender addon archive:
%1").arg(BlenderAddonFile)); + + if (!BlenderAddonExists) + { + AddonStatus = tr("Download addon failed."); + gAddonPreferences->StatusUpdate(true, true, AddonStatus); + } + } + else if (!BlenderAddonExists) + ShowMessage(tr("Blender addon archive %1 was not found").arg(BlenderAddonFile)); + + return BlenderAddonExists; +} + +void lcBlenderPreferences::StatusUpdate(bool Addon, bool Error, const QString& Message) +{ + QString Label, Colour; + QString const Which = Addon ? tr("Blender addon") : tr("Blender"); + if (mProgressBar) + { + if (Addon) + { + mAddonGridLayout->replaceWidget(mProgressBar, mAddonVersionEdit); + } + else + { + mExeGridLayout->replaceWidget(mProgressBar, mBlenderVersionEdit); + mProgressBar->hide(); + } + } + if (Error) + { + if (!Addon) + mPathLineEditList[PATH_BLENDER]->text() = QString(); + + lcSetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE, QString()); + + const lcPreferences& Preferences = lcGetPreferences(); + Label = ! Message.isEmpty() ? Message : tr("%1 not configured").arg(Which); + Colour = Message.startsWith("Error:", Qt::CaseInsensitive) + ? QLatin1String("red") + : Preferences.mColorTheme == lcColorTheme::Dark + ? QLatin1String(LC_THEME_DARK_DECORATE_QUOTED_TEXT) + : QLatin1String("blue"); + mDialogCancelled = true; + } + else + { + Label = !Message.isEmpty() ? Message : tr("%1 setup...").arg(Which); + Colour = QApplication::palette().text().color().name(); + } + + if (Addon) + { + mAddonVersionLabel->setStyleSheet(QString("QLabel { color : %1; }").arg(Colour)); + mAddonVersionLabel->setText(Label); + mAddonVersionEdit->setVisible(!mAddonVersion.isEmpty()); + } + else + { + mBlenderVersionLabel->setStyleSheet(QString("QLabel { color : %1; }").arg(Colour)); + mBlenderVersionLabel->setText(Label); + mBlenderVersionEdit->setVisible(!mBlenderVersion.isEmpty()); + } +} + +void lcBlenderPreferences::ShowResult() +{ + QString Message; + bool Error; + const QString StdErrLog = ReadStdErr(Error); + + if (mProgressBar) + mProgressBar->close(); + + WriteStdOut(); + + if (mProcess->exitStatus() != QProcess::NormalExit || mProcess->exitCode() != 0 || Error) + { + QString const BlenderDir = QString("%1/Blender").arg(mDataDir); + Message = tr("Addon install failed. See %1/stderr-blender-addon-install for details.").arg(BlenderDir); + StatusUpdate(true, true,tr("%1: Addon install failed.").arg("Error")); + mConfigured = false; + + QString const& Title = tr ("%1 Blender Addon Install").arg(LC_PRODUCTNAME_STR); + QString const& Header = "" + tr ("Addon install failed.") + ""; + QString const& Body = tr ("LDraw addon install encountered one or more errors. See Show Details..."); + ShowMessage(Header, Title, Body, StdErrLog, MBB_OK, QMessageBox::Critical); + } + else + { + QString const TextColour = QString("QLabel { color : %1; }").arg(QApplication::palette().text().color().name()); + mAddonGridLayout->replaceWidget(mProgressBar, mAddonVersionEdit); + mConfigured = true; + mBlenderVersionLabel->setText(tr("Blender")); + mBlenderVersionLabel->setStyleSheet(TextColour); + mBlenderVersionEdit->setText(mBlenderVersion); + mBlenderVersionEdit->setToolTip(tr("Display the Blender and %1 Render addon version").arg(LC_PRODUCTNAME_STR)); + mBlenderVersionEdit->setVisible(mConfigured); + mPathsBox->setEnabled(mConfigured); + mSettingsBox->setEnabled(mConfigured); + + if (!mAddonVersion.isEmpty()) + { + mAddonVersionLabel->setText(tr("Blender Addon")); + mAddonVersionLabel->setStyleSheet(TextColour); + mAddonVersionEdit->setText(mAddonVersion); + mAddonVersionEdit->setVisible(true); + mModulesBox->setEnabled(true); + mAddonUpdateButton->setEnabled(true); + lcSetProfileString(LC_PROFILE_BLENDER_VERSION, mBlenderVersion); + lcSetProfileString(LC_PROFILE_BLENDER_ADDON_VERSION, mAddonVersion); + SetModelSize(true); + SaveSettings(); + mDialogCancelled = false; + } + Message = tr("Blender version %1").arg(mBlenderVersion); + } + + delete mProcess; + mProcess = nullptr; + + if (Error) + ShowMessage(Message); +} + +void lcBlenderPreferences::SettingChanged(const QString& Value) +{ + bool Change = false; + + QLineEdit* LineEdit = qobject_cast(sender()); + if (LineEdit) + { + int LblIdx = LineEdit->property("ControlID").toInt(); + if (mImportMMActBox->isChecked()) + { + Change = mBlenderSettingsMM[LblIdx].value != Value; + } + else + { + Change = mBlenderSettings[LblIdx].value != Value; + } + + Change |= SettingsModified(false); + emit SettingChangedSig(Change); + } +} + +void lcBlenderPreferences::SettingChanged(int Index) +{ + int LblIdx = -1; + bool Change = false; + QString Item; + + if (Index > -1) + { + QComboBox* ComboBox = qobject_cast(sender()); + if (ComboBox) + { + LblIdx = ComboBox->property("ControlID").toInt(); + Item = ComboBox->itemData(Index).toString(); + } + } + else + { + QCheckBox* CheckBox = qobject_cast(sender()); + if (CheckBox) + { + LblIdx = CheckBox->property("ControlID").toInt(); + Item = QString::number(CheckBox->isChecked()); + } + } + + if (LblIdx > -1) + { + if (mImportMMActBox->isChecked()) + { + Change = mBlenderSettingsMM[LblIdx].value != Item; + } + else + { + Change = mBlenderSettings[LblIdx].value != Item; + } + } + + Change |= SettingsModified(false); + emit SettingChangedSig(Change); +} + +void lcBlenderPreferences::PathChanged() +{ + QLineEdit* LineEdit = qobject_cast(sender()); + if (LineEdit) + { + bool Change = false; + const int LblIdx = LineEdit->property("ControlID").toInt(); + const QString& Path = QDir::toNativeSeparators(LineEdit->text()).toLower(); + + if (LblIdx != PATH_BLENDER) + { + Change = QDir::toNativeSeparators(mBlenderPaths[LblIdx].value).toLower() != Path; + } + + Change |= SettingsModified(false); + emit SettingChangedSig(Change); + } +} + +void lcBlenderPreferences::GetStandardOutput() +{ + QString const LogFile = QString("%1/Blender/stdout-blender-addon-install").arg(mDataDir); + QFileInfo FileInfo(LogFile); + if (!FileInfo.exists()) + { + ShowMessage(tr("Blender addon standard output file not found: %1.").arg(FileInfo.absoluteFilePath())); + return; + } + + if (LogFile.isEmpty()) + return; + + QDesktopServices::openUrl(QUrl("file:///"+LogFile, QUrl::TolerantMode)); +} + +void lcBlenderPreferences::ReadStdOut(const QString& StdOutput, QString& Errors) +{ + QRegExp RxInfo("^INFO: "); + QRegExp RxData("^DATA: "); + QRegExp RxError("^(?:\\w)*ERROR: ", Qt::CaseInsensitive); + QRegExp RxWarning("^(?:\\w)*WARNING: ", Qt::CaseInsensitive); + QRegExp RxAddonVersion("^ADDON VERSION: ", Qt::CaseInsensitive); + + bool ErrorEncountered = false; + QStringList Items, ErrorList; + QStringList StdOutLines = StdOutput.split(QRegExp("\n|\r\n|\r")); + + QString const SaveAddonVersion = mAddonVersion; + QString const SaveVersion = mBlenderVersion; + + int EditListItems = mPathLineEditList.size(); + for (QString const& StdOutLine : StdOutLines) + { + if (StdOutLine.isEmpty()) + continue; + + if (!mBlenderVersionFound) + { + Items = StdOutLine.split(" "); + if (Items.count() > 6 && Items.at(0) == QLatin1String("Blender")) + { + Items.takeLast(); + mBlenderVersion.clear(); + for (int LblIdx = 1; LblIdx < Items.size(); LblIdx++) + mBlenderVersion.append(Items.at(LblIdx)+" "); + mBlenderVersionFound = !mBlenderVersion.isEmpty(); + if (mBlenderVersionFound) + { + mBlenderVersion = mBlenderVersion.trimmed().prepend("v").append(")"); + mBlenderVersionEdit->setText(mBlenderVersion); + if (!mImportActBox->isChecked() && !mImportMMActBox->isChecked()) + mImportActBox->setChecked(true); + } + } + } + + if (StdOutLine.contains(RxInfo)) + { + Items = StdOutLine.split(": "); + } + else if (StdOutLine.contains(RxData)) + { + Items = StdOutLine.split(": "); + if (Items.at(1) == "ENVIRONMENT_FILE") + { + mBlenderPaths[PATH_ENVIRONMENT].value = Items.at(2); + if (EditListItems > PATH_ENVIRONMENT) + mPathLineEditList[PATH_ENVIRONMENT]->setText(Items.at(2)); + } + else if (Items.at(1) == "LSYNTH_DIRECTORY") + { + mBlenderPaths[PATH_LSYNTH].value = Items.at(2); + if (EditListItems > PATH_LSYNTH) + mPathLineEditList[PATH_LSYNTH]->setText(Items.at(2)); + } + else if (Items.at(1) == "STUDLOGO_DIRECTORY") + { + mBlenderPaths[PATH_STUD_LOGO].value = Items.at(2); + if (EditListItems > PATH_STUD_LOGO) + mPathLineEditList[PATH_STUD_LOGO]->setText(Items.at(2)); + } + } + else if (StdOutLine.contains(RxError) || StdOutLine.contains(RxWarning)) + { + ErrorList << StdOutLine.trimmed() + "
"; + if (!ErrorEncountered) + ErrorEncountered = StdOutLine.contains(RxError); + } + else if (StdOutLine.contains(RxAddonVersion)) + { + Items = StdOutLine.split(":"); + mAddonVersion = tr("v%1").arg(Items.at(1).trimmed()); + mAddonVersionEdit->setText(mAddonVersion); + } + } + if (ErrorList.size()) + { + if (!mBlenderVersionFound) + { + if (mBlenderVersion != SaveVersion) + { + mConfigured = false; + mBlenderVersion = SaveVersion; + mBlenderVersionEdit->setText(mBlenderVersion); + } + } + if (mAddonVersion != SaveAddonVersion) + { + mConfigured = false; + mAddonVersion = SaveAddonVersion; + mAddonVersionEdit->setText(mAddonVersion); + } + Errors = ErrorList.join(" "); + } +} + +void lcBlenderPreferences::ReadStdOut() +{ + QString const& StdOut = QString(mProcess->readAllStandardOutput()); + + mStdOutList.append(StdOut); + + QRegExp RxInfo("^INFO: "); + QRegExp RxError("(?:\\w)*ERROR: ", Qt::CaseInsensitive); + QRegExp RxWarning("(?:\\w)*WARNING: ", Qt::CaseInsensitive); + + bool const Error = StdOut.contains(RxError); + bool const Warning = StdOut.contains(RxWarning); + + if (StdOut.contains(RxInfo) && !Error) + StatusUpdate(true, false); + + QString ErrorsAndWarnings; + + ReadStdOut(StdOut, ErrorsAndWarnings); + + if (!ErrorsAndWarnings.isEmpty()) + { + QString const StdOutLog = QDir::toNativeSeparators(QString("
- See %1/Blender/stdout-blender-addon-install") + .arg(mDataDir)); + + QMessageBox::Icon Icon = Error ? QMessageBox::Critical : QMessageBox::Warning; + QString const& Items = Error ? tr("errors%1").arg(Warning ? tr(" and warnings") : "") : Warning ? tr("warnings") : ""; + + QString const& Title = tr ("%1 Blender Addon Install").arg(LC_PRODUCTNAME_STR); + QString const& Header = "" + tr ("Addon install standard output.") + ""; + QString const& Body = tr ("LDraw addon install encountered %1. See Show Details...").arg(Items); + ShowMessage(Header, Title, Body, ErrorsAndWarnings.append(StdOutLog), MBB_OK, Icon); + } +} + +QString lcBlenderPreferences::ReadStdErr(bool& Error) const +{ + auto CleanLine = [](const QString& Line) + { + return Line.trimmed() + "
"; + }; + + Error = false; + QStringList ReturnLines; + + QString const BlenderDir = QString("%1/Blender").arg(mDataDir); + QFile File(QString("%1/stderr-blender-addon-install").arg(BlenderDir)); + if ( ! File.open(QFile::ReadOnly | QFile::Text)) + { + QString Message = tr("Failed to open log file: %1:\n%2") + .arg(File.fileName()) + .arg(File.errorString()); + return Message; + } + QTextStream In(&File); + while ( ! In.atEnd()) + { + QString const& Line = In.readLine(0); + ReturnLines << CleanLine(Line); + if (!Error) + Error = !Line.isEmpty(); + } + return ReturnLines.join(" "); +} + +void lcBlenderPreferences::WriteStdOut() +{ + QString const BlenderDir = QString("%1/Blender").arg(mDataDir); + QFile File(QString("%1/stdout-blender-addon-install").arg(BlenderDir)); + if (File.open(QFile::WriteOnly | QIODevice::Truncate | QFile::Text)) + { + QTextStream Out(&File); + for (const QString& Line : mStdOutList) + Out << Line << LineEnding; + File.close(); + mAddonStdOutButton->setEnabled(true); + } + else + ShowMessage(tr("Error writing to %1 file '%2':\n%3").arg("stdout").arg(File.fileName(), File.errorString())); +} + +bool lcBlenderPreferences::PromptCancel() +{ +#ifndef QT_NO_PROCESS + if (mProcess) + { + QString const& Title = tr ("Cancel %1 Addon Install").arg(LC_PRODUCTNAME_STR); + QString const& Header = "" + tr("Are you sure you want to cancel the add on install ?") + ""; + int Exec = ShowMessage(Header, Title, QString(), QString(), MBB_YES_NO, QMessageBox::Question); + if (Exec == QMessageBox::Yes) + { + mProcess->kill(); + delete mProcess; + mProcess = nullptr; + } + else + return false; + } +#endif + + mDialogCancelled = true; + return true; +} + +void lcBlenderPreferences::Update() +{ +#ifndef QT_NO_PROCESS + if (!mProcess) + return; + + if (mProcess->state() == QProcess::NotRunning) + ShowResult(); +#endif + QApplication::processEvents(); +} + +void lcBlenderPreferences::Apply(const int Response) +{ + if (Response == QDialog::Accepted) + { + if (SettingsModified()) + SaveSettings(); + } + else if (mDialogCancelled) + { + if (SettingsModified()) + if (PromptAccept()) + SaveSettings(); + } +} + +bool lcBlenderPreferences::SettingsModified(bool Update, const QString& Module) +{ + if (gAddonPreferences->mDialogCancelled) + return false; + + int& Width = gAddonPreferences->mImageWidth; + int& Height = gAddonPreferences->mImageHeight; + double& Scale = gAddonPreferences->mScale; + + bool ModuleMM = !Module.isEmpty() + ? Module == QLatin1String("MM") + : gAddonPreferences->mImportMMActBox->isChecked(); + + bool Ok, Modified = !QFileInfo(lcGetProfileString(LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH)).isReadable(); + qreal _Width = 0.0, _Height = 0.0, _Scale = 0.0, _Value = 0.0, _OldValue = 0.0; + QString OldValue; + + auto ItemChanged = [](qreal OldValue, qreal NewValue) + { + return NewValue > OldValue || NewValue < OldValue; + }; + + if (ModuleMM) + { + for (int LblIdx = 0; LblIdx < NumSettingsMM(); LblIdx++) + { + // checkboxes + if (LblIdx < LBL_BEVEL_SEGMENTS) + { + for (int CtlIdx = 0; CtlIdx < gAddonPreferences->mCheckBoxList.size(); CtlIdx++) + { + OldValue = mBlenderSettingsMM[LblIdx].value; + if (Update) + mBlenderSettingsMM[LblIdx].value = QString::number(gAddonPreferences->mCheckBoxList[CtlIdx]->isChecked()); + Modified |= mBlenderSettingsMM[LblIdx].value != OldValue; + if (LblIdx < LBL_VERBOSE_MM) + LblIdx++; + } + } + // lineedits + else if (LblIdx < LBL_CHOSEN_LOGO) + { + for (int CtlIdx = 0; CtlIdx < gAddonPreferences->mLineEditList.size(); CtlIdx++) + { + if (CtlIdx == CTL_RESOLUTION_WIDTH_EDIT) + { + _OldValue = Width; + _Width = gAddonPreferences->mLineEditList[CtlIdx]->text().toDouble(&Ok); + if (Ok) + { + if (Update) + { + Width = int(_Width); + mBlenderSettingsMM[LblIdx].value = QString::number(Width); + } + Modified |= ItemChanged(_OldValue, _Width); + } + } + else if (CtlIdx == CTL_RESOLUTION_HEIGHT_EDIT) + { + _OldValue = Height; + _Height = gAddonPreferences->mLineEditList[CtlIdx]->text().toDouble(&Ok); + if (Ok) + { + if (Update) + { + Height = int(_Height); + mBlenderSettingsMM[LblIdx].value = QString::number(Height); + } + Modified |= ItemChanged(_OldValue, _Height); + } + } + else if (CtlIdx == CTL_RENDER_PERCENTAGE_EDIT_MM) + { + _OldValue = Scale; + _Scale = gAddonPreferences->mLineEditList[CtlIdx]->text().toInt(&Ok); + if (Ok) + { + if (Update) + { + Scale = double(_Scale / 100); + mBlenderSettingsMM[LblIdx].value = QString::number(_Scale); + } + Modified |= ItemChanged(_OldValue, Scale); + } + } + else + { + _OldValue = mBlenderSettingsMM[LblIdx].value.toDouble(); + _Value = gAddonPreferences->mLineEditList[CtlIdx]->text().toDouble(&Ok); + if (Ok) + { + if (Update) + mBlenderSettingsMM[LblIdx].value = QString::number(_Value); + Modified |= ItemChanged(_OldValue, _Value); + } + } + if (LblIdx < LBL_STARTING_STEP_FRAME) + LblIdx++; + } + } + // comboboxes + else + { + for (int CtlIdx = 0; CtlIdx < gAddonPreferences->mComboBoxList.size(); CtlIdx++) + { + OldValue = mBlenderSettingsMM[LblIdx].value; + QString const Value = gAddonPreferences->mComboBoxList[CtlIdx]->itemData(gAddonPreferences->mComboBoxList[CtlIdx]->currentIndex()).toString(); + if (Update) + mBlenderSettingsMM[LblIdx].value = Value; + Modified |= Value != OldValue; + LblIdx++; + } + } + } + } + else + { + // settings + for (int LblIdx = 0; LblIdx < NumSettings(); LblIdx++) + { + // checkboxes + if (LblIdx < LBL_BEVEL_WIDTH) + { + for (int CtlIdx = 0; CtlIdx < gAddonPreferences->mCheckBoxList.size(); CtlIdx++) + { + OldValue = mBlenderSettings[LblIdx].value; + if (Update) + mBlenderSettings[LblIdx].value = QString::number(gAddonPreferences->mCheckBoxList[CtlIdx]->isChecked()); + Modified |= mBlenderSettings[LblIdx].value != OldValue; + if (LblIdx < LBL_VERBOSE) + LblIdx++; + } + } + // lineedits + else if (LblIdx < LBL_COLOUR_SCHEME) + { + for (int CtlIdx = 0; CtlIdx < gAddonPreferences->mLineEditList.size(); CtlIdx++) + { + if (CtlIdx == CTL_IMAGE_WIDTH_EDIT) + { + _OldValue = Width; + _Width = gAddonPreferences->mLineEditList[CtlIdx]->text().toDouble(&Ok); + if (Ok) + { + if (Update) + { + Width = int(_Width); + mBlenderSettings[LblIdx].value = QString::number(Width); + } + Modified |= ItemChanged(_OldValue, _Width); + } + } + else if (CtlIdx == CTL_IMAGE_HEIGHT_EDIT) + { + _OldValue = Height; + _Height = gAddonPreferences->mLineEditList[CtlIdx]->text().toDouble(&Ok); + if (Ok) + { + if (Update) + { + Height = int(_Height); + mBlenderSettings[LblIdx].value = QString::number(Height); + } + Modified |= ItemChanged(_OldValue, _Height); + } + } + else if (CtlIdx == CTL_RENDER_PERCENTAGE_EDIT) + { + _OldValue = Scale; + _Scale = gAddonPreferences->mLineEditList[CtlIdx]->text().toInt(&Ok); + if (Ok) + { + if (Update) + { + Scale = double(_Scale / 100); + mBlenderSettings[LblIdx].value = QString::number(_Scale); + } + Modified |= ItemChanged(_OldValue, Scale); + } + } + else + { + if (CtlIdx == CTL_DEFAULT_COLOUR_EDIT) + { + _OldValue = lcGetColorIndex(mBlenderSettings[LblIdx].value.toInt()); // colour code + _Value = gAddonPreferences->mLineEditList[CtlIdx]->property("ColorIndex").toInt(&Ok); + } + else + { + _OldValue = mBlenderSettings[LblIdx].value.toDouble(); + _Value = gAddonPreferences->mLineEditList[CtlIdx]->text().toDouble(&Ok); + } + if (Ok) + { + if (Update) + { + if (CtlIdx == CTL_DEFAULT_COLOUR_EDIT) + { + mBlenderSettings[LblIdx].value = QString::number(lcGetColorCode(qint32(_Value))); + } + else + { + mBlenderSettings[LblIdx].value = QString::number(_Value); + } + } + Modified |= ItemChanged(_OldValue, _Value); + } + } + if (LblIdx < LBL_RENDER_PERCENTAGE) + LblIdx++; + } + } + // comboboxes + else + { + for (int CtlIdx = 0; CtlIdx < gAddonPreferences->mComboBoxList.size(); CtlIdx++) + { + OldValue = mBlenderSettings[LblIdx].value; + QString const Value = gAddonPreferences->mComboBoxList[CtlIdx]->itemData(gAddonPreferences->mComboBoxList[CtlIdx]->currentIndex()).toString(); + if (Update) + mBlenderSettings[LblIdx].value = Value; + Modified |= Value != OldValue; + LblIdx++; + } + } + } + } + + // paths + for (int LblIdx = 0; LblIdx < NumPaths(); LblIdx++) + { + OldValue = mBlenderPaths[LblIdx].value; + QString const Value = gAddonPreferences->mPathLineEditList[LblIdx]->text(); + if (Update) + mBlenderPaths[LblIdx].value = Value; + Modified |= Value != OldValue; + } + + return Modified; +} + +void lcBlenderPreferences::ResetSettings() +{ + BlenderPaths const* Paths = mBlenderPaths; + BlenderSettings const* Settings = mBlenderSettings; + BlenderSettings const* SettingsMM = mBlenderSettingsMM; + + QDialog* Dialog = new QDialog(this); + Dialog->setWindowTitle(tr("Addon Reset")); + Dialog->setWhatsThis(tr("Select how to reset settings. Choice is since last apply or system default.")); + QVBoxLayout* Layout = new QVBoxLayout(Dialog); + QGroupBox* DlgGroup = new QGroupBox(Dialog); + QHBoxLayout* DlgLayout = new QHBoxLayout(DlgGroup); + QRadioButton* LastButton = new QRadioButton(tr("Last Apply")); + DlgLayout->addWidget(LastButton); + QRadioButton* DefaultButton = new QRadioButton(tr("System Default")); + DlgLayout->addWidget(DefaultButton); + DlgGroup->setLayout(DlgLayout); + Layout->addWidget(DlgGroup); + Dialog->setLayout(Layout); + + LastButton->setChecked(true); + + QDialogButtonBox ButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, + Qt::Horizontal, Dialog); + Layout->addWidget(&ButtonBox); + connect(&ButtonBox, SIGNAL(accepted()), Dialog, SLOT(accept())); + connect(&ButtonBox, SIGNAL(rejected()), Dialog, SLOT(reject())); + + if (Dialog->exec() == QDialog::Accepted) + { + if (DefaultButton->isChecked()) + { + Paths = mDefaultPaths; + Settings = mDefaultSettings; + SettingsMM = mDefaultSettingsMM; + } + } + + mConfigured = !lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE).isEmpty(); + + mBlenderPaths[PATH_BLENDER].value = lcGetProfileString(LC_PROFILE_BLENDER_PATH); + mBlenderVersion = lcGetProfileString(LC_PROFILE_BLENDER_VERSION); + mAddonVersion = lcGetProfileString(LC_PROFILE_BLENDER_ADDON_VERSION); + + mBlenderVersionEdit->setText(mBlenderVersion); + mAddonVersionEdit->setText(mAddonVersion); + + if (mImportActBox->isChecked()) + { + disconnect(mLineEditList[CTL_IMAGE_HEIGHT_EDIT], SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + disconnect(mLineEditList[CTL_IMAGE_WIDTH_EDIT], SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + + for (int LblIdx = 0; LblIdx < NumSettings(); LblIdx++) + { + if (LblIdx < LBL_BEVEL_WIDTH) + { + for (int CtlIdx = 0; CtlIdx < mCheckBoxList.size(); CtlIdx++) + { + mCheckBoxList[CtlIdx]->setChecked(Settings[LblIdx].value.toInt()); + if (LblIdx < LBL_VERBOSE) + LblIdx++; + } + } + else if (LblIdx < LBL_COLOUR_SCHEME) + { + for (int CtlIdx = 0; CtlIdx < mLineEditList.size(); CtlIdx++) + { + if (CtlIdx == CTL_IMAGE_WIDTH_EDIT) + mLineEditList[CtlIdx]->setText(QString::number(mImageWidth)); + else if (CtlIdx == CTL_IMAGE_HEIGHT_EDIT) + mLineEditList[CtlIdx]->setText(QString::number(mImageHeight)); + else if (CtlIdx == CTL_RENDER_PERCENTAGE_EDIT) + mLineEditList[CtlIdx]->setText(QString::number(mScale * 100)); + else if (CtlIdx == CTL_DEFAULT_COLOUR_EDIT) + SetDefaultColor(lcGetColorIndex(Settings[LBL_DEFAULT_COLOUR].value.toInt())); + else + mLineEditList[CtlIdx]->setText(Settings[LblIdx].value); + if (LblIdx < LBL_RENDER_PERCENTAGE) + LblIdx++; + } + } + else + { + for (int CtlIdx = 0; CtlIdx < mComboBoxList.size(); CtlIdx++) + { + mComboBoxList[CtlIdx]->setCurrentIndex(int(mComboBoxList[CtlIdx]->findData(QVariant::fromValue(Settings[LblIdx].value)))); + LblIdx++; + } + } + } + + for (int LblIdx = 0; LblIdx < NumPaths(); LblIdx++) + { + mPathLineEditList[LblIdx]->setText(Paths[LblIdx].value); + } + + connect(mLineEditList[CTL_IMAGE_HEIGHT_EDIT],SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + connect(mLineEditList[CTL_IMAGE_WIDTH_EDIT], SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + } + else if (mImportMMActBox->isChecked()) + { + disconnect(mLineEditList[CTL_RESOLUTION_HEIGHT_EDIT], SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + disconnect(mLineEditList[CTL_RESOLUTION_WIDTH_EDIT], SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + + for (int LblIdx = 0; LblIdx < NumSettingsMM(); LblIdx++) + { + if (LblIdx < LBL_BEVEL_SEGMENTS) + { + for (int CtlIdx = 0; CtlIdx < mCheckBoxList.size(); CtlIdx++) + { + mCheckBoxList[CtlIdx]->setChecked(SettingsMM[LblIdx].value.toInt()); + if (LblIdx < LBL_VERBOSE_MM) + LblIdx++; + } + } + else if (LblIdx < LBL_CHOSEN_LOGO) + { + for (int CtlIdx = 0; CtlIdx < mLineEditList.size(); CtlIdx++) + { + if (CtlIdx == CTL_RESOLUTION_WIDTH_EDIT) + mLineEditList[CtlIdx]->setText(QString::number(mImageWidth)); + else if (CtlIdx == CTL_RESOLUTION_HEIGHT_EDIT) + mLineEditList[CtlIdx]->setText(QString::number(mImageHeight)); + else if (CtlIdx == CTL_RENDER_PERCENTAGE_EDIT_MM) + mLineEditList[CtlIdx]->setText(QString::number(mScale * 100)); + else + mLineEditList[CtlIdx]->setText(SettingsMM[LblIdx].value); + if (LblIdx < LBL_STARTING_STEP_FRAME) + LblIdx++; + } + } + else + { + for (int CtlIdx = 0; CtlIdx < mComboBoxList.size(); CtlIdx++) + { + mComboBoxList[CtlIdx]->setCurrentIndex(int(mComboBoxList[CtlIdx]->findData(QVariant::fromValue(SettingsMM[LblIdx].value)))); + LblIdx++; + } + } + } + + for (int LblIdx = 0; LblIdx < NumPaths(); LblIdx++) + { + mPathLineEditList[LblIdx]->setText(Paths[LblIdx].value); + } + + connect(mLineEditList[CTL_RESOLUTION_HEIGHT_EDIT], SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + connect(mLineEditList[CTL_RESOLUTION_WIDTH_EDIT], SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + } + + emit SettingChangedSig(true); +} + +void lcBlenderPreferences::LoadSettings() +{ + QStringList const& DataPathList = QStandardPaths::standardLocations(QStandardPaths::DataLocation); + gAddonPreferences->mDataDir = DataPathList.first(); + + if (QFileInfo(lcGetProfileString(LC_PROFILE_BLENDER_PATH)).isReadable()) + { + if (lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE).isEmpty()) + lcSetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE, QLatin1String(LC_BLENDER_ADDON_IMPORT_MODULE)); + } + else + { + lcSetProfileString(LC_PROFILE_BLENDER_PATH, QString()); + lcSetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE, QString()); + } + + if (!QDir(QString("%1/Blender/addons/%2").arg(gAddonPreferences->mDataDir).arg(LC_BLENDER_ADDON_FOLDER_STR)).isReadable()) + lcSetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE, QString()); + + if (!NumPaths()) + { + QString const DefaultBlendFile = QString("%1/Blender/config/%2") + .arg(gAddonPreferences->mDataDir) + .arg(LC_BLENDER_ADDON_BLEND_FILE); + + QStringList const AddonPaths = QStringList() + /* PATH_BLENDER */ << lcGetProfileString(LC_PROFILE_BLENDER_PATH) + /* PATH_BLENDFILE */ << (QFileInfo(DefaultBlendFile).exists() ? DefaultBlendFile : QString()) + /* PATH_ENVIRONMENT */ << QString() + /* PATH_LDCONFIG */ << lcGetProfileString(LC_PROFILE_COLOR_CONFIG) + /* PATH_LDRAW */ << QFileInfo(lcGetProfileString(LC_PROFILE_PARTS_LIBRARY)).absolutePath() + /* PATH_LSYNTH */ << QString() + /* PATH_STUD_LOGO */ << QString() + /* PATH_STUDIO_LDRAW */ << QString(); + for (int LblIdx = 0; LblIdx < NumPaths(DEFAULT_SETTINGS); LblIdx++) + { + mBlenderPaths[LblIdx] = + { + mDefaultPaths[LblIdx].key, + mDefaultPaths[LblIdx].key_mm, + QDir::toNativeSeparators(AddonPaths.at(LblIdx)), + mDefaultPaths[LblIdx].label, + mDefaultPaths[LblIdx].tooltip + }; + } + } + + if (!NumSettings()) + { + for (int LblIdx = 0; LblIdx < NumSettings(DEFAULT_SETTINGS); LblIdx++) + { + mBlenderSettings[LblIdx] = + { + mDefaultSettings[LblIdx].key, + mDefaultSettings[LblIdx].value, + mDefaultSettings[LblIdx].label, + mDefaultSettings[LblIdx].tooltip + }; + } + } + + if (!NumSettingsMM()) + { + for (int LblIdx = 0; LblIdx < NumSettingsMM(DEFAULT_SETTINGS); LblIdx++) + { + mBlenderSettingsMM[LblIdx] = + { + mDefaultSettingsMM[LblIdx].key, + mDefaultSettingsMM[LblIdx].value, + mDefaultSettingsMM[LblIdx].label, + mDefaultSettingsMM[LblIdx].tooltip + }; + } + } + + QFileInfo BlenderConfigFileInfo(lcGetProfileString(LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH)); + + bool ConfigFileExists = BlenderConfigFileInfo.exists(); + + if (ConfigFileExists) + { + QSettings Settings(BlenderConfigFileInfo.absoluteFilePath(), QSettings::IniFormat); + + for (int LblIdx = 1/*skip blender executable*/; LblIdx < NumPaths(); LblIdx++) + { + if (LblIdx == PATH_STUDIO_LDRAW) + continue; + QString const& Key = QString("%1/%2").arg(LC_BLENDER_ADDON, mBlenderPaths[LblIdx].key); + QString const& Value = Settings.value(Key, QString()).toString(); + if (QFileInfo(Value).exists()) + { + mBlenderPaths[LblIdx].value = QDir::toNativeSeparators(Value); + } + } + + for (int LblIdx = 1/*skip blender executable*/; LblIdx < NumPaths(); LblIdx++) + { + if (LblIdx == PATH_LSYNTH || LblIdx == PATH_STUD_LOGO) + continue; + QString const& Key = QString("%1/%2").arg(LC_BLENDER_ADDON_MM, mBlenderPaths[LblIdx].key_mm); + QString const& Value = Settings.value(Key, QString()).toString(); + if (QFileInfo(Value).exists()) + { + mBlenderPaths[LblIdx].value = QDir::toNativeSeparators(Value); + } + } + + for (int LblIdx = 0; LblIdx < NumSettings(); LblIdx++) + { + QString const& Key = QString("%1/%2").arg(LC_BLENDER_ADDON, mBlenderSettings[LblIdx].key); + QString const& Value = Settings.value(Key, QString()).toString(); + if (!Value.isEmpty()) + { + mBlenderSettings[LblIdx].value = Value == "True" ? "1" : Value == "False" ? "0" : Value; + } + if (LblIdx == LBL_IMAGE_WIDTH || LblIdx == LBL_IMAGE_HEIGHT || LblIdx == LBL_RENDER_PERCENTAGE) + { + QString const& Label = mDefaultSettings[LblIdx].label; + mBlenderSettings[LblIdx].label = QString("%1 - Setting (%2)").arg(Label).arg(Value); + } + } + + for (int LblIdx = 0; LblIdx < NumSettingsMM(); LblIdx++) + { + QString const& Key = QString("%1/%2").arg(LC_BLENDER_ADDON_MM, mBlenderSettingsMM[LblIdx].key); + QString const& Value = Settings.value(Key, QString()).toString(); + if (!Value.isEmpty()) + { + mBlenderSettingsMM[LblIdx].value = Value == "True" ? "1" : Value == "False" ? "0" : Value; + } + if (LblIdx == LBL_RENDER_PERCENTAGE_MM || LblIdx == LBL_RESOLUTION_WIDTH || LblIdx == LBL_RESOLUTION_HEIGHT) + { + QString const& Label = mDefaultSettingsMM[LblIdx].label; + mBlenderSettingsMM[LblIdx].label = QString("%1 - Setting (%2)").arg(Label).arg(Value); + } + } + } + else + { + QString const LogFile = QString("%1/Blender/stdout-blender-addon-install").arg(gAddonPreferences->mDataDir); + if (QFileInfo(LogFile).isReadable()) + { + QFile File(LogFile); + if (File.open(QFile::ReadOnly | QFile::Text)) + + { + QByteArray Ba = File.readAll(); + File.close(); + QString Errors; + gAddonPreferences->mProgressBar = nullptr; + gAddonPreferences->ReadStdOut(QString(Ba), Errors); + } + else + { + ShowMessage(tr("Failed to open log file: %1:\n%2") + .arg(File.fileName()) + .arg(File.errorString())); + } + } + } + + mBlenderSettings[LBL_IMAGE_WIDTH].value = QString::number(gAddonPreferences->mImageWidth); + mBlenderSettings[LBL_IMAGE_HEIGHT].value = QString::number(gAddonPreferences->mImageHeight); + mBlenderSettings[LBL_RENDER_PERCENTAGE].value = QString::number(gAddonPreferences->mScale * 100); + + mBlenderSettingsMM[LBL_RESOLUTION_WIDTH].value = QString::number(gAddonPreferences->mImageWidth); + mBlenderSettingsMM[LBL_RESOLUTION_HEIGHT].value = QString::number(gAddonPreferences->mImageHeight); + mBlenderSettingsMM[LBL_RENDER_PERCENTAGE_MM].value = QString::number(gAddonPreferences->mScale * 100); + + mBlenderPaths[PATH_BLENDER].value = lcGetProfileString(LC_PROFILE_BLENDER_PATH); + gAddonPreferences->mBlenderVersion = lcGetProfileString(LC_PROFILE_BLENDER_VERSION); + gAddonPreferences->mAddonVersion = lcGetProfileString(LC_PROFILE_BLENDER_ADDON_VERSION); +} + +void lcBlenderPreferences::SaveSettings() +{ + if (!NumSettings() || !NumSettingsMM()) + LoadSettings(); + + QString const BlenderConfigDir = QString("%1/Blender/setup/addon_setup/config").arg(gAddonPreferences->mDataDir); + + QString Key, Value = mBlenderPaths[PATH_BLENDER].value; + if (Value.isEmpty()) + Value = gAddonPreferences->mPathLineEditList[PATH_BLENDER]->text(); + + lcSetProfileString(LC_PROFILE_BLENDER_PATH, QDir::toNativeSeparators(Value)); + + Value.clear(); + if (!gAddonPreferences->mBlenderVersion.isEmpty()) + Value = gAddonPreferences->mBlenderVersion; + + if (!gAddonPreferences->mAddonVersion.isEmpty()) + { + gAddonPreferences->mModulesBox->setEnabled(true); + gAddonPreferences->mAddonVersionEdit->setText(gAddonPreferences->mAddonVersion); + } + + lcSetProfileString(LC_PROFILE_BLENDER_VERSION, Value); + + Value = lcGetProfileString(LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH).isEmpty() + ? QString("%1/%2").arg(BlenderConfigDir).arg(LC_BLENDER_ADDON_CONFIG_FILE) + : lcGetProfileString(LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH); + lcSetProfileString(LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH, QDir::toNativeSeparators(Value)); + + QString searchDirectoriesKey; + QString parameterFileKey = QLatin1String("ParameterFile"); + QString ParameterFile = QString("%1/%2").arg(BlenderConfigDir).arg(LC_BLENDER_ADDON_PARAMS_FILE); + + QSettings Settings(Value, QSettings::IniFormat); + + auto concludeSettingsGroup = [&]() + { + if (!QFileInfo(ParameterFile).exists()) + ExportParameterFile(); + Value = QDir::toNativeSeparators(QFileInfo(lcGetProfileString(LC_PROFILE_PARTS_LIBRARY)).absolutePath()); + Settings.setValue(searchDirectoriesKey, QVariant(Value)); + Settings.endGroup(); + }; + + Settings.beginGroup(LC_BLENDER_ADDON); + + for (int LblIdx = 1/*skip blender executable*/; LblIdx < NumPaths(); LblIdx++) + { + if (LblIdx == PATH_STUDIO_LDRAW) + continue; + Key = mBlenderPaths[LblIdx].key; + Value = QDir::toNativeSeparators(mBlenderPaths[LblIdx].value); + Settings.setValue(Key, QVariant(Value)); + } + + for (int LblIdx = 0; LblIdx < NumSettings(); LblIdx++) + { + if (LblIdx == LBL_KEEP_ASPECT_RATIO) + { + continue; + } + else if (LblIdx < LBL_BEVEL_WIDTH) + { + Value = mBlenderSettings[LblIdx].value == "1" ? "True" : "False"; + } + else if (LblIdx > LBL_VERBOSE) + { + if (LblIdx == LBL_FLEX_PARTS_SOURCE || LblIdx == LBL_POSITION_OBJECT) + Value = mBlenderSettings[LblIdx].value == "1" ? "True" : "False"; + else + Value = mBlenderSettings[LblIdx].value; + } + + Key = mBlenderSettings[LblIdx].key; + Settings.setValue(Key, QVariant(Value)); + } + + Settings.setValue(parameterFileKey, QVariant(QDir::toNativeSeparators(ParameterFile))); + + searchDirectoriesKey = QLatin1String("additionalSearchDirectories"); + + concludeSettingsGroup(); + + Settings.beginGroup(LC_BLENDER_ADDON_MM); + + for (int LblIdx = 1/*skip blender executable*/; LblIdx < NumPaths(); LblIdx++) + { + if (LblIdx == PATH_LSYNTH || LblIdx == PATH_STUD_LOGO) + continue; + Key = mBlenderPaths[LblIdx].key_mm; + Value = QDir::toNativeSeparators(mBlenderPaths[LblIdx].value); + Settings.setValue(Key, QVariant(Value)); + } + + for (int LblIdx = 0; LblIdx < NumSettingsMM(); LblIdx++) + { + if (LblIdx == LBL_KEEP_ASPECT_RATIO_MM) + { + continue; + } + else if (LblIdx < LBL_BEVEL_SEGMENTS) + { + Value = mBlenderSettingsMM[LblIdx].value == "1" ? "True" : "False"; + } + else if (LblIdx > LBL_VERBOSE_MM) + { + Value = mBlenderSettingsMM[LblIdx].value; + } + + Key = mBlenderSettingsMM[LblIdx].key; + Settings.setValue(Key, QVariant(Value)); + } + + searchDirectoriesKey = QLatin1String("additionalSearchPaths"); + + concludeSettingsGroup(); + + QString const preferredImportModule = + gAddonPreferences->mImportActBox->isChecked() + ? QString("TN") + : gAddonPreferences->mImportMMActBox->isChecked() + ? QString("MM") : QString(); + + if (preferredImportModule != lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE)) + lcSetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE, preferredImportModule); +} + +void lcBlenderPreferences::EnableImportModule() +{ + QString saveImportModule, preferredImportModule; + if (sender() == mImportActBox && mImportActBox->isChecked()) + { + preferredImportModule = QLatin1String("TN"); + if (mImportMMActBox->isChecked()) + saveImportModule = QLatin1String("MM"); + mImportMMActBox->setChecked(false); + } + else if (sender() == mImportMMActBox && mImportMMActBox->isChecked()) + { + preferredImportModule = QLatin1String("MM"); + if (mImportActBox->isChecked()) + saveImportModule = QLatin1String("TN"); + mImportActBox->setChecked(false); + } + + if (preferredImportModule.isEmpty()) + return; + + if (SettingsModified(true, saveImportModule)) + SaveSettings(); + + lcSetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE, preferredImportModule); + + if (mImportMMActBox->isChecked()) + InitPathsAndSettingsMM(); + else + InitPathsAndSettings(); + + ConfigureBlenderAddon(false, false, true); +} + +int lcBlenderPreferences::NumSettings(bool DefaultSettings) +{ + int Size = 0; + if (!mBlenderSettings[0].key.isEmpty() || DefaultSettings) + Size = sizeof(mBlenderSettings)/sizeof(mBlenderSettings[0]); + return Size; +} + +int lcBlenderPreferences::NumSettingsMM(bool DefaultSettings) +{ + int Size = 0; + if (!mBlenderSettingsMM[0].key.isEmpty() || DefaultSettings) + Size = sizeof(mBlenderSettingsMM)/sizeof(mBlenderSettingsMM[0]); + return Size; +} + +int lcBlenderPreferences::NumPaths(bool DefaultSettings) +{ + int Size = 0; + if (!mBlenderPaths[0].key.isEmpty() || DefaultSettings) + Size = sizeof(mBlenderPaths)/sizeof(mBlenderPaths[0]); + return Size; +} + +void lcBlenderPreferences::ShowPathsGroup() +{ + if (mPathsBox->isHidden()) + mPathsBox->show(); + else + mPathsBox->hide(); + + mContent->adjustSize(); +} + +void lcBlenderPreferences::ColorButtonClicked(bool) +{ + int ColorIndex = mLineEditList[CTL_DEFAULT_COLOUR_EDIT]->property("ColorIndex").toInt(); + + QWidget* Parent = mLineEditList[CTL_DEFAULT_COLOUR_EDIT]; + lcColorPickerPopup* Popup = new lcColorPickerPopup(Parent, ColorIndex); + connect(Popup, SIGNAL(selected(int)), SLOT(SetDefaultColor(int))); + Popup->setMinimumSize(300, 200); + + const QRect DesktopGeom = QApplication::desktop()->geometry(); + + QPoint Pos = Parent->mapToGlobal(Parent->rect().bottomLeft()); + if (Pos.x() < DesktopGeom.left()) + Pos.setX(DesktopGeom.left()); + + if (Pos.y() < DesktopGeom.top()) + Pos.setY(DesktopGeom.top()); + + if ((Pos.x() + Popup->width()) > DesktopGeom.width()) + Pos.setX(DesktopGeom.width() - Popup->width()); + + if ((Pos.y() + Popup->height()) > DesktopGeom.bottom()) + Pos.setY(DesktopGeom.bottom() - Popup->height()); + + Popup->move(Pos); + + Popup->setFocus(); + Popup->show(); +} + +void lcBlenderPreferences::SetDefaultColor(int ColorIndex) +{ + QImage Image(12, 12, QImage::Format_ARGB32); + Image.fill(0); + + lcColor* Colour =& gColorList[ColorIndex]; + QPainter Painter(&Image); + Painter.setCompositionMode(QPainter::CompositionMode_Source); + Painter.setPen(Qt::darkGray); + Painter.setBrush(QColor::fromRgbF(qreal(Colour->Value[0]), qreal(Colour->Value[1]), qreal(Colour->Value[2]))); + Painter.drawRect(0, 0, Image.width() - 1, Image.height() - 1); + Painter.end(); + + int const ColourCode = lcGetColorCode(ColorIndex); + mLineEditList[CTL_DEFAULT_COLOUR_EDIT]->setText(QString("%1 (%2)").arg(Colour->Name).arg(ColourCode)); + mLineEditList[CTL_DEFAULT_COLOUR_EDIT]->setProperty("ColorIndex", QVariant::fromValue(ColorIndex)); + mDefaultColourEditAction->setIcon(QPixmap::fromImage(Image)); + mDefaultColourEditAction->setToolTip(tr("Select Colour")); + + bool Change = mBlenderSettings[LBL_DEFAULT_COLOUR].value != QString::number(ColourCode); + Change |= SettingsModified(false); + emit SettingChangedSig(Change); +} + +void lcBlenderPreferences::BrowseBlender(bool) +{ + for (int LblIdx = 0; LblIdx < NumPaths(); ++LblIdx) + { + if (sender() == mPathBrowseButtonList.at(LblIdx)) + { + QString const BlenderPath = QDir::toNativeSeparators(mBlenderPaths[LblIdx].value).toLower(); + QFileDialog FileDialog(nullptr); + FileDialog.setWindowTitle(tr("Locate %1").arg(mBlenderPaths[LblIdx].label)); + if (LblIdx < PATH_LDRAW) + FileDialog.setFileMode(QFileDialog::ExistingFile); + else + FileDialog.setFileMode(QFileDialog::Directory); + if (!BlenderPath.isEmpty()) + FileDialog.setDirectory(QFileInfo(BlenderPath).absolutePath()); + if (FileDialog.exec()) + { + QStringList SelectedPathList = FileDialog.selectedFiles(); + if (SelectedPathList.size() == 1) + { + QFileInfo PathInfo(SelectedPathList.at(0)); + if (PathInfo.exists()) + { + QString const SelectedPath = QDir::toNativeSeparators(PathInfo.absoluteFilePath()).toLower(); + mPathLineEditList[LblIdx]->setText(SelectedPathList.at(0)); + if (LblIdx != PATH_BLENDER) + { + bool Change = false; + if (mImportMMActBox->isChecked()) + Change = QDir::toNativeSeparators(mBlenderSettingsMM[LblIdx].value).toLower() != SelectedPath; + else + Change = QDir::toNativeSeparators(mBlenderSettings[LblIdx].value).toLower() != SelectedPath; + Change |= SettingsModified(false); + emit SettingChangedSig(Change); + } + + if (LblIdx == PATH_BLENDER && BlenderPath != SelectedPath) + { + mBlenderPaths[LblIdx].value = SelectedPath; + UpdateBlenderAddon(); + } + } + } + } + } + } +} + +void lcBlenderPreferences::SizeChanged(const QString& Value) +{ + const bool ImportMM = mImportMMActBox->isChecked(); + const int Keep_Aspect_Ratio = ImportMM ? int(CTL_KEEP_ASPECT_RATIO_BOX_MM) : int(CTL_KEEP_ASPECT_RATIO_BOX); + const int Width_Edit = ImportMM ? int(CTL_RESOLUTION_WIDTH_EDIT) : int(CTL_IMAGE_WIDTH_EDIT); + const int Height_Edit = ImportMM ? int(CTL_RESOLUTION_HEIGHT_EDIT) : int(CTL_IMAGE_HEIGHT_EDIT); + BlenderSettings const* Settings = ImportMM ? mBlenderSettingsMM : mBlenderSettings; + + bool Change = false; + int NewValue = Value.toInt(); + if (mCheckBoxList[Keep_Aspect_Ratio]->isChecked()) + { + if (sender() == mLineEditList[Width_Edit]) + { + disconnect(mLineEditList[Height_Edit],SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + + QString const Height = QString::number(qRound(double(mImageHeight * NewValue / mImageWidth))); + mLineEditList[Height_Edit]->setText(Height); + + Change = Settings[Height_Edit].value != Height; + + connect(mLineEditList[Height_Edit],SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + } + else if (sender() == mLineEditList[Height_Edit]) + { + disconnect(mLineEditList[Width_Edit],SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + + QString const Width = QString::number(qRound(double(NewValue * mImageWidth / mImageHeight))); + mLineEditList[Width_Edit]->setText(Width); + + Change = Settings[Height_Edit].value != Width; + + connect(mLineEditList[Width_Edit],SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + } + + // Change is provided here for consistency only as ImageWidth, + // ImageHeight, and RenderPercentage are passed at the render command + Change |= SettingsModified(false); + emit SettingChangedSig(Change); + } +} + +void lcBlenderPreferences::SetModelSize(bool Update) +{ + const bool ImportMM = mImportMMActBox->isChecked(); + const int Crop_Image = ImportMM ? int(CTL_CROP_IMAGE_BOX_MM) : int(CTL_CROP_IMAGE_BOX); + const int Add_Environment = ImportMM ? int(CTL_ADD_ENVIRONMENT_BOX_MM) : int(CTL_ADD_ENVIRONMENT_BOX); + const int Trans_Background = ImportMM ? int(CTL_TRANSPARENT_BACKGROUND_BOX_MM) : int(CTL_TRANSPARENT_BACKGROUND_BOX); + const int Keep_Aspect_Ratio = ImportMM ? int(CTL_KEEP_ASPECT_RATIO_BOX_MM) : int(CTL_KEEP_ASPECT_RATIO_BOX); + const int Width_Edit = ImportMM ? int(CTL_RESOLUTION_WIDTH_EDIT) : int(CTL_IMAGE_WIDTH_EDIT); + const int Height_Edit = ImportMM ? int(CTL_RESOLUTION_HEIGHT_EDIT) : int(CTL_IMAGE_HEIGHT_EDIT); + const int Crop_Image_Label = ImportMM ? int(LBL_CROP_IMAGE_MM) : int(LBL_CROP_IMAGE); + + QString const CropImageLabel = mSettingLabelList[Crop_Image_Label]->text(); + + int ImageWidth = mImageWidth; + int ImageHeight = mImageHeight; + + const bool CropImage = mCheckBoxList[Crop_Image]->isChecked(); + + lcModel* Model = lcGetActiveProject()->GetActiveModel(); + if (Model) + { + struct NativeImage + { + QImage RenderedImage; + QRect Bounds; + }; + + std::vector Images; + Images.push_back(NativeImage()); + NativeImage& Image = Images.back(); + Image.RenderedImage = Model->GetStepImage(false, ImageWidth, ImageHeight, Model->GetCurrentStep()); + + auto CalculateImageBounds = [](NativeImage& Image) + { + QImage& RenderedImage = Image.RenderedImage; + int Width = RenderedImage.width(); + int Height = RenderedImage.height(); + + int MinX = Width; + int MinY = Height; + int MaxX = 0; + int MaxY = 0; + + for (int x = 0; x < Width; x++) + { + for (int y = 0; y < Height; y++) + { + if (qAlpha(RenderedImage.pixel(x, y))) + { + MinX = qMin(x, MinX); + MinY = qMin(y, MinY); + MaxX = qMax(x, MaxX); + MaxY = qMax(y, MaxY); + } + } + } + + Image.Bounds = QRect(QPoint(MinX, MinY), QPoint(MaxX, MaxY)); + }; + + QtConcurrent::blockingMap(Images, CalculateImageBounds); + + ImageWidth = Image.Bounds.width(); + ImageHeight = Image.Bounds.height(); + } + + mSettingLabelList[Crop_Image_Label]->setText(QString("%1 (%2 x %3)").arg(CropImageLabel).arg(ImageWidth).arg(ImageHeight)); + + if (CropImage) + { + bool Conflict[3]; + + if ((Conflict[1] = mCheckBoxList[Add_Environment]->isChecked())) + mCheckBoxList[Add_Environment]->setChecked(!CropImage); + if ((Conflict[2] = !mCheckBoxList[Trans_Background]->isChecked())) + mCheckBoxList[Trans_Background]->setChecked(CropImage); + if ((Conflict[0] = mCheckBoxList[Keep_Aspect_Ratio]->isChecked())) + mCheckBoxList[Keep_Aspect_Ratio]->setChecked(!CropImage); + + if (Conflict[0] || Conflict[1] || Conflict[2]) + { + QString const& Title = tr ("LDraw Render Settings Conflict"); + QString const& Header = "" + tr ("Crop image configuration settings conflict were resolved.") + ""; + QString const& Body = QString("%1%2%3") + .arg(Conflict[0] ? tr("Keep aspect ratio set to false.
") : "") + .arg(Conflict[1] ? tr("Add environment (backdrop and base plane) set to false.
") : "") + .arg(Conflict[2] ? tr("Transparent background set to true.
") : ""); + ShowMessage(Header, Title, Body, QString(), MBB_OK, QMessageBox::Information); + } + } + + disconnect(mLineEditList[Width_Edit],SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + disconnect(mLineEditList[Height_Edit],SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + + QString const Width = QString::number(CropImage ? ImageWidth : mImageWidth); + QString const Height = QString::number(CropImage ? ImageHeight : mImageHeight); + + mLineEditList[Width_Edit]->setText(Width); + mLineEditList[Height_Edit]->setText(Height); + + if (Update) + SettingsModified(true); + + connect(mLineEditList[Height_Edit],SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); + connect(mLineEditList[Width_Edit],SIGNAL(textChanged(const QString&)), this, SLOT (SizeChanged(const QString&))); +} + +void lcBlenderPreferences::ValidateColourScheme(int Index) +{ + QComboBox* ComboBox = qobject_cast(sender()); + if (!ComboBox) + return; + + const bool ImportMM = mImportMMActBox->isChecked(); + const int Color_Scheme = ImportMM ? int(LBL_COLOUR_SCHEME_MM) : int(LBL_COLOUR_SCHEME); + BlenderSettings* Settings = ImportMM ? mBlenderSettingsMM : mBlenderSettings; + + if (ComboBox->itemText(Index) == "custom" && mBlenderPaths[PATH_LDCONFIG].value.isEmpty() && lcGetProfileString(LC_PROFILE_COLOR_CONFIG).isEmpty()) + { + BlenderSettings const* defaultSettings = ImportMM ? mDefaultSettingsMM : mDefaultSettings; + Settings[Color_Scheme].value = defaultSettings[Color_Scheme].value; + + QString const& Title = tr ("Custom LDraw Colours"); + QString const& Header = "" + tr ("Colour scheme 'custom' cannot be enabled. Custom LDConfig file not found.") + ""; + QString const& Body = tr ("Colour scheme 'custom' selected but no LDConfig file was specified.
" + "The default colour scheme '%1' will be used.
") + .arg(Settings[Color_Scheme].value); + ShowMessage(Header, Title, Body, QString(), MBB_OK, QMessageBox::Warning); + } + else + { + bool Change = Settings[Color_Scheme].value != ComboBox->itemText(Index); + Change |= SettingsModified(false); + emit SettingChangedSig(Change); + } +} + +bool lcBlenderPreferences::PromptAccept() +{ + QString const& Title = tr ("Render Settings Modified"); + QString const& Header = "" + tr("Do you want to accept the modified settings before quitting ?") + ""; + int Exec = ShowMessage(Header, Title, QString(), QString(), MBB_YES_NO, QMessageBox::Question); + if (Exec == QMessageBox::Yes) + return true; + + return false; +} + +void lcBlenderPreferences::LoadDefaultParameters(QByteArray& Buffer, int Which) +{ + /* + # File: BlenderLDrawParameters.lst + # + # This config file captures parameters for the Blender LDraw Render addon + # Parameters must be prefixed using one of the following predefined + # 'Item' labels: + # - lgeo_colour + # - sloped_brick + # - light_brick + + # LGEO CUSTOM COLOURS + # LGEO is a parts library for rendering LEGO using the POV-Ray + # rendering software. This is the list of LEGO colours suitable + # for realistic rendering extracted from the LGEO file 'lg_color.inc'. + # When the 'Colour Scheme' option is set to 'Realistic', the standard + # LDraw colours RGB value is overwritten with the values defined here. + # Note: You can customize these RGB values as you want. + + # Item------- ID-- R-- G-- B-- + */ + const char DefaultCustomColours[] = + { + "lgeo_colour, 0, 33, 33, 33\n" + "lgeo_colour, 1, 13, 105, 171\n" + "lgeo_colour, 2, 40, 127, 70\n" + "lgeo_colour, 3, 0, 143, 155\n" + "lgeo_colour, 4, 196, 40, 27\n" + "lgeo_colour, 5, 205, 98, 152\n" + "lgeo_colour, 6, 98, 71, 50\n" + "lgeo_colour, 7, 161, 165, 162\n" + "lgeo_colour, 8, 109, 110, 108\n" + "lgeo_colour, 9, 180, 210, 227\n" + "lgeo_colour, 10, 75, 151, 74\n" + "lgeo_colour, 11, 85, 165, 175\n" + "lgeo_colour, 12, 242, 112, 94\n" + "lgeo_colour, 13, 252, 151, 172\n" + "lgeo_colour, 14, 245, 205, 47\n" + "lgeo_colour, 15, 242, 243, 242\n" + "lgeo_colour, 17, 194, 218, 184\n" + "lgeo_colour, 18, 249, 233, 153\n" + "lgeo_colour, 19, 215, 197, 153\n" + "lgeo_colour, 20, 193, 202, 222\n" + "lgeo_colour, 21, 224, 255, 176\n" + "lgeo_colour, 22, 107, 50, 123\n" + "lgeo_colour, 23, 35, 71, 139\n" + "lgeo_colour, 25, 218, 133, 64\n" + "lgeo_colour, 26, 146, 57, 120\n" + "lgeo_colour, 27, 164, 189, 70\n" + "lgeo_colour, 28, 149, 138, 115\n" + "lgeo_colour, 29, 228, 173, 200\n" + "lgeo_colour, 30, 172, 120, 186\n" + "lgeo_colour, 31, 225, 213, 237\n" + "lgeo_colour, 32, 0, 20, 20\n" + "lgeo_colour, 33, 123, 182, 232\n" + "lgeo_colour, 34, 132, 182, 141\n" + "lgeo_colour, 35, 217, 228, 167\n" + "lgeo_colour, 36, 205, 84, 75\n" + "lgeo_colour, 37, 228, 173, 200\n" + "lgeo_colour, 38, 255, 43, 0\n" + "lgeo_colour, 40, 166, 145, 130\n" + "lgeo_colour, 41, 170, 229, 255\n" + "lgeo_colour, 42, 198, 255, 0\n" + "lgeo_colour, 43, 193, 223, 240\n" + "lgeo_colour, 44, 150, 112, 159\n" + "lgeo_colour, 46, 247, 241, 141\n" + "lgeo_colour, 47, 252, 252, 252\n" + "lgeo_colour, 52, 156, 149, 199\n" + "lgeo_colour, 54, 255, 246, 123\n" + "lgeo_colour, 57, 226, 176, 96\n" + "lgeo_colour, 65, 236, 201, 53\n" + "lgeo_colour, 66, 202, 176, 0\n" + "lgeo_colour, 67, 255, 255, 255\n" + "lgeo_colour, 68, 243, 207, 155\n" + "lgeo_colour, 69, 142, 66, 133\n" + "lgeo_colour, 70, 105, 64, 39\n" + "lgeo_colour, 71, 163, 162, 164\n" + "lgeo_colour, 72, 99, 95, 97\n" + "lgeo_colour, 73, 110, 153, 201\n" + "lgeo_colour, 74, 161, 196, 139\n" + "lgeo_colour, 77, 220, 144, 149\n" + "lgeo_colour, 78, 246, 215, 179\n" + "lgeo_colour, 79, 255, 255, 255\n" + "lgeo_colour, 80, 140, 140, 140\n" + "lgeo_colour, 82, 219, 172, 52\n" + "lgeo_colour, 84, 170, 125, 85\n" + "lgeo_colour, 85, 52, 43, 117\n" + "lgeo_colour, 86, 124, 92, 69\n" + "lgeo_colour, 89, 155, 178, 239\n" + "lgeo_colour, 92, 204, 142, 104\n" + "lgeo_colour, 100, 238, 196, 182\n" + "lgeo_colour, 115, 199, 210, 60\n" + "lgeo_colour, 134, 174, 122, 89\n" + "lgeo_colour, 135, 171, 173, 172\n" + "lgeo_colour, 137, 106, 122, 150\n" + "lgeo_colour, 142, 220, 188, 129\n" + "lgeo_colour, 148, 62, 60, 57\n" + "lgeo_colour, 151, 14, 94, 77\n" + "lgeo_colour, 179, 160, 160, 160\n" + "lgeo_colour, 183, 242, 243, 242\n" + "lgeo_colour, 191, 248, 187, 61\n" + "lgeo_colour, 212, 159, 195, 233\n" + "lgeo_colour, 216, 143, 76, 42\n" + "lgeo_colour, 226, 253, 234, 140\n" + "lgeo_colour, 232, 125, 187, 221\n" + "lgeo_colour, 256, 33, 33, 33\n" + "lgeo_colour, 272, 32, 58, 86\n" + "lgeo_colour, 273, 13, 105, 171\n" + "lgeo_colour, 288, 39, 70, 44\n" + "lgeo_colour, 294, 189, 198, 173\n" + "lgeo_colour, 297, 170, 127, 46\n" + "lgeo_colour, 308, 53, 33, 0\n" + "lgeo_colour, 313, 171, 217, 255\n" + "lgeo_colour, 320, 123, 46, 47\n" + "lgeo_colour, 321, 70, 155, 195\n" + "lgeo_colour, 322, 104, 195, 226\n" + "lgeo_colour, 323, 211, 242, 234\n" + "lgeo_colour, 324, 196, 0, 38\n" + "lgeo_colour, 326, 226, 249, 154\n" + "lgeo_colour, 330, 119, 119, 78\n" + "lgeo_colour, 334, 187, 165, 61\n" + "lgeo_colour, 335, 149, 121, 118\n" + "lgeo_colour, 366, 209, 131, 4\n" + "lgeo_colour, 373, 135, 124, 144\n" + "lgeo_colour, 375, 193, 194, 193\n" + "lgeo_colour, 378, 120, 144, 129\n" + "lgeo_colour, 379, 94, 116, 140\n" + "lgeo_colour, 383, 224, 224, 224\n" + "lgeo_colour, 406, 0, 29, 104\n" + "lgeo_colour, 449, 129, 0, 123\n" + "lgeo_colour, 450, 203, 132, 66\n" + "lgeo_colour, 462, 226, 155, 63\n" + "lgeo_colour, 484, 160, 95, 52\n" + "lgeo_colour, 490, 215, 240, 0\n" + "lgeo_colour, 493, 101, 103, 97\n" + "lgeo_colour, 494, 208, 208, 208\n" + "lgeo_colour, 496, 163, 162, 164\n" + "lgeo_colour, 503, 199, 193, 183\n" + "lgeo_colour, 504, 137, 135, 136\n" + "lgeo_colour, 511, 250, 250, 250\n" + }; + + /* + # SLOPED BRICKS + # Dictionary with part number (without any extension for decorations), as key, + # of pieces that have grainy slopes, and, as values, a set containing the angles (in + # degrees) of the face's normal to the horizontal plane. Use a | delimited tuple to + # represent a range within which the angle must lie. + + # Item-------- PartID- Angle/Angle Range (in degrees) + */ + const char DefaultSlopedBricks[] = + { + "sloped_brick, 962, 45\n" + "sloped_brick, 2341, -45\n" + "sloped_brick, 2449, -16\n" + "sloped_brick, 2875, 45\n" + "sloped_brick, 2876, 40|63\n" + "sloped_brick, 3037, 45\n" + "sloped_brick, 3038, 45\n" + "sloped_brick, 3039, 45\n" + "sloped_brick, 3040, 45\n" + "sloped_brick, 3041, 45\n" + "sloped_brick, 3042, 45\n" + "sloped_brick, 3043, 45\n" + "sloped_brick, 3044, 45\n" + "sloped_brick, 3045, 45\n" + "sloped_brick, 3046, 45\n" + "sloped_brick, 3048, 45\n" + "sloped_brick, 3049, 45\n" + "sloped_brick, 3135, 45\n" + "sloped_brick, 3297, 63\n" + "sloped_brick, 3298, 63\n" + "sloped_brick, 3299, 63\n" + "sloped_brick, 3300, 63\n" + "sloped_brick, 3660, -45\n" + "sloped_brick, 3665, -45\n" + "sloped_brick, 3675, 63\n" + "sloped_brick, 3676, -45\n" + "sloped_brick, 3678b, 24\n" + "sloped_brick, 3684, 15\n" + "sloped_brick, 3685, 16\n" + "sloped_brick, 3688, 15\n" + "sloped_brick, 3747, -63\n" + "sloped_brick, 4089, -63\n" + "sloped_brick, 4161, 63\n" + "sloped_brick, 4286, 63\n" + "sloped_brick, 4287, -63\n" + "sloped_brick, 4445, 45\n" + "sloped_brick, 4460, 16\n" + "sloped_brick, 4509, 63\n" + "sloped_brick, 4854, -45\n" + "sloped_brick, 4856, -60|-70, -45\n" + "sloped_brick, 4857, 45\n" + "sloped_brick, 4858, 72\n" + "sloped_brick, 4861, 45, 63\n" + "sloped_brick, 4871, -45\n" + "sloped_brick, 4885, 72\n" + "sloped_brick, 6069, 72, 45\n" + "sloped_brick, 6153, 60|70, 26|4\n" + "sloped_brick, 6227, 45\n" + "sloped_brick, 6270, 45\n" + "sloped_brick, 13269, 40|63\n" + "sloped_brick, 13548, 45\n" + "sloped_brick, 15571, 45\n" + "sloped_brick, 18759, -45\n" + "sloped_brick, 22390, 40|55\n" + "sloped_brick, 22391, 40|55\n" + "sloped_brick, 22889, -45\n" + "sloped_brick, 28192, 45\n" + "sloped_brick, 30180, 47\n" + "sloped_brick, 30182, 45\n" + "sloped_brick, 30183, -45\n" + "sloped_brick, 30249, 35\n" + "sloped_brick, 30283, -45\n" + "sloped_brick, 30363, 72\n" + "sloped_brick, 30373, -24\n" + "sloped_brick, 30382, 11, 45\n" + "sloped_brick, 30390, -45\n" + "sloped_brick, 30499, 16\n" + "sloped_brick, 32083, 45\n" + "sloped_brick, 43708, 72\n" + "sloped_brick, 43710, 72, 45\n" + "sloped_brick, 43711, 72, 45\n" + "sloped_brick, 47759, 40|63\n" + "sloped_brick, 52501, -45\n" + "sloped_brick, 60219, -45\n" + "sloped_brick, 60477, 72\n" + "sloped_brick, 60481, 24\n" + "sloped_brick, 63341, 45\n" + "sloped_brick, 72454, -45\n" + "sloped_brick, 92946, 45\n" + "sloped_brick, 93348, 72\n" + "sloped_brick, 95188, 65\n" + "sloped_brick, 99301, 63\n" + "sloped_brick, 303923, 45\n" + "sloped_brick, 303926, 45\n" + "sloped_brick, 304826, 45\n" + "sloped_brick, 329826, 64\n" + "sloped_brick, 374726, -64\n" + "sloped_brick, 428621, 64\n" + "sloped_brick, 4162628, 17\n" + "sloped_brick, 4195004, 45\n" + }; + + /* + # LIGHTED BRICKS + # Dictionary with part number (with extension), as key, + # of lighted bricks, light emission colour and intensity, as values. + + # Item--------- PartID--- Light-------------- Intensity + */ + const char DefaultLightedBricks[] = + { + "lighted_brick, 62930.dat, 1.000, 0.373, 0.059, 1.0\n" + "lighted_brick, 54869.dat, 1.000, 0.052, 0.017, 1.0\n" + }; + + Buffer.clear(); + if (Which == PARAMS_CUSTOM_COLOURS) + Buffer.append(DefaultCustomColours, sizeof(DefaultCustomColours)); + else if (Which == PARAMS_SLOPED_BRICKS) + Buffer.append(DefaultSlopedBricks, sizeof(DefaultSlopedBricks)); + else if (Which == PARAMS_LIGHTED_BRICKS) + Buffer.append(DefaultLightedBricks, sizeof(DefaultLightedBricks)); +} + +bool lcBlenderPreferences::ExportParameterFile() +{ + QString const BlenderConfigDir = QString("%1/Blender/setup/addon_setup/config").arg(gAddonPreferences->mDataDir); + QString const ParameterFile = QString("%1/%2").arg(BlenderConfigDir).arg(LC_BLENDER_ADDON_PARAMS_FILE); + QFile File(ParameterFile); + + if (!OverwriteFile(File.fileName())) + return true; + + QString Message; + if(File.open(QIODevice::WriteOnly | QIODevice::Text)) + { + int Counter = 1; + QByteArray Buffer; + QTextStream Stream(&File); + Stream << "# File: " << QFileInfo(ParameterFile).fileName() << LineEnding; + Stream << "" << LineEnding; + Stream << "# This config file captures parameters for the Blender LDraw Render addon" << LineEnding; + Stream << "# Parameters must be prefixed using one of the following predefined" << LineEnding; + Stream << "# 'Item' labels:" << LineEnding; + Stream << "# - lgeo_colour" << LineEnding; + Stream << "# - sloped_brick" << LineEnding; + Stream << "# - light_brick" << LineEnding; + Stream << "" << LineEnding; + Stream << "# All items must use a comma',' delimiter as the primary delimiter." << LineEnding; + Stream << "# For sloped_brick items, a pipe'|' delimiter must be used to specify" << LineEnding; + Stream << "# a range (min|max) within which the angle must lie when appropriate." << LineEnding; + Stream << "# Spaces between item attributes are not required and are used to" << LineEnding; + Stream << "# facilitate human readability." << LineEnding; + Stream << "" << LineEnding; + Stream << "" << LineEnding; + Stream << "# LGEO CUSTOM COLOURS" << LineEnding; + Stream << "# LGEO is a parts library for rendering LEGO using the POV-Ray" << LineEnding; + Stream << "# rendering software. This is the list of LEGO colours suitable" << LineEnding; + Stream << "# for realistic rendering extracted from the LGEO file 'lg_color.inc'." << LineEnding; + Stream << "# When the 'Colour Scheme' option is set to 'Realistic', the standard" << LineEnding; + Stream << "# LDraw colours RGB value is overwritten with the values defined here." << LineEnding; + Stream << "# Note: You can customize these RGB values as you want." << LineEnding; + Stream << "" << LineEnding; + Stream << "# Item----- ID- R-- G-- B--" << LineEnding; + + LoadDefaultParameters(Buffer, PARAMS_CUSTOM_COLOURS); + QTextStream colourstream(Buffer); + for (QString Line = colourstream.readLine(); !Line.isNull(); Line = colourstream.readLine()) + { + Stream << Line << LineEnding; + Counter++; + } + + Stream << "" << LineEnding; + Stream << "# SLOPED BRICKS" << LineEnding; + Stream << "# Dictionary with part number (without any extension for decorations), as key," << LineEnding; + Stream << "# of pieces that have grainy slopes, and, as values, a set containing the angles (in" << LineEnding; + Stream << "# degrees) of the face's normal to the horizontal plane. Use a | delimited tuple to" << LineEnding; + Stream << "# represent a range within which the angle must lie." << LineEnding; + Stream << "" << LineEnding; + Stream << "# Item------ PartID- Angle/Angle Range (in degrees)" << LineEnding; + + LoadDefaultParameters(Buffer, PARAMS_SLOPED_BRICKS); + QTextStream SlopedStream(Buffer); + for (QString Line = SlopedStream.readLine(); !Line.isNull(); Line = SlopedStream.readLine()) + { + Stream << Line << LineEnding; + Counter++; + } + + Stream << "" << LineEnding; + Stream << "# LIGHTED BRICKS" << LineEnding; + Stream << "# Dictionary with part number (with extension), as key," << LineEnding; + Stream << "# of lighted bricks, light emission colour and intensity, as values." << LineEnding; + Stream << "" << LineEnding; + Stream << "# Item------- PartID--- Light-------------- Intensity" << LineEnding; + + LoadDefaultParameters(Buffer, PARAMS_LIGHTED_BRICKS); + QTextStream lightedstream(Buffer); + for (QString Line = lightedstream.readLine(); !Line.isNull(); Line = lightedstream.readLine()) + { + Stream << Line << LineEnding; + Counter++; + } + + Stream << "" << LineEnding; + Stream << "# end of parameters" << LineEnding; + + File.close(); + Message = tr("Finished writing Blender parameter entries. Processed %1 lines in file [%2].") + .arg(Counter) + .arg(File.fileName()); + } + else + { + Message = tr("Failed to open Blender parameter file: %1:
%2") + .arg(File.fileName()) + .arg(File.errorString()); + ShowMessage(Message); + return false; + } + return true; +} + +bool lcBlenderPreferences::OverwriteFile(const QString& File) +{ + QFileInfo fileInfo(File); + + if (!fileInfo.exists()) + return true; + + QString const& Title = tr ("Replace Existing File"); + QString const Header = "" + QMessageBox::tr ("Existing file %1 detected.").arg(fileInfo.fileName()) + ""; + QString const Body = QMessageBox::tr ("\"%1\"
This file already exists.
Replace existing file?").arg(fileInfo.fileName()); + int Exec = ShowMessage(Header, Title, Body, QString(), MBB_YES, QMessageBox::NoIcon); + + return (Exec == QMessageBox::Yes); +} + +int lcBlenderPreferences::ShowMessage(QString const& Header, QString const& Title, QString const& Body, const QString& Detail, int const Buttons, int const Icon) +{ + if (!gMainWindow) + return QMessageBox::Ok; + + QMessageBox Box; + Box.setWindowIcon(QIcon()); + if (!Icon) + { + QPixmap Pixmap = QPixmap(":/resources/leocad.png"); + Box.setIconPixmap (Pixmap); + } + else + Box.setIcon (static_cast(Icon)); + + Box.setTextFormat (Qt::RichText); + Box.setWindowFlags (Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + if (!Title.isEmpty()) + Box.setWindowTitle(Title); + else + Box.setWindowTitle(tr("%1 Blender LDraw Addon").arg(LC_PRODUCTNAME_STR)); + + Box.setText (Header); + if (!Body.isEmpty()) + Box.setInformativeText (Body); + + if (!Detail.isEmpty()) + Box.setDetailedText(QString(Detail).replace("
", "\n")); + + switch (Buttons) + { + case MBB_YES: + Box.setStandardButtons (QMessageBox::Yes | QMessageBox::Cancel); + Box.setDefaultButton (QMessageBox::Yes); + break; + case MBB_YES_NO: + Box.setStandardButtons (QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + Box.setDefaultButton (QMessageBox::Yes); + break; + default: + Box.setStandardButtons (QMessageBox::Ok); + break; + } + + int MinimumWidth = 400; + int FontWidth = QFontMetrics(Box.font()).averageCharWidth(); + int FixedTextLength = (MinimumWidth / FontWidth); + if (Header.length() < Body.length() && Header.length() < FixedTextLength) + { + QGridLayout* BoxLayout = (QGridLayout*)Box.layout(); + QLayoutItem* BoxLayoutItem = BoxLayout->itemAtPosition(0, 2); + QWidget* TextWidget = BoxLayoutItem->widget(); + if (TextWidget) + { + int FixedWidth = Body.length() * FontWidth; + if (FixedWidth == MinimumWidth) + { + int Index = (MinimumWidth / FontWidth) - 1; + if (!Body.midRef(Index,1).isEmpty()) + FixedWidth = Body.indexOf(" ", Index); + } + else if (FixedWidth < MinimumWidth) + FixedWidth = MinimumWidth; + TextWidget->setFixedWidth(FixedWidth); + } + } + + return Box.exec(); +} + +// TODO - Refactor, use LeoCAD mHttpReply/HttpManager +void lcBlenderPreferences::DownloadFile(QString URL, QString Title, bool PromptRedirect, bool ShowProgress) +{ + gAddonPreferences->mTitle = Title; + gAddonPreferences->mShowProgress = ShowProgress; + gAddonPreferences->mPromptRedirect = PromptRedirect; + + if (gAddonPreferences->mShowProgress) + { + gAddonPreferences->mProgressDialog = new QProgressDialog(nullptr); + connect(gAddonPreferences->mProgressDialog, SIGNAL(canceled()), gAddonPreferences, SLOT(cancelDownload())); + + gAddonPreferences->mProgressDialog->setWindowTitle(tr("Downloading")); + gAddonPreferences->mProgressDialog->setLabelText(tr("Downloading %1").arg(gAddonPreferences->mTitle)); + gAddonPreferences->mProgressDialog->show(); + } + + gAddonPreferences->mHttpManager = new QNetworkAccessManager(gAddonPreferences); + + gAddonPreferences->mUrl = URL; + + gAddonPreferences->mHttpRequestAborted = false; + + gAddonPreferences->StartRequest(gAddonPreferences->mUrl); + + while (gAddonPreferences->mHttpReply) + QApplication::processEvents(); +} + +QByteArray lcBlenderPreferences::GetDownloadedFile() +{ + return gAddonPreferences->mByteArray; +} + +void lcBlenderPreferences::StartRequest(QUrl Url) +{ + QNetworkRequest Request(Url); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + if (!mPromptRedirect) + Request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, true); +#else + if (!mPromptRedirect) + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); +#endif +#endif + mHttpReply = mHttpManager->get(Request); + + connect(mHttpReply, SIGNAL(downloadProgress(qint64,qint64)), + this, SLOT(UpdateDownloadProgress(qint64,qint64))); + + connect(mHttpReply, SIGNAL(finished()), + this, SLOT(HttpDownloadFinished())); +} + +void lcBlenderPreferences::HttpDownloadFinished() +{ + if (mHttpRequestAborted) + { + mByteArray.clear(); + mHttpReply->deleteLater(); + mHttpReply = nullptr; + if (mShowProgress) + mProgressDialog->close(); + return; + } + + QString Message; + QString const MessageTitle = tr("%1 Download").arg(LC_PRODUCTNAME_STR); + QVariant RedirectionTarget = mHttpReply->attribute(QNetworkRequest::RedirectionTargetAttribute); + if (mHttpReply->error()) + { + mByteArray.clear(); + Message = tr("%1 download failed: %2.") + .arg(mTitle).arg(mHttpReply->errorString()); + + if (Message.endsWith("not found.")) + Message.append(tr("
Your internet connection may be interrupted.")); + + if (gMainWindow) + ShowMessage(Message, MessageTitle, QString(), QString(), MBB_OK, QMessageBox::Warning); + + } + else if (!RedirectionTarget.isNull()) + { + // This block should only trigger for redirects when Qt + // is less than 5.6.0 or if prompt redirect set to True + QUrl NewUrl = mUrl.resolved(RedirectionTarget.toUrl()); + bool ProceedToRedirect = true; + + if (mPromptRedirect && gMainWindow) + ProceedToRedirect = ShowMessage(tr("Download redirect to %1 ?").arg(NewUrl.toString()), + MessageTitle, QString(), QString(), MBB_YES_NO, QMessageBox::Question) == QMessageBox::Yes; + + if (ProceedToRedirect) + { + mUrl = NewUrl; + mHttpReply->deleteLater(); + mByteArray.resize(0); + StartRequest(mUrl); + return; + } + } + else + mByteArray = mHttpReply->readAll(); + + if (mShowProgress) + mProgressDialog->close(); + + mHttpReply->deleteLater(); + mHttpReply = nullptr; + mHttpManager = nullptr; +} + +void lcBlenderPreferences::CancelDownload() +{ + mHttpRequestAborted = true; + mHttpReply->abort(); +} + +void lcBlenderPreferences::UpdateDownloadProgress(qint64 BytesRead, qint64 BotalBytes) +{ + if (mHttpRequestAborted) + return; + + if (mShowProgress) + { + mProgressDialog->setMaximum(int(BotalBytes)); + mProgressDialog->setValue(int(BytesRead)); + } +} diff --git a/common/lc_blenderpreferences.h b/common/lc_blenderpreferences.h new file mode 100644 index 00000000..4d11448d --- /dev/null +++ b/common/lc_blenderpreferences.h @@ -0,0 +1,484 @@ +#pragma once + +#include +#include +#include "qtimer.h" + +#define LC_UNIX_SHELL "/bin/sh" +#define LC_WINDOWS_SHELL "cmd.exe" +#define LC_BLENDER_ADDON_BLEND_FILE "leocad.blend" +#define LC_BLENDER_ADDON_IMPORT_MODULE "TN" +#define LC_BLENDER_ADDON_FOLDER_STR "io_scene_render_ldraw" + +class QFormLayout; +class QGridLayout; +class QLabel; +class QGroupBox; +class QLineEdit; +class QCheckBox; +class QComboBox; +class QPushButton; +class QScrollArea; +class QProgressBar; +class QProcess; +class QTimer; + +class QNetworkAccessManager; +class QNetworkReply; +class lcBlenderPreferences; + +class lcBlenderPreferencesDialog : public QDialog +{ + Q_OBJECT +public: + + explicit lcBlenderPreferencesDialog(int Width, int Height, double Scale, QWidget* Parent = nullptr); + + virtual ~lcBlenderPreferencesDialog(); + + static bool GetBlenderPreferences(int& Width, int& Height, double& Scale, QWidget* Parent = nullptr); + +public slots: + void ShowPathsGroup(); + void ResetSettings(); + void EnableButton(bool); + void accept() override; + void reject() override; + +private: + QPushButton* mApplyButton; + QPushButton* mResetButton; + QPushButton* mPathsButton; + lcBlenderPreferences* mPreferences; +}; + +class lcBlenderPreferences : public QWidget +{ + Q_OBJECT +public: + + explicit lcBlenderPreferences(int Width, int Height, double Scale, QWidget* Parent = nullptr); + + ~lcBlenderPreferences(); + + static void LoadSettings(); + static void SaveSettings(); + static bool ExportParameterFile(); + static bool SettingsModified(bool Update = true, QString const& Module = QString()); + static int ShowMessage(QString const& Header, QString const& Title = QString(), QString const& Body = QString(), QString const& Detail = QString(), const int Buttons = MBB_OK, const int Icon = 3 /*QMessageBox::Critical*/); + + void Apply(const int Response = QDialog::Accepted); + + int mImageWidth; + int mImageHeight; + qreal mScale; + +protected: + static int NumPaths(bool DefaultSettings = false); + static int NumSettings(bool DefaultSettings = false); + static int NumSettingsMM(bool DefaultSettings = false); + static bool GetBlenderAddon(const QString& BlenderDir); + static bool ExtractBlenderAddon(const QString& BlenderDir); + static void LoadDefaultParameters(QByteArray& Buffer, int Which); + static bool OverwriteFile(const QString& File); + + // TODO - Refactor, use LeoCAD mHttpReply/HttpManager + static void DownloadFile(QString URL, QString Title, bool PromptRedirect = false, bool ShowProgress = true); + void StartRequest(QUrl Url); + static QByteArray GetDownloadedFile(); + + QString ReadStdErr(bool& Error) const; + void ClearGroupBox(QGroupBox* GroupBox); + void InitPathsAndSettings(); + void InitPathsAndSettingsMM(); + +signals: + void SettingChangedSig(bool); + +public slots: + void ResetSettings(); + void ShowPathsGroup(); + bool PromptCancel(); + +private slots: + void BrowseBlender(bool); + void ConfigureBlenderAddon(bool = false, bool = false, bool = false); + void UpdateBlenderAddon(); + void EnableImportModule(); + void SizeChanged(const QString&); + void SetDefaultColor(int); + void ColorButtonClicked(bool); + void SetModelSize(bool = false); + void ValidateColourScheme(int); + void SettingChanged(int = -1); + void SettingChanged(const QString&); + void PathChanged(); + bool PromptAccept(); + void Update(); + void ReadStdOut(); + void ReadStdOut(const QString&, QString&); + void WriteStdOut(); + void GetStandardOutput(); + void ShowResult(); + void StatusUpdate(bool, bool = true, const QString& = QString()); + + void HttpDownloadFinished(); + void CancelDownload(); + void UpdateDownloadProgress(qint64, qint64); + void FileLoaded(bool b) + { + mFileLoaded = true; + mFileLoadFail = !b; + } + +private: + enum BlenderPathType + { + PATH_BLENDER, // 0 QLineEdit/QPushButton + PATH_BLENDFILE, // 1 QLineEdit/QPushButton + PATH_ENVIRONMENT, // 2 QLineEdit/QPushButton + PATH_LDCONFIG, // 3 QLineEdit/QPushButton + PATH_LDRAW, // 4 QLineEdit/QPushButton + PATH_LSYNTH, // 5 QLineEdit/QPushButton + PATH_STUD_LOGO, // 6 QLineEdit/QPushButton + PATH_STUDIO_LDRAW, // 7 QLineEdit/QPushButton + + NUM_PATHS + }; + + enum BlenderLabelType + { + LBL_ADD_ENVIRONMENT, // 0 QCheckBox + DEFAULT_SETTINGS, // 1 + LBL_ADD_GAPS = DEFAULT_SETTINGS, // 1 QCheckBox + LBL_BEVEL_EDGES, // 2 QCheckBox + LBL_BLENDFILE_TRUSTED, // 3 QCheckBox + LBL_CROP_IMAGE, // 4 QCheckBox + LBL_CURVED_WALLS, // 5 QCheckBox + LBL_FLATTEN_HIERARCHY, // 6 QCheckBox + NUM_COMBO_ITEMS, // 7 + LBL_IMPORT_CAMERAS = NUM_COMBO_ITEMS, // 7 QCheckBox + LBL_IMPORT_LIGHTS, // 8 QCheckBox + LBL_INSTANCE_STUDS, // 9 QCheckBox + LBL_KEEP_ASPECT_RATIO, // 10 QCheckBox + LBL_LINK_PARTS, // 11 QCheckBox + LBL_MINIFIG_HIERARCHY, // 12 QCheckBox + LBL_NUMBER_NODES, // 13 QCheckBox + LBL_OVERWRITE_IMAGE, // 14 QCheckBox + LBL_OVERWRITE_MATERIALS, // 15 QCheckBox + LBL_OVERWRITE_MESHES, // 16 QCheckBox + LBL_POSITION_CAMERA, // 17 QCheckBox + LBL_REMOVE_DOUBLES, // 18 QCheckBox + LBL_RENDER_WINDOW, // 19 QCheckBox + LBL_USE_ARCHIVE_LIBS, // 20 QChekcBox + LBL_SEARCH_ADDL_PATHS, // 21 QCheckBox + LBL_SMOOTH_SHADING, // 22 QCheckBox + LBL_TRANSPARENT_BACKGROUND, // 23 QCheckBox + LBL_UNOFFICIAL_PARTS, // 24 QCheckBox + LBL_USE_LOGO_STUDS, // 25 QCheckBox + LBL_VERBOSE, // 26 QCheckBox + + LBL_BEVEL_WIDTH, // 27/0 QLineEdit + LBL_CAMERA_BORDER_PERCENT, // 28/1 QLineEdit + LBL_DEFAULT_COLOUR, // 29/2 QLineEdit + LBL_GAPS_SIZE, // 30/3 QLineEdit + LBL_IMAGE_WIDTH, // 31/4 QLineEdit + LBL_IMAGE_HEIGHT, // 32/5 QLineEdit + LBL_IMAGE_SCALE, // 33/6 QLineEdit + LBL_RENDER_PERCENTAGE, // 34/7 QLineEdit + + LBL_COLOUR_SCHEME, // 35/0 QComboBox + LBL_FLEX_PARTS_SOURCE, // 36/1 QComboBox + LBL_LOGO_STUD_VERSION, // 27/2 QComboBox + LBL_LOOK, // 38/3 QComboBox + LBL_POSITION_OBJECT, // 39/4 QComboBox + LBL_RESOLUTION, // 40/5 QComboBox + LBL_RESOLVE_NORMALS, // 41/6 QComboBox + + NUM_SETTINGS + }; + + enum BlenderControlType + { + CTL_BLENDER_PATH_EDIT, // 0 + CTL_ADD_ENVIRONMENT_BOX = CTL_BLENDER_PATH_EDIT, // 0 + CTL_BEVEL_WIDTH_EDIT = CTL_BLENDER_PATH_EDIT, // 0 + CTL_COLOUR_SCHEME_COMBO = CTL_BLENDER_PATH_EDIT, // 0 + CTL_ADD_GAPS_BOX, // 1 + CTL_CAMERA_BORDER_PERCENT_EDIT = CTL_ADD_GAPS_BOX, // 1 + CTL_FLEX_PARTS_SOURCE_COMBO = CTL_ADD_GAPS_BOX, // 1 + CTL_BEVEL_EDGES_BOX, // 2 + CTL_DEFAULT_COLOUR_EDIT = CTL_BEVEL_EDGES_BOX, // 2 + CTL_LOGO_STUD_VERSION_COMBO = CTL_BEVEL_EDGES_BOX, // 2 + CTL_BLENDFILE_TRUSTED_BOX, // 3 + CTL_GAPS_SIZE_EDIT = CTL_BLENDFILE_TRUSTED_BOX, // 3 + CTL_LOOK_COMBO = CTL_BLENDFILE_TRUSTED_BOX, // 3 + CTL_CROP_IMAGE_BOX, // 4 + CTL_IMAGE_WIDTH_EDIT = CTL_CROP_IMAGE_BOX, // 4 + CTL_POSITION_OBJECT_COMBO = CTL_CROP_IMAGE_BOX, // 4 + CTL_CURVED_WALLS_BOX, // 5 + CTL_IMAGE_HEIGHT_EDIT = CTL_CURVED_WALLS_BOX, // 5 + CTL_RESOLUTION_COMBO = CTL_CURVED_WALLS_BOX, // 5 + CTL_FLATTEN_HIERARCHY_BOX, // 6 + CTL_IMAGE_SCALE_EDIT = CTL_FLATTEN_HIERARCHY_BOX, // 6 + CTL_RESOLVE_NORMALS_COMBO = CTL_FLATTEN_HIERARCHY_BOX, // 6 + CTL_IMPORT_CAMERAS_BOX, // 7 + CTL_RENDER_PERCENTAGE_EDIT = CTL_IMPORT_CAMERAS_BOX, // 7 + CTL_IMPORT_LIGHTS_BOX, // 8 + CTL_INSTANCE_STUDS_BOX, // 9 + CTL_KEEP_ASPECT_RATIO_BOX, // 10 + CTL_LINK_PARTS_BOX, // 11 + CTL_MINIFIG_HIERARCHY_BOX, // 12 + CTL_NUMBER_NODES_BOX, // 13 + CTL_OVERWRITE_IMAGE_BOX, // 14 + CTL_OVERWRITE_MATERIALS_BOX, // 15 + CTL_OVERWRITE_MESHES_BOX, // 16 + CTL_POSITION_CAMERA_BOX, // 17 + CTL_REMOVE_DOUBLES_BOX, // 18 + CTL_RENDER_WINDOW_BOX, // 19 + CTL_USE_ARCHIVE_LIBS_BOX, // 20 + CTL_SEARCH_ADDL_PATHS_BOX, // 21 + CTL_SMOOTH_SHADING_BOX, // 22 + CTL_TRANSPARENT_BACKGROUND_BOX, // 23 + CTL_UNOFFICIAL_PARTS_BOX, // 24 + CTL_USE_LOGO_STUDS_BOX, // 25 + CTL_VERBOSE_BOX, // 26 + }; + + enum BlenderLabelTypeMM + { + LBL_ADD_ENVIRONMENT_MM, // 0 QCheckBox + LBL_BEVEL_EDGES_MM, // 1 QCheckBox + LBL_BLEND_FILE_TRUSTED_MM, // 2 QCheckBox + LBL_CROP_IMAGE_MM, // 3 QCheckBox + LBL_DISPLAY_LOGO, // 4 QCheckBox + LBL_IMPORT_CAMERAS_MM, // 5 QCheckBox + LBL_IMPORT_EDGES, // 6 QCheckBox + NUM_COMBO_ITEMS_MM, // 7 + LBL_IMPORT_LIGHTS_MM = NUM_COMBO_ITEMS_MM, // 7 QCheckBox + LBL_KEEP_ASPECT_RATIO_MM, // 8 QCheckBox + LBL_MAKE_GAPS, // 9 QCheckBox + LBL_META_BFC, // 10 QCheckBox + LBL_META_CLEAR, // 11 QCheckBox + LBL_META_GROUP, // 12 QCheckBox + LBL_META_PAUSE, // 13 QCheckBox + LBL_META_PRINT_WRITE, // 14 QCheckBox + LBL_META_SAVE, // 15 QCheckBox + LBL_META_STEP, // 16 QCheckBox + LBL_META_STEP_GROUPS, // 17 QCheckBox + LBL_META_TEXMAP, // 18 QCheckBox + LBL_NO_STUDS, // 19 QCheckBox + LBL_OVERWRITE_IMAGE_MM, // 20 QCheckBox + LBL_POSITION_CAMERA_MM, // 21 QCheckBox + LBL_PARENT_TO_EMPTY, // 22 QCheckBox + LBL_PREFER_STUDIO, // 23 QCheckBox + LBL_PREFER_UNOFFICIAL, // 24 QCheckBox + LBL_PRESERVE_HIERARCHY, // 25 QCheckBox + LBL_PROFILE, // 26 QCheckBox + LBL_RECALCULATE_NORMALS, // 27 QCheckBox + LBL_REMOVE_DOUBLES_MM, // 28 QCheckBox + LBL_RENDER_WINDOW_MM, // 29 QCheckBox + LBL_SEARCH_ADDL_PATHS_MM, // 30 QCheckBox + LBL_SETEND_FRAME, // 31 QCheckBox + LBL_SET_TIMELINE_MARKERS, // 32 QCheckBox + LBL_SHADE_SMOOTH, // 33 QCheckBox + LBL_TRANSPARENT_BACKGROUND_MM, // 34 QCheckBox + LBL_TREAT_MODELS_WITH_SUBPARTS_AS_PARTS, // 35 QCheckBox + LBL_TREAT_SHORTCUT_AS_MODEL, // 36 QCheckBox + LBL_TRIANGULATE, // 37 QCheckBox + LBL_USE_ARCHIVE_LIBRARY_MM, // 38 QCheckBox + LBL_USE_FREESTYLE_EDGES, // 39 QCheckBox + LBL_VERBOSE_MM, // 40 QCheckBox + + LBL_BEVEL_SEGMENTS, // 41/ 0 QLineEdit + LBL_BEVEL_WEIGHT, // 42/ 1 QLineEdit + LBL_BEVEL_WIDTH_MM, // 43/ 2 QLineEdit + LBL_CAMERA_BORDER_PERCENT_MM, // 44/ 3 QLineEdit + LBL_FRAMES_PER_STEP, // 45/ 4 QLineEdit + LBL_GAP_SCALE, // 46/ 5 QLineEdit + LBL_IMPORT_SCALE, // 47/ 6 QLineEdit + LBL_MERGE_DISTANCE, // 48/ 7 QLineEdit + LBL_RENDER_PERCENTAGE_MM, // 49/ 8 QLineEdit + LBL_RESOLUTION_WIDTH, // 50/ 9 QLineEdit + LBL_RESOLUTION_HEIGHT, // 51/10 QLineEdit + LBL_STARTING_STEP_FRAME, // 52/11 QLineEdit + + LBL_CHOSEN_LOGO, // 53/ 0 QComboBox + LBL_COLOUR_SCHEME_MM, // 54/ 1 QComboBox + LBL_GAP_SCALE_STRATEGY, // 55/ 2 QComboBox + LBL_GAP_TARGET, // 56/ 3 QComboBox + LBL_RESOLUTION_MM, // 57/ 4 QComboBox + LBL_SMOOTH_TYPE, // 58/ 5 QComboBox + + NUM_SETTINGS_MM + }; + + enum BlenderControlTypeMM + { + CTL_BLENDER_PATH_EDIT_MM, // 0 + CTL_ADD_ENVIRONMENT_BOX_MM = CTL_BLENDER_PATH_EDIT_MM, // 0 + CTL_BEVEL_SEGMENTS_EDIT = CTL_BLENDER_PATH_EDIT_MM, // 0 + CTL_CHOSEN_LOGO_COMBO = CTL_BLENDER_PATH_EDIT_MM, // 0 + CTL_BEVEL_EDGES_BOX_MM, // 1 + CTL_BEVEL_WEIGHT_EDIT = CTL_BEVEL_EDGES_BOX_MM, // 1 + CTL_COLOUR_SCHEME_COMBO_MM = CTL_BEVEL_EDGES_BOX_MM, // 1 + CTL_BLEND_FILE_TRUSTED_BOX_MM, // 2 + CTL_BEVEL_WIDTH_EDIT_MM = CTL_BLEND_FILE_TRUSTED_BOX_MM,// 2 + CTL_GAP_SCALE_STRATEGY_COMBO = CTL_BLEND_FILE_TRUSTED_BOX_MM,// 2 + CTL_CROP_IMAGE_BOX_MM, // 3 + CTL_CAMERA_BORDER_PERCENT_EDIT_MM = CTL_CROP_IMAGE_BOX_MM, // 3 + CTL_GAP_TARGET_COMBO = CTL_CROP_IMAGE_BOX_MM, // 3 + CTL_DISPLAY_LOGO_BOX, // 4 + CTL_FRAMES_PER_STEP_EDIT = CTL_DISPLAY_LOGO_BOX, // 4 + CTL_RESOLUTION_COMBO_MM = CTL_DISPLAY_LOGO_BOX, // 4 + CTL_IMPORT_CAMERAS_BOX_MM, // 5 + CTL_GAP_SCALE_EDIT = CTL_IMPORT_CAMERAS_BOX_MM, // 5 + CTL_SMOOTH_TYPE_COMBO = CTL_IMPORT_CAMERAS_BOX_MM, // 5 + CTL_IMPORT_EDGES_BOX, // 6 + CTL_IMPORT_SCALE_EDIT = CTL_IMPORT_EDGES_BOX, // 6 + CTL_IMPORT_LIGHTS_BOX_MM, // 7 + CTL_MERGE_DISTANCE_EDIT = CTL_IMPORT_LIGHTS_BOX_MM, // 7 + CTL_KEEP_ASPECT_RATIO_BOX_MM, // 8 + CTL_RENDER_PERCENTAGE_EDIT_MM = CTL_KEEP_ASPECT_RATIO_BOX_MM, // 8 + CTL_MAKE_GAPS_BOX, // 9 + CTL_RESOLUTION_WIDTH_EDIT = CTL_MAKE_GAPS_BOX, // 9 + CTL_META_BFC_BOX, // 10 + CTL_RESOLUTION_HEIGHT_EDIT = CTL_META_BFC_BOX, // 10 + CTL_META_CLEAR_BOX, // 11 + CTL_STARTING_STEP_FRAME_EDIT = CTL_META_CLEAR_BOX, // 11 + CTL_META_GROUP_BOX, // 12 + CTL_META_PAUSE_BOX, // 13 + CTL_META_PRINT_WRITE_BOX, // 14 + CTL_META_SAVE_BOX, // 15 + CTL_META_STEP_BOX, // 16 + CTL_META_STEP_GROUPS_BOX, // 17 + CTL_META_TEXMAP_BOX, // 18 + CTL_NO_STUDS_BOX, // 19 + CTL_POSITION_CAMERA_BOX_MM, // 10 + CTL_OVERWRITE_IMAGE_BOX_MM, // 21 + CTL_PARENT_TO_EMPTY_BOX, // 22 + CTL_PREFER_STUDIO_BOX, // 23 + CTL_PREFER_UNOFFICIAL_BOX, // 24 + CTL_PRESERVE_HIERARCHY_BOX, // 25 + CTL_PROFILE_BOX, // 26 + CTL_RECALCULATE_NORMALS_BOX, // 27 + CTL_REMOVE_DOUBLES_BOX_MM, // 28 + CTL_RENDER_WINDOW_BOX_MM, // 29 + CTL_SEARCH_ADDL_PATHS_MM_BOX, // 20 + CTL_SETEND_FRAME_BOX, // 31 + CTL_SET_TIMELINE_MARKERS_BOX, // 32 + CTL_SHADE_SMOOTH_BOX, // 33 + CTL_TRANSPARENT_BACKGROUND_BOX_MM, // 34 + CTL_TREAT_MODELS_WITH_SUBPARTS_AS_PARTS_BOX, // 35 + CTL_TREAT_SHORTCUT_AS_MODEL_BOX, // 36 + CTL_TRIANGULATE_BOX, // 37 + CTL_USE_ARCHIVE_LIBRARY_BOX_MM, // 38 + CTL_USE_FREESTYLE_EDGES_BOX, // 39 + CTL_VERBOSE_BOX_MM // 40 + }; + + enum BlenderBrickType + { + PARAMS_CUSTOM_COLOURS, + PARAMS_SLOPED_BRICKS, + PARAMS_LIGHTED_BRICKS + }; + + enum MessageBoxButtonsType + { + MBB_OK, + MBB_YES, + MBB_YES_NO + }; + + struct BlenderSettings + { + QString key; + QString value; + QString label; + QString tooltip; + }; + + struct BlenderPaths + { + QString key; + QString key_mm; + QString value; + QString label; + QString tooltip; + }; + + struct ComboItems + { + QString dataList; + QString itemList; + }; + + static BlenderPaths mBlenderPaths[]; + static BlenderPaths mDefaultPaths[]; + static BlenderSettings mBlenderSettings[]; + static BlenderSettings mBlenderSettingsMM[]; + static BlenderSettings mDefaultSettings[]; + static BlenderSettings mDefaultSettingsMM[]; + static ComboItems mComboItems[]; + static ComboItems mComboItemsMM[]; + + QList mPathLineEditList; + QList mPathBrowseButtonList; + QList mSettingLabelList; + QList mCheckBoxList; + QList mLineEditList; + QList mComboBoxList; + + QWidget* mContent = nullptr; + + QFormLayout* mForm = nullptr; + QGridLayout* mExeGridLayout = nullptr; + QGridLayout* mAddonGridLayout = nullptr; + QGridLayout* mPathsGridLayout = nullptr; + QFormLayout* mSettingsSubform = nullptr; + + QLineEdit* mBlenderVersionEdit = nullptr; + QLabel* mBlenderVersionLabel = nullptr; + QLineEdit* mAddonVersionEdit = nullptr; + QLabel* mAddonVersionLabel = nullptr; + QGroupBox* mModulesBox = nullptr; + QCheckBox* mImportActBox = nullptr; + QCheckBox* mImportMMActBox = nullptr; + QCheckBox* mRenderActBox = nullptr; + QGroupBox* mSettingsBox = nullptr; + QGroupBox* mPathsBox = nullptr; + + QAction* mDefaultColourEditAction = nullptr; + QPushButton* mAddonUpdateButton = nullptr; + QPushButton* mAddonStdOutButton = nullptr; + QPushButton* mPathsGroupButton = nullptr; + + QProgressBar* mProgressBar = nullptr; + QProcess* mProcess = nullptr; + + // TODO - Refactor, use LeoCAD mHttpReply/HttpManager + QProgressDialog* mProgressDialog = nullptr; + QNetworkAccessManager* mHttpManager = nullptr; + QNetworkReply* mHttpReply = nullptr; + QByteArray mByteArray; + QString mTitle; + QUrl mUrl; + bool mPromptRedirect = false; + bool mHttpRequestAborted = false; + bool mFileLoaded = false; + bool mFileLoadFail = false; + bool mShowProgress = false; + + QTimer mUpdateTimer; + + QStringList mStdOutList; + QString mDataDir; + QString mBlenderVersion; + QString mAddonVersion; + bool mConfigured; + bool mDialogCancelled; + bool mBlenderVersionFound; +}; + +extern class lcBlenderPreferences* gAddonPreferences; diff --git a/leocad.pro b/leocad.pro index 6b588bfe..3ae26763 100644 --- a/leocad.pro +++ b/leocad.pro @@ -175,6 +175,7 @@ SOURCES += \ common/lc_aboutdialog.cpp \ common/lc_application.cpp \ common/lc_arraydialog.cpp \ + common/lc_blenderpreferences.cpp \ common/lc_bricklink.cpp \ common/lc_category.cpp \ common/lc_categorydialog.cpp \ @@ -244,6 +245,7 @@ HEADERS += \ common/lc_application.h \ common/lc_array.h \ common/lc_arraydialog.h \ + common/lc_blenderpreferences.h \ common/lc_bricklink.h \ common/lc_category.h \ common/lc_categorydialog.h \ From c71a4be5f0e6a7c52aa983631088e13e19f05cfb Mon Sep 17 00:00:00 2001 From: Trevor SANDY Date: Fri, 26 May 2023 16:17:16 +0200 Subject: [PATCH 03/14] Blender addon - settings lc_http --- common/lc_blenderpreferences.cpp | 175 ++++++------------------------- common/lc_blenderpreferences.h | 32 +----- 2 files changed, 36 insertions(+), 171 deletions(-) diff --git a/common/lc_blenderpreferences.cpp b/common/lc_blenderpreferences.cpp index 0bf6e660..485992c2 100644 --- a/common/lc_blenderpreferences.cpp +++ b/common/lc_blenderpreferences.cpp @@ -1358,6 +1358,23 @@ bool lcBlenderPreferences::GetBlenderAddon(const QString& BlenderDir) auto GetBlenderAddonVersionMatch = [&]() { + lcHttpManager* HttpManager = new lcHttpManager(gAddonPreferences); + connect(HttpManager, SIGNAL(DownloadFinished(lcHttpReply*)), gAddonPreferences, SLOT(DownloadFinished(lcHttpReply*))); + gAddonPreferences->mHttpReply = HttpManager->DownloadFile(QLatin1String(LC_BLENDER_ADDON_LATEST_URL)); + while (gAddonPreferences->mHttpReply) + QApplication::processEvents(); + if (!gAddonPreferences->mData.isEmpty()) + { + QJsonDocument Json = QJsonDocument::fromJson(gAddonPreferences->mData); + OnlineVersion = Json.object()["tag_name"].toString(); + gAddonPreferences->mData.clear(); + } + else + { + ShowMessage(tr("Check latest addon version failed."), tr("Latest Addon"), QString(), QString(), MBB_OK, QMessageBox::Warning); + return true; // Reload existing archive + } + QByteArray Ba; if (!ExtractedAddon) { @@ -1415,22 +1432,11 @@ bool lcBlenderPreferences::GetBlenderAddon(const QString& BlenderDir) } } - // TODO - Refactor, use LeoCAD mHttpReply/HttpManager - DownloadFile(LC_BLENDER_ADDON_LATEST_URL, tr("Latest Addon"),false/*promptRedirect*/,false/*showProgress*/); - QByteArray const &Response_Data = GetDownloadedFile(); - if (!Response_Data.isEmpty()) - { - QJsonDocument Json = QJsonDocument::fromJson(Response_Data); - OnlineVersion = Json.object()["tag_name"].toString(); - } - else - ShowMessage(tr("Check latest addon version failed."), tr("Latest Addon"), QString(), QString(), MBB_OK, QMessageBox::Warning); - if (!LocalVersion.isEmpty() && !OnlineVersion.isEmpty()) { - // LocalVersion is smaller than OnlineVersion so prompt to download new archive + // localVersion is smaller than onlineVersion so prompt to download new archive if (VersionStringCompare(LocalVersion.toStdString(), OnlineVersion.toStdString()) < 0) - return false; + return false; // Download new archive } return true; // Reload existing archive @@ -1496,10 +1502,12 @@ bool lcBlenderPreferences::GetBlenderAddon(const QString& BlenderDir) if (AddonAction == ADDON_DOWNLOAD) { BlenderAddonExists = false; - // TODO - Refactor, use LeoCAD mHttpReply/HttpManager - DownloadFile(LC_BLENDER_ADDON_URL, tr("Blender Addon"),false/*promptRedirect*/,false/*showProgress*/); - QByteArray const &Buffer = GetDownloadedFile(); - if (!Buffer.isEmpty()) + lcHttpManager* HttpManager = new lcHttpManager(gAddonPreferences); + connect(HttpManager, SIGNAL(DownloadFinished(lcHttpReply*)), gAddonPreferences, SLOT(DownloadFinished(lcHttpReply*))); + gAddonPreferences->mHttpReply = HttpManager->DownloadFile(QLatin1String(LC_BLENDER_ADDON_URL)); + while (gAddonPreferences->mHttpReply) + QApplication::processEvents(); + if (!gAddonPreferences->mData.isEmpty()) { if (QFileInfo(BlenderAddonFile).exists()) { @@ -1510,9 +1518,10 @@ bool lcBlenderPreferences::GetBlenderAddon(const QString& BlenderDir) QFile File(BlenderAddonFile); if (File.open(QIODevice::WriteOnly)) { - File.write(Buffer); + File.write(gAddonPreferences->mData); File.close(); BlenderAddonExists = true; + gAddonPreferences->mData.clear(); } else ShowMessage(tr("Failed to open Blender addon file:
%1:
%2") @@ -3485,134 +3494,14 @@ int lcBlenderPreferences::ShowMessage(QString const& Header, QString const& Titl return Box.exec(); } -// TODO - Refactor, use LeoCAD mHttpReply/HttpManager -void lcBlenderPreferences::DownloadFile(QString URL, QString Title, bool PromptRedirect, bool ShowProgress) +void lcBlenderPreferences::DownloadFinished(lcHttpReply* Reply) { - gAddonPreferences->mTitle = Title; - gAddonPreferences->mShowProgress = ShowProgress; - gAddonPreferences->mPromptRedirect = PromptRedirect; - - if (gAddonPreferences->mShowProgress) - { - gAddonPreferences->mProgressDialog = new QProgressDialog(nullptr); - connect(gAddonPreferences->mProgressDialog, SIGNAL(canceled()), gAddonPreferences, SLOT(cancelDownload())); - - gAddonPreferences->mProgressDialog->setWindowTitle(tr("Downloading")); - gAddonPreferences->mProgressDialog->setLabelText(tr("Downloading %1").arg(gAddonPreferences->mTitle)); - gAddonPreferences->mProgressDialog->show(); - } - - gAddonPreferences->mHttpManager = new QNetworkAccessManager(gAddonPreferences); - - gAddonPreferences->mUrl = URL; - - gAddonPreferences->mHttpRequestAborted = false; - - gAddonPreferences->StartRequest(gAddonPreferences->mUrl); - - while (gAddonPreferences->mHttpReply) - QApplication::processEvents(); -} - -QByteArray lcBlenderPreferences::GetDownloadedFile() -{ - return gAddonPreferences->mByteArray; -} - -void lcBlenderPreferences::StartRequest(QUrl Url) -{ - QNetworkRequest Request(Url); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - if (!mPromptRedirect) - Request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, true); -#else - if (!mPromptRedirect) - request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); -#endif -#endif - mHttpReply = mHttpManager->get(Request); - - connect(mHttpReply, SIGNAL(downloadProgress(qint64,qint64)), - this, SLOT(UpdateDownloadProgress(qint64,qint64))); - - connect(mHttpReply, SIGNAL(finished()), - this, SLOT(HttpDownloadFinished())); -} - -void lcBlenderPreferences::HttpDownloadFinished() -{ - if (mHttpRequestAborted) - { - mByteArray.clear(); - mHttpReply->deleteLater(); - mHttpReply = nullptr; - if (mShowProgress) - mProgressDialog->close(); - return; - } - - QString Message; - QString const MessageTitle = tr("%1 Download").arg(LC_PRODUCTNAME_STR); - QVariant RedirectionTarget = mHttpReply->attribute(QNetworkRequest::RedirectionTargetAttribute); - if (mHttpReply->error()) - { - mByteArray.clear(); - Message = tr("%1 download failed: %2.") - .arg(mTitle).arg(mHttpReply->errorString()); - - if (Message.endsWith("not found.")) - Message.append(tr("
Your internet connection may be interrupted.")); - - if (gMainWindow) - ShowMessage(Message, MessageTitle, QString(), QString(), MBB_OK, QMessageBox::Warning); - - } - else if (!RedirectionTarget.isNull()) - { - // This block should only trigger for redirects when Qt - // is less than 5.6.0 or if prompt redirect set to True - QUrl NewUrl = mUrl.resolved(RedirectionTarget.toUrl()); - bool ProceedToRedirect = true; - - if (mPromptRedirect && gMainWindow) - ProceedToRedirect = ShowMessage(tr("Download redirect to %1 ?").arg(NewUrl.toString()), - MessageTitle, QString(), QString(), MBB_YES_NO, QMessageBox::Question) == QMessageBox::Yes; - - if (ProceedToRedirect) - { - mUrl = NewUrl; - mHttpReply->deleteLater(); - mByteArray.resize(0); - StartRequest(mUrl); - return; - } - } + if (!Reply->error()) + mData = Reply->readAll(); else - mByteArray = mHttpReply->readAll(); + ShowMessage(tr("Addon download failed.")); - if (mShowProgress) - mProgressDialog->close(); - - mHttpReply->deleteLater(); mHttpReply = nullptr; - mHttpManager = nullptr; -} -void lcBlenderPreferences::CancelDownload() -{ - mHttpRequestAborted = true; - mHttpReply->abort(); -} - -void lcBlenderPreferences::UpdateDownloadProgress(qint64 BytesRead, qint64 BotalBytes) -{ - if (mHttpRequestAborted) - return; - - if (mShowProgress) - { - mProgressDialog->setMaximum(int(BotalBytes)); - mProgressDialog->setValue(int(BytesRead)); - } + Reply->deleteLater(); } diff --git a/common/lc_blenderpreferences.h b/common/lc_blenderpreferences.h index 4d11448d..fdc2a283 100644 --- a/common/lc_blenderpreferences.h +++ b/common/lc_blenderpreferences.h @@ -23,8 +23,7 @@ class QProgressBar; class QProcess; class QTimer; -class QNetworkAccessManager; -class QNetworkReply; +class lcHttpReply; class lcBlenderPreferences; class lcBlenderPreferencesDialog : public QDialog @@ -82,11 +81,6 @@ protected: static void LoadDefaultParameters(QByteArray& Buffer, int Which); static bool OverwriteFile(const QString& File); - // TODO - Refactor, use LeoCAD mHttpReply/HttpManager - static void DownloadFile(QString URL, QString Title, bool PromptRedirect = false, bool ShowProgress = true); - void StartRequest(QUrl Url); - static QByteArray GetDownloadedFile(); - QString ReadStdErr(bool& Error) const; void ClearGroupBox(QGroupBox* GroupBox); void InitPathsAndSettings(); @@ -121,15 +115,7 @@ private slots: void GetStandardOutput(); void ShowResult(); void StatusUpdate(bool, bool = true, const QString& = QString()); - - void HttpDownloadFinished(); - void CancelDownload(); - void UpdateDownloadProgress(qint64, qint64); - void FileLoaded(bool b) - { - mFileLoaded = true; - mFileLoadFail = !b; - } + void DownloadFinished(lcHttpReply*); private: enum BlenderPathType @@ -457,18 +443,8 @@ private: QProgressBar* mProgressBar = nullptr; QProcess* mProcess = nullptr; - // TODO - Refactor, use LeoCAD mHttpReply/HttpManager - QProgressDialog* mProgressDialog = nullptr; - QNetworkAccessManager* mHttpManager = nullptr; - QNetworkReply* mHttpReply = nullptr; - QByteArray mByteArray; - QString mTitle; - QUrl mUrl; - bool mPromptRedirect = false; - bool mHttpRequestAborted = false; - bool mFileLoaded = false; - bool mFileLoadFail = false; - bool mShowProgress = false; + lcHttpReply *mHttpReply = nullptr; + QByteArray mData; QTimer mUpdateTimer; From 6e721d76c4491d875ac4d79e16cc318cb0946a85 Mon Sep 17 00:00:00 2001 From: Trevor SANDY Date: Fri, 26 May 2023 16:17:59 +0200 Subject: [PATCH 04/14] Blender addon - settings lc_zipfile --- common/lc_blenderpreferences.cpp | 275 +++++++++++++++++++++++++++---- common/lc_blenderpreferences.h | 1 + 2 files changed, 247 insertions(+), 29 deletions(-) diff --git a/common/lc_blenderpreferences.cpp b/common/lc_blenderpreferences.cpp index 485992c2..c845397b 100644 --- a/common/lc_blenderpreferences.cpp +++ b/common/lc_blenderpreferences.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "lc_blenderpreferences.h" #include "lc_application.h" @@ -23,6 +24,8 @@ #include "lc_colorpicker.h" #include "lc_profile.h" #include "lc_http.h" +#include "lc_zipfile.h" +#include "lc_file.h" #include "project.h" #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) @@ -1289,15 +1292,21 @@ bool lcBlenderPreferences::ExtractBlenderAddon(const QString& BlenderDir) { gAddonPreferences->StatusUpdate(true, false, tr("Extract addon...")); QString const BlenderAddonFile = QDir::toNativeSeparators(QString("%1/%2").arg(BlenderDir).arg(LC_BLENDER_ADDON_FILE)); - // TODO - Replace QuaZip call to unzip addon - /* QStringList addonList = JlCompress::extractDir(blenderAddonFile, blenderDir); - if (addonList.isEmpty()) + + QString Result; + bool Success = gAddonPreferences->ExtractAddon(BlenderAddonFile, Result); + + if (!Success) { - showMessage(tr("Failed to extract %1 to %2") - .arg(blenderAddonFile).arg(blenderDir), tr("Extract addon")); - proceed = false; + QString Message = tr("Failed to extract %1 to %2") + .arg(LC_BLENDER_ADDON_FILE).arg(BlenderDir); + if (Result.size()) + Message.append(" "+Result); + + ShowMessage(Message, tr("Extract addon")); + + Proceed = false; } - */ } if (!Proceed) @@ -1378,34 +1387,30 @@ bool lcBlenderPreferences::GetBlenderAddon(const QString& BlenderDir) QByteArray Ba; if (!ExtractedAddon) { - // TODO - Replace QuaZip archive file check - /* - QuaZip Zip(BlenderAddonFile); - if (!Zip.open(QuaZip::mdUnzip)) + const char* VersionFile = "addons/" LC_BLENDER_ADDON_FOLDER_STR "/__version__.py"; + + lcZipFile ZipFile; + + if (!ZipFile.OpenRead(BlenderAddonFile)) { - QString const Result = tr("Could not open archive to check content. Return code %1.
" - "Archive file %2 may be open in another program.") - .arg(Zip.getZipError()).arg(QFileInfo(BlenderAddonFile).fileName()); - emit gui->messageSig(LOG_WARNING, Result); - return false; // Download new archive + ShowMessage(tr("Cannot open addon archive file: %1.").arg(BlenderAddonFile)); + return false; } - QString const AddonVersionFile = QString("addons/%1/__version__.py").arg(LC_BLENDER_ADDON_FOLDER_STR); - Zip.setCurrentFile(AddonVersionFile); - QuaZipFile File(&Zip); - if (!File.open(QIODevice::ReadOnly)) + lcMemFile File; + + if (!ZipFile.ExtractFile(VersionFile, File)) { - ShowMessage(tr("Cannot read addon archive version file: [%1]
%2.") - .arg(AddonVersionFile).arg(File.errorString())); + ShowMessage(tr("Cannot extract addon archive version file: %1.").arg(VersionFile)); + return false; + } + + Ba = QByteArray::fromRawData((const char*)File.mBuffer, File.GetLength()); + if (Ba.isEmpty()) + { + ShowMessage(tr("Cannot read addon archive version file: %1.").arg(VersionFile)); return false; // Download new archive } - Ba = File.readAll(); - File.close(); - Zip.close(); - if (Zip.getZipError() != UNZ_OK) - showMessage(tr("Archive close error. Return code %1.").arg(Zip.getZipError())); - */ - ShowMessage(tr("Check addon archive is not supported. Manually extract addon and continue.")); } else { @@ -3505,3 +3510,215 @@ void lcBlenderPreferences::DownloadFinished(lcHttpReply* Reply) Reply->deleteLater(); } + +namespace WindowsFileAttributes +{ +enum +{ + Dir = 0x10, // FILE_ATTRIBUTE_DIRECTORY + File = 0x80, // FILE_ATTRIBUTE_NORMAL + TypeMask = 0x90, + ReadOnly = 0x01, // FILE_ATTRIBUTE_READONLY + PermMask = 0x01 +}; +} + +namespace UnixFileAttributes +{ +enum +{ + Dir = 0040000, // __S_IFDIR + File = 0100000, // __S_IFREG + SymLink = 0120000, // __S_IFLNK + TypeMask = 0170000, // __S_IFMT + ReadUser = 0400, // __S_IRUSR + WriteUser = 0200, // __S_IWUSR + ExeUser = 0100, // __S_IXUSR + ReadGroup = 0040, // __S_IRGRP + WriteGroup = 0020, // __S_IWGRP + ExeGroup = 0010, // __S_IXGRP + ReadOther = 0004, // __S_IROTH + WriteOther = 0002, // __S_IWOTH + ExeOther = 0001, // __S_IXOTH + PermMask = 0777 +}; +} + +bool lcBlenderPreferences::ExtractAddon(const QString FileName, QString& Result) +{ + enum ZipHostOS + { + HostFAT = 0, + HostUnix = 3, + HostHPFS = 6, // filesystem used by OS/2 (and NT 3.x) + HostNTFS = 11, // filesystem used by Windows NT + HostVFAT = 14, // filesystem used by Windows 95, NT + HostOSX = 19 + }; + + struct ZipFileInfo + { + ZipFileInfo(lcZipFileInfo& FileInfo) noexcept + : ZipInfo(FileInfo), isDir(false), isFile(false), isSymLink(false) + { + } + bool IsValid() const noexcept { return isDir || isFile || isSymLink; } + lcZipFileInfo& ZipInfo; + QString filePath; + uint isDir : 1; + uint isFile : 1; + uint isSymLink : 1; + QFile::Permissions permissions; + }; + + auto ModeToPermissions = [](quint32 Mode) + { + QFile::Permissions Permissions; + if (Mode& UnixFileAttributes::ReadUser) + Permissions |= QFile::ReadOwner | QFile::ReadUser; + if (Mode& UnixFileAttributes::WriteUser) + Permissions |= QFile::WriteOwner | QFile::WriteUser; + if (Mode& UnixFileAttributes::ExeUser) + Permissions |= QFile::ExeOwner | QFile::ExeUser; + if (Mode& UnixFileAttributes::ReadGroup) + Permissions |= QFile::ReadGroup; + if (Mode& UnixFileAttributes::WriteGroup) + Permissions |= QFile::WriteGroup; + if (Mode& UnixFileAttributes::ExeGroup) + Permissions |= QFile::ExeGroup; + if (Mode& UnixFileAttributes::ReadOther) + Permissions |= QFile::ReadOther; + if (Mode& UnixFileAttributes::WriteOther) + Permissions |= QFile::WriteOther; + if (Mode& UnixFileAttributes::ExeOther) + Permissions |= QFile::ExeOther; + return Permissions; + }; + + lcZipFile ZipFile; + + if (!ZipFile.OpenRead(FileName)) + return false; + + QString const DestinationDir = QFileInfo(FileName).absolutePath(); + + bool Ok = true; + + int Extracted = 0; + + for (int FileIdx = 0; FileIdx < ZipFile.mFiles.GetSize(); FileIdx++) + { + ZipFileInfo FileInfo(ZipFile.mFiles[FileIdx]); + quint32 Mode = FileInfo.ZipInfo.external_fa; + const ZipHostOS HostOS = ZipHostOS(FileInfo.ZipInfo.version >> 8); + switch (HostOS) + { + case HostOSX: + case HostUnix: + Mode = (Mode >> 16)& 0xffff; + switch (Mode& UnixFileAttributes::TypeMask) + { + case UnixFileAttributes::SymLink: + FileInfo.isSymLink = true; + break; + case UnixFileAttributes::Dir: + FileInfo.isDir = true; + break; + case UnixFileAttributes::File: + default: + FileInfo.isFile = true; + break; + } + FileInfo.permissions = ModeToPermissions(Mode); + break; + case HostFAT: + case HostNTFS: + case HostHPFS: + case HostVFAT: + switch (Mode& WindowsFileAttributes::TypeMask) + { + case WindowsFileAttributes::Dir: + FileInfo.isDir = true; + break; + case WindowsFileAttributes::File: + default: + FileInfo.isFile = true; + break; + } + FileInfo.permissions |= QFile::ReadOwner | QFile::ReadUser | QFile::ReadGroup | QFile::ReadOther; + if ((Mode& WindowsFileAttributes::ReadOnly) == 0) + FileInfo.permissions |= QFile::WriteOwner | QFile::WriteUser | QFile::WriteGroup | QFile::WriteOther; + if (FileInfo.isDir) + FileInfo.permissions |= QFile::ExeOwner | QFile::ExeUser | QFile::ExeGroup | QFile::ExeOther; + break; + default: + ShowMessage(tr("ZipFile entry format (HostOS %1) at index %2 is not supported. Extract terminated.") + .arg(HostOS).arg(FileIdx), tr("Extract Addon"), QString(), QString(), MBB_OK, QMessageBox::Warning); + Ok = false; + } + + if (!Ok || !(Ok = FileInfo.IsValid())) + break; + + // Check the file path - if broken, convert separators, eat leading and trailing ones + FileInfo.filePath = QDir::fromNativeSeparators(FileInfo.ZipInfo.file_name); + QStringRef FilePathRef(&FileInfo.filePath); + while (FilePathRef.startsWith(QLatin1Char('.')) || FilePathRef.startsWith(QLatin1Char('/'))) + FilePathRef = FilePathRef.mid(1); + while (FilePathRef.endsWith(QLatin1Char('/'))) + FilePathRef.chop(1); + FileInfo.filePath = FilePathRef.toString(); + + QString const AbsPath = QDir::fromNativeSeparators(DestinationDir + QDir::separator() + FileInfo.filePath); + + // directories + if (FileInfo.isDir) + { + QDir BaseDir(DestinationDir); + if (!(Ok = BaseDir.mkpath(FileInfo.filePath))) + break; + if (!(Ok = QFile::setPermissions(AbsPath, FileInfo.permissions))) + break; + Extracted++; + continue; + } + + lcMemFile MemFile; + + ZipFile.ExtractFile(FileIdx, MemFile); + + QByteArray const& ByteArray = QByteArray::fromRawData((const char*)MemFile.mBuffer, MemFile.GetLength()); + + // symlinks + if (FileInfo.isSymLink) + { + QString Destination = QFile::decodeName(ByteArray); + if (!(Ok = !Destination.isEmpty())) + break; + QFileInfo LinkFileInfo(AbsPath); + if (!QFile::exists(LinkFileInfo.absolutePath())) + QDir::root().mkpath(LinkFileInfo.absolutePath()); + if (!(Ok = QFile::link(Destination, AbsPath))) + break; + Extracted++; + continue; + } + + // files + if (FileInfo.isFile) + { + QFile File(AbsPath); + if(!(Ok = File.open(QIODevice::WriteOnly))) + break; + File.write(ByteArray); + File.setPermissions(FileInfo.permissions); + File.close(); + Extracted++; + } + } + + if (!Ok) + Result = tr("%1 of %2 files extracted.").arg(Extracted).arg(ZipFile.mFiles.GetSize()); + + return Ok; +} diff --git a/common/lc_blenderpreferences.h b/common/lc_blenderpreferences.h index fdc2a283..5c1f9bd6 100644 --- a/common/lc_blenderpreferences.h +++ b/common/lc_blenderpreferences.h @@ -85,6 +85,7 @@ protected: void ClearGroupBox(QGroupBox* GroupBox); void InitPathsAndSettings(); void InitPathsAndSettingsMM(); + bool ExtractAddon(const QString FileName, QString &Result); signals: void SettingChangedSig(bool); From ccddcbf7c70354792ebe5fcd7a37ce5cbec58a63 Mon Sep 17 00:00:00 2001 From: Trevor SANDY Date: Sun, 21 May 2023 15:53:59 +0200 Subject: [PATCH 05/14] Blender addon - settings preferences --- qt/lc_qpreferencesdialog.cpp | 18 +++++ qt/lc_qpreferencesdialog.h | 1 + qt/lc_qpreferencesdialog.ui | 141 +++++++++++++++++++---------------- 3 files changed, 97 insertions(+), 63 deletions(-) diff --git a/qt/lc_qpreferencesdialog.cpp b/qt/lc_qpreferencesdialog.cpp index 78220a26..be33cac2 100644 --- a/qt/lc_qpreferencesdialog.cpp +++ b/qt/lc_qpreferencesdialog.cpp @@ -9,6 +9,7 @@ #include "lc_glextensions.h" #include "pieceinf.h" #include "lc_edgecolordialog.h" +#include "lc_blenderpreferences.h" static const char* gLanguageLocales[] = { @@ -365,6 +366,23 @@ void lcQPreferencesDialog::on_partsArchiveBrowse_clicked() ui->partsLibrary->setText(QDir::toNativeSeparators(result)); } +void lcQPreferencesDialog::on_BlenderAddonSettingsButton_clicked() +{ + int Width = 1280; + int Height = 720; + double Scale = 1.0f; + + lcBlenderPreferencesDialog::GetBlenderPreferences( + Width, + Height, + Scale, + this); + + Q_UNUSED(Width) + Q_UNUSED(Height) + Q_UNUSED(Scale) +} + void lcQPreferencesDialog::on_ColorConfigBrowseButton_clicked() { QString Result = QFileDialog::getOpenFileName(this, tr("Select Color Configuration File"), ui->ColorConfigEdit->text(), tr("Settings Files (*.ldr);;All Files (*.*)")); diff --git a/qt/lc_qpreferencesdialog.h b/qt/lc_qpreferencesdialog.h index c0cf6dfd..c7f23ce2 100644 --- a/qt/lc_qpreferencesdialog.h +++ b/qt/lc_qpreferencesdialog.h @@ -67,6 +67,7 @@ public slots: void ColorButtonClicked(); void AutomateEdgeColor(); void on_AutomateEdgeColor_toggled(); + void on_BlenderAddonSettingsButton_clicked(); void on_studStyleCombo_currentIndexChanged(int index); void on_antiAliasing_toggled(); void on_edgeLines_toggled(); diff --git a/qt/lc_qpreferencesdialog.ui b/qt/lc_qpreferencesdialog.ui index 2c53c1ff..99e2e13e 100644 --- a/qt/lc_qpreferencesdialog.ui +++ b/qt/lc_qpreferencesdialog.ui @@ -333,10 +333,17 @@ - - + + - Contrast Settings... + Mesh LOD + + + + + + + @@ -359,6 +366,64 @@ + + + + Fade Previous Steps + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QSlider::NoTicks + + + + + + + + + + + + + + Anti-aliasing + + + + + + + Stud Style + + + + + + + Highlight New Parts + + + + + + + Shading Mode: + + + @@ -403,10 +468,10 @@ - - + + - Shading Mode: + Contrast Settings... @@ -423,20 +488,6 @@ - - - - Stud Style - - - - - - - Anti-aliasing - - - @@ -463,54 +514,17 @@ - - + + - Highlight New Parts + Blender LDraw Render: - - + + - Mesh LOD - - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - QSlider::NoTicks - - - - - - - - - - - - - - Fade Previous Steps - - - - - - - + Addon Settings... @@ -2007,6 +2021,7 @@ studStyleCombo HighContrastButton ShadingMode + BlenderAddonSettingsButton edgeLines ConditionalLinesCheckBox AutomateEdgeColor From 13244e108e97580ddd7763557631848fea980dba Mon Sep 17 00:00:00 2001 From: Trevor SANDY Date: Tue, 23 May 2023 08:55:11 +0200 Subject: [PATCH 06/14] Blender addon - render images --- leocad.qrc | 7 +++++++ resources/file_render_blender.png | Bin 0 -> 1803 bytes resources/file_render_blender_16.png | Bin 0 -> 1284 bytes resources/file_render_blender_logo_1280x720.png | Bin 0 -> 83312 bytes resources/file_render_open_in_blender.png | Bin 0 -> 2085 bytes resources/file_render_open_in_blender_16.png | Bin 0 -> 1267 bytes resources/file_render_povray.png | Bin 0 -> 2093 bytes resources/file_render_povray_16.png | Bin 0 -> 932 bytes 8 files changed, 7 insertions(+) create mode 100644 resources/file_render_blender.png create mode 100644 resources/file_render_blender_16.png create mode 100644 resources/file_render_blender_logo_1280x720.png create mode 100644 resources/file_render_open_in_blender.png create mode 100644 resources/file_render_open_in_blender_16.png create mode 100644 resources/file_render_povray.png create mode 100644 resources/file_render_povray_16.png diff --git a/leocad.qrc b/leocad.qrc index c5ebff8f..13b59431 100644 --- a/leocad.qrc +++ b/leocad.qrc @@ -19,6 +19,9 @@ resources/file_open_16.png resources/file_print_16.png resources/file_print_preview_16.png + resources/file_render_blender_16.png + resources/file_render_open_in_blender_16.png + resources/file_render_povray_16.png resources/file_save_16.png resources/action_camera.png resources/action_color_picker.png @@ -77,6 +80,10 @@ resources/file_open.png resources/file_print.png resources/file_print_preview.png + resources/file_render_blender.png + resources/file_render_blender_logo_1280x720.png + resources/file_render_open_in_blender.png + resources/file_render_povray.png resources/file_save.png resources/time_first.png resources/time_last.png diff --git a/resources/file_render_blender.png b/resources/file_render_blender.png new file mode 100644 index 0000000000000000000000000000000000000000..23a0843bcba04a40fa3089bb1efe010ff317c26b GIT binary patch literal 1803 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 literal 0 HcmV?d00001 diff --git a/resources/file_render_blender_logo_1280x720.png b/resources/file_render_blender_logo_1280x720.png new file mode 100644 index 0000000000000000000000000000000000000000..e0f303deb34e175298331dbd6ef83f497ff23f6b GIT binary patch literal 83312 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�xLgD_cBWFWH$=On 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

^^wM)@A?bE5n*tUu#t*$aBSjZZy*!`h)>BRu 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 XE-@}OWJg$100000NkvXXu0mjfj%msl literal 0 HcmV?d00001 diff --git a/resources/file_render_povray_16.png b/resources/file_render_povray_16.png new file mode 100644 index 0000000000000000000000000000000000000000..f7fa7d76612d33ba943314140307eee521d5c865 GIT binary patch literal 932 zcmV;V16%xwP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!2kdb!2!6DYwZ94135`VK~y+TO;Sy0 z8%G#@v(jp{T2)pmsgi2RY9oUS0oyUeO^}dsiNO^*6>`zR5Xixh%U&0tvww zYMpZEB^O@;PORDohopfJiV2QlQ7b=EjZGTMrm}ukyE{AGS%Jy>m|+Iyy?Ngcfra9H z@#@+1*?C1#OoJMNzyx?jFGN8EWfVjw=8QQL zv1x-D?BUu&l1t=p>-;Um<8efzQOrm)sJ*D+?yI}P{@%WpPNqXH#U+&AlphORfbbv$ z?M@pr*%@TUGgzoDAlJ^}vVIxGb`jUlTt`3(;JNhNkv^YBCY_<2+ZV9NRHFzE1o;(j zx87oTa~X%#L&$X*tEE*ii6QlU3h!UPNBy5VOvAL_+5%ueuh&DT+kvKO2!%qp7`O=O zcM0A3E+!fi2n+_Wt!-nlGYFB2&^6tr+C`)$g=T8JU1&91*z9d$CAflXlh@GvyNSuv zBo?cS*!p7&v!`ZZ5)*pw3jv-2?}UrEu>bEq7T+ym*|7|{CS#P2;@Mx%P}!`Y@IwKa z(-}}gAqai*$0;zK*=zW_VW?phr~;9X5%`D?Bi#|)p1O^EHV@3)n?0l}h+f z`+y^Q1ewTq*?npMEQylkv>$|7t+t#9M1ZDhD3{9?6ZjuLO=y-{m6?bD0000 Date: Fri, 19 May 2023 16:10:00 +0200 Subject: [PATCH 07/14] Blender addon - render lc_profile --- common/lc_profile.cpp | 5 +++-- common/lc_profile.h | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/common/lc_profile.cpp b/common/lc_profile.cpp index 66847237..f1cc0c7f 100644 --- a/common/lc_profile.cpp +++ b/common/lc_profile.cpp @@ -140,8 +140,9 @@ static lcProfileEntry gProfileEntries[LC_NUM_PROFILE_KEYS] = lcProfileEntry("POVRay", "Path", "/usr/bin/povray"), // LC_PROFILE_POVRAY_PATH lcProfileEntry("POVRay", "LGEOPath", ""), // LC_PROFILE_POVRAY_LGEO_PATH - lcProfileEntry("POVRay", "Width", 1280), // LC_PROFILE_POVRAY_WIDTH - lcProfileEntry("POVRay", "Height", 720), // LC_PROFILE_POVRAY_HEIGHT + + lcProfileEntry("Render", "Width", 1280), // LC_PROFILE_RENDER_WIDTH + lcProfileEntry("Render", "Height", 720), // LC_PROFILE_RENDER_HEIGHT lcProfileEntry("Blender", "Path", ""), // LC_PROFILE_BLENDER_PATH lcProfileEntry("Blender", "LDrawConfigPath", ""), // LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH diff --git a/common/lc_profile.h b/common/lc_profile.h index aaadc873..16508434 100644 --- a/common/lc_profile.h +++ b/common/lc_profile.h @@ -88,8 +88,8 @@ enum LC_PROFILE_KEY LC_PROFILE_HTML_IMAGE_HEIGHT, LC_PROFILE_POVRAY_PATH, LC_PROFILE_POVRAY_LGEO_PATH, - LC_PROFILE_POVRAY_WIDTH, - LC_PROFILE_POVRAY_HEIGHT, + LC_PROFILE_RENDER_WIDTH, + LC_PROFILE_RENDER_HEIGHT, LC_PROFILE_BLENDER_PATH, LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH, LC_PROFILE_BLENDER_VERSION, From 4d2ecee58d9e7b65f23233202e6cb4e39f474c19 Mon Sep 17 00:00:00 2001 From: Trevor SANDY Date: Tue, 23 May 2023 13:05:56 +0200 Subject: [PATCH 08/14] Blender addon - render current step --- common/lc_model.cpp | 12 ++++-- common/lc_model.h | 4 +- common/project.cpp | 103 ++++++++++++++++++++++++++++++++++++++++++++ common/project.h | 1 + 4 files changed, 114 insertions(+), 6 deletions(-) diff --git a/common/lc_model.cpp b/common/lc_model.cpp index 59a1ad1e..754c6014 100644 --- a/common/lc_model.cpp +++ b/common/lc_model.cpp @@ -263,7 +263,7 @@ void lcModel::UpdatePieceInfo(std::vector& UpdatedModels) mPieceInfo->SetBoundingBox(Min, Max); } -void lcModel::SaveLDraw(QTextStream& Stream, bool SelectedOnly) const +void lcModel::SaveLDraw(QTextStream& Stream, bool SelectedOnly, lcStep CurrentStep) const { const QLatin1String LineEnding("\r\n"); @@ -273,12 +273,16 @@ void lcModel::SaveLDraw(QTextStream& Stream, bool SelectedOnly) const lcStep Step = 1; int CurrentLine = 0; int AddedSteps = 0; + bool Saved = false; for (lcPiece* Piece : mPieces) { if (SelectedOnly && !Piece->IsSelected()) continue; + if ((Saved = CurrentStep && Piece->GetStepShow() > CurrentStep)) + break; + while (Piece->GetFileLine() > CurrentLine && CurrentLine < mFileLines.size()) { QString Line = mFileLines[CurrentLine]; @@ -391,7 +395,7 @@ void lcModel::SaveLDraw(QTextStream& Stream, bool SelectedOnly) const Stream << QLatin1String("0 !LEOCAD SYNTH END\r\n"); } - while (CurrentLine < mFileLines.size()) + while ( !Saved && CurrentLine < mFileLines.size()) { QString Line = mFileLines[CurrentLine]; QTextStream LineStream(&Line, QIODevice::ReadOnly); @@ -3323,11 +3327,11 @@ void lcModel::GetPartsList(int DefaultColorIndex, bool ScanSubModels, bool AddSu } } -void lcModel::GetPartsListForStep(lcStep Step, int DefaultColorIndex, lcPartsList& PartsList) const +void lcModel::GetPartsListForStep(lcStep Step, int DefaultColorIndex, lcPartsList& PartsList, bool Cumulative) const { for (const lcPiece* Piece : mPieces) { - if (Piece->GetStepShow() != Step || Piece->IsHidden()) + if (Cumulative ? Piece->GetStepShow() > Step : Piece->GetStepShow() != Step || Piece->IsHidden()) continue; int ColorIndex = Piece->GetColorIndex(); diff --git a/common/lc_model.h b/common/lc_model.h index b9637ef4..da4c7a0f 100644 --- a/common/lc_model.h +++ b/common/lc_model.h @@ -205,7 +205,7 @@ public: void RemoveFocusPieceFromGroup(); void ShowEditGroupsDialog(); - void SaveLDraw(QTextStream& Stream, bool SelectedOnly) const; + void SaveLDraw(QTextStream& Stream, bool SelectedOnly, lcStep CurrentStep = 0) const; void LoadLDraw(QIODevice& Device, Project* Project); bool LoadBinary(lcFile* File); bool LoadLDD(const QString& FileData); @@ -263,7 +263,7 @@ public: bool GetVisiblePiecesBoundingBox(lcVector3& Min, lcVector3& Max) const; std::vector GetPiecesBoundingBoxPoints() const; void GetPartsList(int DefaultColorIndex, bool ScanSubModels, bool AddSubModels, lcPartsList& PartsList) const; - void GetPartsListForStep(lcStep Step, int DefaultColorIndex, lcPartsList& PartsList) const; + void GetPartsListForStep(lcStep Step, int DefaultColorIndex, lcPartsList& PartsList, bool Cumulative = false) const; void GetModelParts(const lcMatrix44& WorldMatrix, int DefaultColorIndex, std::vector& ModelParts) const; void GetSelectionInformation(int* Flags, lcArray& Selection, lcObject** Focus) const; lcArray GetSelectionModePieces(const lcPiece* SelectedPiece) const; diff --git a/common/project.cpp b/common/project.cpp index 19b634ec..89733b72 100644 --- a/common/project.cpp +++ b/common/project.cpp @@ -652,6 +652,109 @@ std::vector Project::GetModelParts() return ModelParts; } +bool Project::ExportCurrentStep(const QString& FileName) +{ + QFile File(FileName); + + if (!File.open(QIODevice::WriteOnly)) + { + QMessageBox::warning(gMainWindow, tr("Error"), tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString())); + return false; + } + + QStringList Models; + + Models.append(lcGetActiveModel()->GetProperties().mFileName); + + std::function ParseStepModel = [&](const QString& ModelName) + { + Models.append(ModelName); + + for (lcModel* Model : mModels) + { + if (Model->GetProperties().mFileName == ModelName) + { + lcPartsList ModelParts; + + Model->GetPartsList(gDefaultColor, false, true, ModelParts); + + for (const auto& PartIt : ModelParts) + { + const PieceInfo* PartInfo = PartIt.first; + + if (PartInfo->IsModel()) + { + ParseStepModel(PartInfo->mFileName); + } + else + continue; + } + + break; + } + } + }; + + const lcStep CurrentStep = lcGetActiveModel()->GetCurrentStep(); + + bool MPD = mModels.GetSize() > 1; + + if (MPD) + { + lcPartsList StepParts; + + lcGetActiveModel()->GetPartsListForStep(CurrentStep, gDefaultColor, StepParts, true); + + if (!StepParts.empty()) + { + for (const auto& PartIt : StepParts) + { + const PieceInfo *PartInfo = PartIt.first; + + if (PartInfo->IsModel()) + { + ParseStepModel(PartInfo->mFileName); + } + else + continue; + } + } + } + + QTextStream Stream(&File); + + for (lcModel* Model : mModels) + { + if (!Models.contains(Model->GetProperties().mFileName)) + continue; + + const lcStep ModelStep = Model->GetCurrentStep(); + + if (!Model->IsActive()) + Model->SetTemporaryStep(CurrentStep); + + if (MPD) + Stream << QLatin1String("0 FILE ") << Model->GetProperties().mFileName << QLatin1String("\r\n"); + + Model->SaveLDraw(Stream, false, CurrentStep); + + if (MPD) + Stream << QLatin1String("0 NOFILE\r\n"); + + if (!Model->IsActive()) + { + Model->SetTemporaryStep(ModelStep); + Model->CalculateStep(LC_STEP_MAX); + } + } + + File.close(); + + lcSetProfileString(LC_PROFILE_PROJECTS_PATH, QFileInfo(FileName).absolutePath()); + + return true; +} + bool Project::ExportModel(const QString& FileName, lcModel* Model) const { QFile File(FileName); diff --git a/common/project.h b/common/project.h index e40efdcf..8b941e0c 100644 --- a/common/project.h +++ b/common/project.h @@ -89,6 +89,7 @@ public: bool ImportInventory(const QByteArray& Inventory, const QString& Name, const QString& Description); void SaveImage(); + bool ExportCurrentStep(const QString& FileName); bool ExportModel(const QString& FileName, lcModel* Model) const; bool Export3DStudio(const QString& FileName); void ExportBrickLink(); From f4da046f81490337ab11fde546e3b7daac714f6b Mon Sep 17 00:00:00 2001 From: Trevor SANDY Date: Fri, 2 Jun 2023 00:02:53 +0200 Subject: [PATCH 09/14] Blender addon - render dialog --- qt/lc_renderdialog.cpp | 876 +++++++++++++++++++++++++++++++++-------- qt/lc_renderdialog.h | 53 ++- qt/lc_renderdialog.ui | 31 ++ 3 files changed, 785 insertions(+), 175 deletions(-) diff --git a/qt/lc_renderdialog.cpp b/qt/lc_renderdialog.cpp index bd8a9102..36138a45 100644 --- a/qt/lc_renderdialog.cpp +++ b/qt/lc_renderdialog.cpp @@ -4,6 +4,13 @@ #include "project.h" #include "lc_application.h" #include "lc_profile.h" +#include "lc_blenderpreferences.h" +#include "lc_mainwindow.h" +#include "lc_model.h" + +#ifdef Q_OS_WIN +#include +#endif #define LC_POVRAY_PREVIEW_WIDTH 768 #define LC_POVRAY_PREVIEW_HEIGHT 432 @@ -11,6 +18,23 @@ #define LC_POVRAY_MEMORY_MAPPED_FILE 1 #endif +static inline QString ElapsedTime(const qint64 &Duration) +{ + qint64 Elapsed = Duration; + int Milliseconds = int(Elapsed % 1000); + Elapsed /= 1000; + int Seconds = int(Elapsed % 60); + Elapsed /= 60; + int Minutes = int(Elapsed % 60); + Elapsed /= 60; + int Hours = int(Elapsed % 24); + + return QObject::tr("Elapsed time: %1%2%3") + .arg(Hours > 0 ? QString("%1 %2 ").arg(Hours).arg(Hours > 1 ? QObject::tr("hours") : QObject::tr("hour")) : QString()) + .arg(Minutes > 0 ? QString("%1 %2 ").arg(Minutes).arg(Minutes > 1 ? QObject::tr("minutes") : QObject::tr("minute")) : QString()) + .arg(QString("%1.%2 %3").arg(Seconds).arg(Milliseconds,3,10,QLatin1Char('0')).arg(Seconds > 1 ? QObject::tr("seconds") : QObject::tr("second"))); +} + void lcRenderPreviewWidget::resizeEvent(QResizeEvent* Event) { mScaledImage = QImage(); @@ -37,8 +61,9 @@ void lcRenderPreviewWidget::paintEvent(QPaintEvent* PaintEvent) Painter.fillRect(rect(), Qt::white); } -lcRenderDialog::lcRenderDialog(QWidget* Parent) +lcRenderDialog::lcRenderDialog(QWidget* Parent, int Command) : QDialog(Parent), + mCommand(Command), ui(new Ui::lcRenderDialog) { #ifndef QT_NO_PROCESS @@ -48,18 +73,105 @@ lcRenderDialog::lcRenderDialog(QWidget* Parent) ui->setupUi(this); - ui->WidthEdit->setText(QString::number(lcGetProfileInt(LC_PROFILE_POVRAY_WIDTH))); + mWidth = lcGetProfileInt(LC_PROFILE_RENDER_WIDTH); + mHeight = lcGetProfileInt(LC_PROFILE_RENDER_HEIGHT); + mScale = 1.0f; + + ui->WidthEdit->setText(QString::number(mWidth)); ui->WidthEdit->setValidator(new QIntValidator(16, INT_MAX)); - ui->HeightEdit->setText(QString::number(lcGetProfileInt(LC_PROFILE_POVRAY_HEIGHT))); + ui->HeightEdit->setText(QString::number(mHeight)); ui->HeightEdit->setValidator(new QIntValidator(16, INT_MAX)); ui->OutputEdit->setText(lcGetActiveProject()->GetImageFileName(false)); - QImage Image(LC_POVRAY_PREVIEW_WIDTH, LC_POVRAY_PREVIEW_HEIGHT, QImage::Format_RGB32); - Image.fill(QColor(255, 255, 255)); - ui->preview->SetImage(Image); + lcModel* Model = lcGetActiveProject()->GetActiveModel(); + QString LabelMessage = tr("Render image:"); + if (Model) + LabelMessage = tr("Render STEP %1 image:").arg(Model->GetCurrentStep()); + + ui->renderLabel->setText(LabelMessage); + ui->RenderOutputButton->setEnabled(false); + + if (mCommand == POVRAY_RENDER) + { + setWindowTitle(tr("POV-Ray Image Render")); + + ui->RenderButton->setToolTip(tr("Render LDraw model")); + + QImage Image(LC_POVRAY_PREVIEW_WIDTH, LC_POVRAY_PREVIEW_HEIGHT, QImage::Format_RGB32); + Image.fill(QColor(255, 255, 255)); + ui->preview->SetImage(Image); + + ui->RenderSettingsButton->hide(); + } + else + { + setWindowTitle(tr("Blender %1").arg(mCommand == OPEN_IN_BLENDER ? tr("LDraw Import") : tr("Image Render"))); + + bool BlenderConfigured = !lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE).isEmpty(); + + QStringList const& DataPathList = QStandardPaths::standardLocations(QStandardPaths::DataLocation); + if (!QDir(QString("%1/Blender/addons/%2").arg(DataPathList.first()).arg(LC_BLENDER_ADDON_FOLDER_STR)).isReadable()) + { + BlenderConfigured = false; + lcSetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE, QString()); + } + + if (BlenderConfigured) + mImportModule = lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE) == QLatin1String("TN") + ? tr("LDraw Import TN") + : tr("LDraw Import MM"); + + ui->RenderButton->setToolTip(BlenderConfigured + ? tr("Render LDraw Model") + : tr("Blender not configured. Use Settings... to configure.")); + + ui->RenderButton->setEnabled(BlenderConfigured); + + ui->RenderSettingsButton->setToolTip(tr("Blender render settings")); + + ui->qualityLabel->hide(); + ui->QualityComboBox->hide(); + + if (mCommand == OPEN_IN_BLENDER) + { + LabelMessage = tr("Open%1 in Blender using %2:") + .arg(Model ? tr(" STEP %1").arg(Model->GetCurrentStep()) : "").arg(mImportModule); + + ui->RenderSettingsButton->setToolTip(tr("Blender import settings")); + + ui->RenderButton->setText(tr("Open in Blender")); + ui->RenderButton->setFixedWidth(ui->RenderButton->sizeHint().width() + 20); + if (BlenderConfigured) + ui->RenderButton->setToolTip(tr("Import and open LDraw model in Blender")); + + ui->renderLabel->setText(LabelMessage); + ui->renderLabel->setAlignment(Qt::AlignTrailing | Qt::AlignVCenter); + + ui->outputLabel->hide(); + ui->OutputEdit->hide(); + ui->RenderProgress->hide(); + ui->OutputBrowseButton->hide(); + ui->RenderOutputButton->hide(); + + adjustSize(); + setMinimumWidth(ui->preview->geometry().width()); + ui->preview->hide(); + } + else + { + QImage Image(QPixmap(":/resources/file_render_blender_logo_1280x720.png").toImage()); + Image = Image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + ui->preview->SetImage(Image); + } + + connect(&mUpdateTimer, SIGNAL(timeout()), this, SLOT(UpdateElapsedTime())); + } connect(&mUpdateTimer, SIGNAL(timeout()), this, SLOT(Update())); + mUpdateTimer.start(500); + + setSizeGripEnabled(true); } lcRenderDialog::~lcRenderDialog() @@ -67,9 +179,12 @@ lcRenderDialog::~lcRenderDialog() delete ui; } -QString lcRenderDialog::GetOutputFileName() const +QString lcRenderDialog::GetOutputFileName(int StdErr) const { - return QDir(QDir::tempPath()).absoluteFilePath("leocad-render.out"); + QString LogFile = mCommand == POVRAY_RENDER + ? StdErr ? QLatin1String("leocad-povray-render.err") : QLatin1String("leocad-povray-render.out") + : StdErr ? QLatin1String("leocad-blender-render.err") : QLatin1String("leocad-blender-render.out"); + return QDir(QDir::tempPath()).absoluteFilePath(LogFile); } QString lcRenderDialog::GetPOVFileName() const @@ -77,6 +192,21 @@ QString lcRenderDialog::GetPOVFileName() const return QDir(QDir::tempPath()).absoluteFilePath("leocad-render.pov"); } +void lcRenderDialog::UpdateElapsedTime() const +{ + if (mProcess && mCommand == BLENDER_RENDER) + { + QString const RenderType = lcGetProfileString(LC_PROFILE_BLENDER_VERSION).startsWith("v3") + ? QLatin1String("Samples") + : QLatin1String("Tiles"); + ui->renderLabel->setText(tr("%1: %2/%3, %4") + .arg(RenderType) + .arg(mBlendProgValue) + .arg(mBlendProgMax) + .arg(ElapsedTime(mRenderTime.elapsed()))); + } +} + void lcRenderDialog::CloseProcess() { #ifndef QT_NO_PROCESS @@ -84,17 +214,21 @@ void lcRenderDialog::CloseProcess() mProcess = nullptr; #endif + if (mCommand == POVRAY_RENDER) + { #if LC_POVRAY_MEMORY_MAPPED_FILE - mOutputFile.unmap((uchar*)mOutputBuffer); - mOutputBuffer = nullptr; - mOutputFile.close(); + mOutputFile.unmap((uchar*)mOutputBuffer); + mOutputBuffer = nullptr; + mOutputFile.close(); - QFile::remove(GetOutputFileName()); + QFile::remove(GetOutputFileName()); #endif - QFile::remove(GetPOVFileName()); + QFile::remove(GetPOVFileName()); + } - ui->RenderButton->setText(tr("Render")); + if (mCommand != OPEN_IN_BLENDER) + ui->RenderButton->setText(tr("Render")); } bool lcRenderDialog::PromptCancel() @@ -104,17 +238,31 @@ bool lcRenderDialog::PromptCancel() { if (QMessageBox::question(this, tr("Cancel Render"), tr("Are you sure you want to cancel the current render?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (mProcess) + if (mCommand == POVRAY_RENDER) + gMainWindow->mActions[LC_FILE_RENDER_POVRAY]->setEnabled(true); + else if (mCommand == BLENDER_RENDER) + gMainWindow->mActions[LC_FILE_RENDER_BLENDER]->setEnabled(true); +#ifdef Q_OS_WIN + TerminateChildProcess(mProcess->processId(), + QCoreApplication::applicationPid()); +#endif + mProcess->kill(); + CloseProcess(); + if (mStdOutList.size()) { - mProcess->kill(); - CloseProcess(); + WriteStdOut(); + ui->RenderOutputButton->setEnabled(true); } + if (mCommand == BLENDER_RENDER) + ui->renderLabel->setText(tr("Tiles: %1/%2, Render Cancelled.") + .arg(mBlendProgValue) + .arg(mBlendProgMax)); } else return false; } #endif - + return true; } @@ -124,6 +272,19 @@ void lcRenderDialog::reject() QDialog::reject(); } +void lcRenderDialog::on_RenderSettingsButton_clicked() +{ + if (mCommand == POVRAY_RENDER) + return; + + lcBlenderPreferencesDialog::GetBlenderPreferences(mWidth, mHeight, mScale, this); + + if (lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE).isEmpty()) + ui->RenderButton->setToolTip(tr("Blender not configured. Use Settings... to configure.")); + else + ui->RenderButton->setEnabled(true); +} + void lcRenderDialog::on_RenderButton_clicked() { #ifndef QT_NO_PROCESS @@ -133,103 +294,428 @@ void lcRenderDialog::on_RenderButton_clicked() return; } - QString FileName = GetPOVFileName(); + ui->RenderOutputButton->setEnabled(false); - if (!lcGetActiveProject()->ExportPOVRay(FileName)) - return; + mPreviewWidth = ui->preview->width(); + mPreviewHeight = ui->preview->height(); - QStringList Arguments; + mRenderTime.start(); - Arguments.append(QString::fromLatin1("+I\"%1\"").arg(FileName)); - Arguments.append(QString::fromLatin1("+W%1").arg(ui->WidthEdit->text())); - Arguments.append(QString::fromLatin1("+H%1").arg(ui->HeightEdit->text())); - Arguments.append("-O-"); + if (mCommand == POVRAY_RENDER) + { + gMainWindow->mActions[LC_FILE_RENDER_POVRAY]->setEnabled(false); + + QString FileName = GetPOVFileName(); + + QImage Image(mPreviewWidth, mPreviewHeight, QImage::Format_RGB32); + Image.fill(QColor(255, 255, 255)); + ui->preview->SetImage(Image); + + if (!lcGetActiveProject()->ExportPOVRay(FileName)) + return; + + QStringList Arguments; + + Arguments.append(QString::fromLatin1("+I\"%1\"").arg(FileName)); + Arguments.append(QString::fromLatin1("+W%1").arg(ui->WidthEdit->text())); + Arguments.append(QString::fromLatin1("+H%1").arg(ui->HeightEdit->text())); + Arguments.append("-O-"); #if LC_POVRAY_MEMORY_MAPPED_FILE - Arguments.append(QString::fromLatin1("+SM\"%1\"").arg(GetOutputFileName())); + Arguments.append(QString::fromLatin1("+SM\"%1\"").arg(GetOutputFileName())); #endif - int Quality = ui->QualityComboBox->currentIndex(); + int Quality = ui->QualityComboBox->currentIndex(); - switch (Quality) - { - case 0: - Arguments.append("+Q11"); - Arguments.append("+R3"); - Arguments.append("+A0.1"); - Arguments.append("+J0.5"); - break; + switch (Quality) + { + case 0: + Arguments.append("+Q11"); + Arguments.append("+R3"); + Arguments.append("+A0.1"); + Arguments.append("+J0.5"); + break; - case 1: - Arguments.append("+Q5"); - Arguments.append("+A0.1"); - break; + case 1: + Arguments.append("+Q5"); + Arguments.append("+A0.1"); + break; - case 2: - break; - } + case 2: + break; + } - /* - if (!LGEOPath.isEmpty()) - { - Arguments.append(QString::fromLatin1("+L%1lg/").arg(LGEOPath)); - Arguments.append(QString::fromLatin1("+L%1ar/").arg(LGEOPath)); - } - */ + /* + if (!LGEOPath.isEmpty()) + { + Arguments.append(QString::fromLatin1("+L%1lg/").arg(LGEOPath)); + Arguments.append(QString::fromLatin1("+L%1ar/").arg(LGEOPath)); + } + */ - QString POVRayPath; + QString POVRayPath; #ifdef Q_OS_WIN - POVRayPath = QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/povconsole32-sse2.exe")); + POVRayPath = QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/povconsole32-sse2.exe")); #endif #ifdef Q_OS_LINUX - POVRayPath = lcGetProfileString(LC_PROFILE_POVRAY_PATH); - Arguments.append("+FN"); - Arguments.append("-D"); + POVRayPath = lcGetProfileString(LC_PROFILE_POVRAY_PATH); + Arguments.append("+FN"); + Arguments.append("-D"); #endif #ifdef Q_OS_MACOS - POVRayPath = QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/povray")); + POVRayPath = QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/povray")); #endif - mProcess = new QProcess(this); + mProcess = new RenderProcess(this); #ifdef Q_OS_LINUX - connect(mProcess, SIGNAL(readyReadStandardError()), this, SLOT(ReadStdErr())); + connect(mProcess, SIGNAL(readyReadStandardError()), this, SLOT(ReadStdErr())); #endif - mProcess->start(POVRayPath, Arguments); + int const StdErr = 1; + QStringList POVEnv = QProcess::systemEnvironment(); + POVEnv.prepend("POV_IGNORE_SYSCONF_MSG=1"); + mProcess->setEnvironment(POVEnv); + mProcess->setStandardErrorFile(GetOutputFileName(StdErr)); + mProcess->start(POVRayPath, Arguments); - mImage = QImage(ui->WidthEdit->text().toInt(), ui->HeightEdit->text().toInt(), QImage::Format_ARGB32); - mImage.fill(QColor(255, 255, 255)); - ui->preview->SetImage(mImage); + mImage = QImage(ui->WidthEdit->text().toInt(), ui->HeightEdit->text().toInt(), QImage::Format_ARGB32); + mImage.fill(QColor(255, 255, 255)); + ui->preview->SetImage(mImage); - if (mProcess->waitForStarted()) - { - ui->RenderButton->setText(tr("Cancel")); - ui->RenderProgress->setValue(ui->RenderProgress->minimum()); - mStdErrList.clear(); + if (mProcess->waitForStarted()) + { + ui->RenderButton->setText(tr("Cancel")); + ui->RenderProgress->setValue(ui->RenderProgress->minimum()); + mStdErrList.clear(); + } + else + { + gMainWindow->mActions[LC_FILE_RENDER_POVRAY]->setEnabled(true); + QMessageBox::warning(this, tr("Error"), tr("Error starting POV-Ray.")); + CloseProcess(); + } } else { - QMessageBox::warning(this, tr("Error"), tr("Error starting POV-Ray.")); - CloseProcess(); - } + QString const BlenderLDrawConfigFile = lcGetProfileString(LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH); + QString const BlenderImportModule = lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE); + if (!QFileInfo(BlenderLDrawConfigFile).isReadable() && !BlenderImportModule.isEmpty()) + lcBlenderPreferences::SaveSettings(); + + QString const Option = mCommand == OPEN_IN_BLENDER ? tr("import") : tr("render"); + + ui->renderLabel->setText(tr("Saving Blender %1 model...").arg(Option)); + + QApplication::processEvents(); + + mBlendProgValue = 0; + mBlendProgMax = 0; + + QStringList const &DataPathList = QStandardPaths::standardLocations(QStandardPaths::DataLocation); + mDataPath = DataPathList.first(); + QString const DefaultBlendFile = QString("%1/blender/config/%2").arg(mDataPath).arg(LC_BLENDER_ADDON_BLEND_FILE); + + lcModel* Model = lcGetActiveProject()->GetActiveModel(); + QString const ModelFileName = QFileInfo(QDir(lcGetProfileString(LC_PROFILE_PROJECTS_PATH)), QString("%1_Step_%2.ldr").arg(QFileInfo(Model->GetProperties().mFileName).baseName()).arg(Model->GetCurrentStep())).absoluteFilePath(); + + lcGetActiveProject()->ExportCurrentStep(ModelFileName); + + if (!QFileInfo(ModelFileName).isReadable()) + return; + + bool SearchCustomDir = true; + + QString Message; + QStringList Arguments; + QString PythonExpression = QString("\"import bpy; bpy.ops.render_scene.lpub3d_render_ldraw(" + "'EXEC_DEFAULT', " + "resolution_width=%1, resolution_height=%2, " + "render_percentage=%3, model_file=r'%4', " + "image_file=r'%5', preferences_file=r'%6'") + .arg(mWidth).arg(mHeight) + .arg(mScale * 100) + .arg(QDir::toNativeSeparators(ModelFileName).replace("\\","\\\\")) + .arg(QDir::toNativeSeparators(ui->OutputEdit->text()).replace("\\","\\\\")) + .arg(QDir::toNativeSeparators(BlenderLDrawConfigFile).replace("\\","\\\\")); + if (BlenderImportModule == QLatin1String("MM")) + PythonExpression.append(", use_ldraw_import_mm=True"); + if (SearchCustomDir) + PythonExpression.append(", search_additional_paths=True"); + if (mCommand == OPEN_IN_BLENDER) + { + PythonExpression.append(", import_only=True"); + + Arguments << QLatin1String("--window-geometry"); + Arguments << QLatin1String("200 100 1440 900"); + } + else + { + Arguments << QLatin1String("--background"); + gMainWindow->mActions[LC_FILE_RENDER_BLENDER]->setEnabled(false); + } + + PythonExpression.append(", cli_render=True)\""); + + if (QFileInfo(DefaultBlendFile).exists()) + Arguments << QDir::toNativeSeparators(DefaultBlendFile); + Arguments << QString("--python-expr"); + Arguments << PythonExpression; + + QString ScriptName, ScriptCommand, ShellProgram; + +#ifdef Q_OS_WIN + ScriptName = QLatin1String("render_ldraw_model.bat"); +#else + ScriptName = QLatin1String("render_ldraw_model.sh"); +#endif + ScriptCommand = QString("%1 %2").arg(lcGetProfileString(LC_PROFILE_BLENDER_PATH)).arg(Arguments.join(" ")); + + if (mCommand == OPEN_IN_BLENDER) + ScriptCommand.append(QString(" > %1").arg(QDir::toNativeSeparators(GetOutputFileName()))); + + const QLatin1String LineEnding("\r\n"); + + QFile Script(QString("%1/%2").arg(QDir::tempPath()).arg(ScriptName)); + if(Script.open(QIODevice::WriteOnly | QIODevice::Text)) + { + QTextStream Stream(&Script); +#ifdef Q_OS_WIN + Stream << QLatin1String("@ECHO OFF &SETLOCAL") << LineEnding; +#else + Stream << QLatin1String("#!/bin/bash") << LineEnding; +#endif + Stream << ScriptCommand << LineEnding; + Script.close(); + } + else + { + QMessageBox::warning(this, tr("Error"), tr("Cannot write Blender render script file [%1] %2.") + .arg(Script.fileName()) + .arg(Script.errorString())); + + gMainWindow->mActions[LC_FILE_RENDER_BLENDER]->setEnabled(true); + return; + } + + QThread::sleep(2); + +#ifdef Q_OS_WIN + ShellProgram = QLatin1String(LC_WINDOWS_SHELL); +#else + ShellProgram = QLatin1String(LC_UNIX_SHELL); +#endif + + mProcess = new RenderProcess(this); + + connect(mProcess, SIGNAL(readyReadStandardOutput()), this, SLOT(ReadStdOut())); + + QString const LDrawLibPath = QFileInfo(lcGetProfileString(LC_PROFILE_PARTS_LIBRARY)).absolutePath(); + QStringList SystemEnvironment = QProcess::systemEnvironment(); + SystemEnvironment.prepend("LDRAW_DIRECTORY=" + LDrawLibPath); + int const StdErr = 1; + + mProcess->setEnvironment(SystemEnvironment); + + mProcess->setWorkingDirectory(QDir::toNativeSeparators(QString("%1/blender").arg(mDataPath))); + + mProcess->setStandardErrorFile(GetOutputFileName(StdErr)); + + if (mCommand == OPEN_IN_BLENDER) + { + QFileInfo Info(GetOutputFileName()); + if (Info.exists()) + QFile::remove(Info.absoluteFilePath()); +#ifdef Q_OS_WIN + mProcess->startDetached(ShellProgram, QStringList() << "/C" << Script.fileName()); +#else + mProcess->startDetached(ShellProgram, QStringList() << Script.fileName()); +#endif + if (mProcess) + { + mProcess->kill(); + CloseProcess(); + if (mStdOutList.size()) + WriteStdOut(); + if (Info.exists()) + { + QFile Log(Info.absoluteFilePath()); + QTime Wait = QTime::currentTime().addSecs(3); + while (!Log.size() || QTime::currentTime() < Wait) + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + if (Log.size()) { + if (Log.open(QFile::ReadOnly | QFile::Text)) + { + QByteArray Ba = Log.readAll(); + bool const Error = QString(Ba).contains(QRegExp("(?:\\w)*ERROR: ", Qt::CaseInsensitive)); + bool const Warning = QString(Ba).contains(QRegExp("(?:\\w)*WARNING: ", Qt::CaseInsensitive)); + if (Error || Warning) + { + QMessageBox::Icon Icon = Error ? QMessageBox::Critical : QMessageBox::Warning; + QString const &Items = Error ? tr("errors%1").arg(Warning ? tr(" and warnings") : "") : Warning ? tr("warnings") : ""; + QString const &Title = tr("Open in Blender output"); + QString const &Body = tr("Open in Blender encountered %1. See Show Details...").arg(Items); + lcBlenderPreferences::ShowMessage(Body, Title, QString(), QString(Ba), 0, Icon); + } + } + } + } + close(); + return; + } + } + else + { +#ifdef Q_OS_WIN + mProcess->start(ShellProgram, QStringList() << "/C" << Script.fileName()); +#else + mProcess->start(ShellProgram, QStringList() << Script.fileName()); +#endif + } + + if (mProcess->waitForStarted()) + { + ui->RenderButton->setText(tr("Cancel")); + ui->RenderProgress->setValue(ui->RenderProgress->minimum()); + ui->renderLabel->setText(tr("Loading LDraw model... %1") + .arg(ElapsedTime(mRenderTime.elapsed()))); + QApplication::processEvents(); + } + else + { + gMainWindow->mActions[LC_FILE_RENDER_BLENDER]->setEnabled(true); + Message = tr("Error starting Blender render process"); + QMessageBox::warning(this, tr("Error"), Message); + CloseProcess(); + } + } // BLENDER_RENDER #endif } -void lcRenderDialog::ReadStdErr() +#ifdef Q_OS_WIN +int lcRenderDialog::TerminateChildProcess(const qint64 Pid, const qint64 Ppid) { - QString StdErr = QString(mProcess->readAllStandardError()); - mStdErrList.append(StdErr); -#ifdef Q_OS_LINUX - QRegExp RegexPovRayProgress("Rendered (\\d+) of (\\d+) pixels.*"); - RegexPovRayProgress.setCaseSensitivity(Qt::CaseInsensitive); - if (RegexPovRayProgress.indexIn(StdErr) == 0) + DWORD pID = DWORD(Pid); + DWORD ppID = DWORD(Ppid); + HANDLE hSnapshot = INVALID_HANDLE_VALUE, hProcess = INVALID_HANDLE_VALUE; + PROCESSENTRY32 pe32; + + if ((hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, ppID)) == INVALID_HANDLE_VALUE) { - ui->RenderProgress->setMaximum(RegexPovRayProgress.cap(2).toInt()); - ui->RenderProgress->setValue(RegexPovRayProgress.cap(1).toInt()); + QMessageBox::warning(this, tr("Error"), QString("%1 failed: %1").arg("CreateToolhelp32Snapshot").arg(GetLastError())); + return -1; } + pe32.dwSize = sizeof(PROCESSENTRY32); + if (Process32First(hSnapshot, &pe32) == FALSE) + { + QMessageBox::warning(this, tr("Error"), QString("%1 failed: %2").arg("Process32First").arg(GetLastError())); + CloseHandle(hSnapshot); + return -2; + } + do + { + if (QString::fromWCharArray(pe32.szExeFile).contains(QRegExp("^(?:cmd\\.exe|conhost\\.exe|blender\\.exe)$", Qt::CaseInsensitive))) + { + if ((pe32.th32ProcessID == pID && pe32.th32ParentProcessID == ppID) || pe32.th32ParentProcessID == pID) + { + if ((hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pe32.th32ProcessID)) == INVALID_HANDLE_VALUE) + { + QMessageBox::warning(this, tr("Error"), tr("%1 failed: %2").arg("OpenProcess").arg(GetLastError())); + return -3; + } + else + { + TerminateProcess(hProcess, 9); + CloseHandle(hProcess); + } + } + } + } + while (Process32Next(hSnapshot, &pe32)); + + CloseHandle(hSnapshot); + + return 0; +} #endif + +void lcRenderDialog::ReadStdOut() +{ + if (mCommand == POVRAY_RENDER) + return; + + QString StdOut = QString(mProcess->readAllStandardOutput()); + mStdOutList.append(StdOut); + QRegExp RxRenderProgress; + RxRenderProgress.setCaseSensitivity(Qt::CaseInsensitive); + bool BlenderVersion3 = lcGetProfileString(LC_PROFILE_BLENDER_VERSION).startsWith("v3"); + if (BlenderVersion3) + RxRenderProgress.setPattern("Sample (\\d+)\\/(\\d+)"); + else + RxRenderProgress.setPattern("(\\d+)\\/(\\d+) Tiles"); + if (StdOut.contains(RxRenderProgress)) + { + mBlendProgValue = RxRenderProgress.cap(1).toInt(); + mBlendProgMax = RxRenderProgress.cap(2).toInt(); + ui->RenderProgress->setMaximum(mBlendProgMax); + ui->RenderProgress->setValue(mBlendProgValue); + } +} + +QString lcRenderDialog::ReadStdErr(bool &HasError) const +{ + HasError = mCommand == BLENDER_RENDER ? false : true; + + QFile File; + QStringList returnLines; + + int const StdErr = 1; + File.setFileName(GetOutputFileName(StdErr)); + + if (! File.open(QFile::ReadOnly | QFile::Text)) + { + QString message = tr("Failed to open log file: %1:\n%2") + .arg(File.fileName()) + .arg(File.errorString()); + return message; + } + + QTextStream In(&File); + while (! In.atEnd()) + { + QString Line = In.readLine(0); + returnLines << Line.trimmed() + "
"; + if (mCommand == POVRAY_RENDER) + { + if (Line.contains(QRegExp("^POV-Ray finished$", Qt::CaseSensitive))) + HasError = false; + } + else if (mCommand == BLENDER_RENDER) + { + if (!HasError && !Line.isEmpty()) + HasError = true; + } + } + + return returnLines.join(" "); +} + +void lcRenderDialog::WriteStdOut() +{ + QFile File(GetOutputFileName()); + + if (File.open(QFile::WriteOnly | QIODevice::Truncate | QFile::Text)) + { + QTextStream Out(&File); + + for (const QString& Line : mStdOutList) + Out << Line; + + File.close(); + + ui->RenderOutputButton->setEnabled(true); + } } void lcRenderDialog::Update() @@ -250,117 +736,148 @@ void lcRenderDialog::Update() } #endif -#if LC_POVRAY_MEMORY_MAPPED_FILE - if (!mOutputBuffer) + if (mCommand == POVRAY_RENDER) { - mOutputFile.setFileName(GetOutputFileName()); - - if (!mOutputFile.open(QFile::ReadWrite)) - return; - - mOutputBuffer = mOutputFile.map(0, mOutputFile.size()); - +#if LC_POVRAY_MEMORY_MAPPED_FILE if (!mOutputBuffer) { - mOutputFile.close(); - return; + mOutputFile.setFileName(GetOutputFileName()); + + if (!mOutputFile.open(QFile::ReadWrite)) + return; + + mOutputBuffer = mOutputFile.map(0, mOutputFile.size()); + + if (!mOutputBuffer) + { + mOutputFile.close(); + return; + } } - } - struct lcSharedMemoryHeader - { - quint32 Version; - quint32 Width; - quint32 Height; - quint32 PixelsWritten; - quint32 PixelsRead; - }; - - lcSharedMemoryHeader* Header = (lcSharedMemoryHeader*)mOutputBuffer; - - if (Header->PixelsWritten == Header->PixelsRead) - return; - - int Width = Header->Width; - int Height = Header->Height; - int PixelsWritten = Header->PixelsWritten; - - quint8* Pixels = (quint8*)(Header + 1); - - for (int y = 0; y < Height; y++) - { - for (int x = 0; x < Width; x++) + struct lcSharedMemoryHeader { - mImage.setPixel(x, y, qRgba(Pixels[0], Pixels[1], Pixels[2], Pixels[3])); - Pixels += 4; + quint32 Version; + quint32 Width; + quint32 Height; + quint32 PixelsWritten; + quint32 PixelsRead; + }; + + lcSharedMemoryHeader* Header = (lcSharedMemoryHeader*)mOutputBuffer; + + if (Header->PixelsWritten == Header->PixelsRead) + return; + + int Width = Header->Width; + int Height = Header->Height; + int PixelsWritten = Header->PixelsWritten; + + if (!Header->PixelsRead) + mImage = QImage(Width, Height, QImage::Format_ARGB32); + + quint8* Pixels = (quint8*)(Header + 1); + + for (int y = 0; y < Height; y++) + { + for (int x = 0; x < Width; x++) + { + mImage.setPixel(x, y, qRgba(Pixels[0], Pixels[1], Pixels[2], Pixels[3])); + Pixels += 4; + } } - } - Header->PixelsRead = PixelsWritten; + Header->PixelsRead = PixelsWritten; - ui->RenderProgress->setMaximum(mImage.width() * mImage.height()); - ui->RenderProgress->setValue(int(Header->PixelsRead)); + ui->RenderProgress->setMaximum(mImage.width() * mImage.height()); + ui->RenderProgress->setValue(int(Header->PixelsRead)); - ui->preview->SetImage(mImage); + if (PixelsWritten == Width * Height) + ui->RenderProgress->setValue(ui->RenderProgress->maximum()); + + ui->preview->SetImage(mImage.scaled(mPreviewWidth, mPreviewHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + + /*ui->preview->SetImage(mImage);*/ #endif + } } void lcRenderDialog::ShowResult() { - ReadStdErr(); - ui->RenderProgress->setValue(ui->RenderProgress->maximum()); +#ifndef QT_NO_PROCESS + bool Error; - bool Error = (mProcess->exitStatus() != QProcess::NormalExit || mProcess->exitCode() != 0); - if (Error) + const QString StdErrLog = ReadStdErr(Error); + + const QString RenderLabel = mCommand == BLENDER_RENDER ? tr("Blender Render") : tr("POV-Ray Render"); + + if (mProcess->exitStatus() != QProcess::NormalExit || mProcess->exitCode() != 0 || Error) { - WriteStdLog(Error); - QMessageBox MessageBox; - MessageBox.setWindowTitle(tr("Error")); - MessageBox.setIcon(QMessageBox::Information); - MessageBox.setText(tr("An error occurred while rendering. Check details or try again.")); - MessageBox.setDetailedText(mStdErrList.join(QString())); - MessageBox.exec(); + ui->renderLabel->setText(tr("Image generation failed.")); + ui->RenderProgress->setRange(0,1); + ui->RenderProgress->setValue(0); + + QString const &Title = mCommand == BLENDER_RENDER ? tr("Blender Render") : tr("POV-Ray Render"); + QString const &Body = tr ("An error occurred while rendering. See Show Details..."); + lcBlenderPreferences::ShowMessage(Body, Title, QString(), StdErrLog, 0, QMessageBox::Warning); return; } - - ui->preview->SetImage(mImage); - - QString FileName = ui->OutputEdit->text(); - - if (!FileName.isEmpty()) - { - QImageWriter Writer(FileName); - - bool Result = Writer.write(mImage); - - if (!Result) - QMessageBox::information(this, tr("Error"), tr("Error writing to file '%1':\n%2").arg(FileName, Writer.errorString())); - } - - WriteStdLog(); -} - -void lcRenderDialog::WriteStdLog(bool Error) -{ - QString FileName = lcGetActiveProject()->GetFileName(); - if (FileName.isEmpty()) - return; - - QFile LogFile(QDir::toNativeSeparators(QFileInfo(FileName).absolutePath() + "/" + - (Error ? "stderr-povrayrender" : "stdout-povrayrender"))); - - if (LogFile.open(QFile::WriteOnly | QIODevice::Truncate | QFile::Text)) - { - QTextStream Out(&LogFile); - for (const QString& Line : mStdErrList) - Out << Line; - LogFile.close(); - } else { - QMessageBox::information(this, tr("Error"), tr("Error writing to %1 file '%2':\n%3") - .arg(Error ? "stderr" : "stdout").arg(LogFile.fileName(), LogFile.errorString())); + ui->RenderProgress->setValue(ui->RenderProgress->maximum()); } +#endif + + bool Success = false; + + QString FileName = ui->OutputEdit->text(); + + if (mCommand == POVRAY_RENDER) + { + gMainWindow->mActions[LC_FILE_RENDER_POVRAY]->setEnabled(true); + + ui->preview->SetImage(mImage.scaled(mPreviewWidth, mPreviewHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + + if (!FileName.isEmpty()) + { + QImageWriter Writer(FileName); + + Success = Writer.write(mImage); + + if (!Success) + QMessageBox::warning(this, tr("Error"), tr("Error writing to file '%1':\n%2").arg(FileName, Writer.errorString())); + } + + } + else if (mCommand == BLENDER_RENDER) + { + gMainWindow->mActions[LC_FILE_RENDER_BLENDER]->setEnabled(true); + + Success = QFileInfo(FileName).exists(); + if (Success) + { + setMinimumSize(100, 100); + QImageReader Reader(FileName); + mImage = Reader.read(); + mImage = mImage.convertToFormat(QImage::Format_ARGB32_Premultiplied); + + mPreviewWidth = mImage.width(); + mPreviewHeight = mImage.height(); + + ui->preview->SetImage(mImage.scaled(mPreviewWidth, mPreviewHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } + } + + if (!Success) + { + ui->renderLabel->setStyleSheet("QLabel { color : red; }"); + ui->renderLabel->setText(tr("Image render failed.")); + QString const Message = QString("%1 %2. %3").arg(RenderLabel).arg(tr("failed (unknown reason)")) + .arg(ElapsedTime(mRenderTime.elapsed())); + QMessageBox::warning(this, tr("Error"), Message); + } + + WriteStdOut(); } void lcRenderDialog::on_OutputBrowseButton_clicked() @@ -369,5 +886,28 @@ void lcRenderDialog::on_OutputBrowseButton_clicked() if (!Result.isEmpty()) ui->OutputEdit->setText(QDir::toNativeSeparators(Result)); +} +void lcRenderDialog::on_RenderOutputButton_clicked() +{ + QString RenderType = mCommand == POVRAY_RENDER ? QLatin1String("POV-Ray") : QLatin1String("Blender"); + QFileInfo FileInfo(GetOutputFileName()); + if (!FileInfo.exists()) + { + QMessageBox::warning(this, tr("Error"), tr("%1 Standard output file not found: %2.") + .arg(RenderType).arg(FileInfo.absoluteFilePath())); + return; + } + + QDesktopServices::openUrl(QUrl("file:///"+FileInfo.absoluteFilePath(), QUrl::TolerantMode)); +} + + +RenderProcess::~RenderProcess() +{ + if(state() == QProcess::Running || state() == QProcess::Starting) + { + terminate(); + waitForFinished(); + } } diff --git a/qt/lc_renderdialog.h b/qt/lc_renderdialog.h index 95c6fdd9..a3d709a3 100644 --- a/qt/lc_renderdialog.h +++ b/qt/lc_renderdialog.h @@ -6,6 +6,19 @@ namespace Ui { class lcRenderDialog; } +class QProcess; +class RenderProcess : public QProcess +{ + Q_OBJECT + +public: + explicit RenderProcess(QObject* parent = nullptr) + : QProcess(parent) + { + } + ~RenderProcess(); +}; + class lcRenderPreviewWidget : public QWidget { Q_OBJECT @@ -36,34 +49,60 @@ class lcRenderDialog : public QDialog Q_OBJECT public: - explicit lcRenderDialog(QWidget* Parent); + explicit lcRenderDialog(QWidget* Parent, int Command); ~lcRenderDialog(); public slots: void reject() override; void on_RenderButton_clicked(); void on_OutputBrowseButton_clicked(); + void on_RenderSettingsButton_clicked(); + void on_RenderOutputButton_clicked(); void Update(); protected slots: - void ReadStdErr(); - void WriteStdLog(bool = false); + void ReadStdOut(); + void WriteStdOut(); + void UpdateElapsedTime() const; protected: - QString GetOutputFileName() const; + QString GetOutputFileName(int StdErr = 0) const; QString GetPOVFileName() const; + QString ReadStdErr(bool& Error) const; void CloseProcess(); bool PromptCancel(); void ShowResult(); - -#ifndef QT_NO_PROCESS - QProcess* mProcess; +#ifdef Q_OS_WIN + int TerminateChildProcess(const qint64 Pid, const qint64 Ppid); #endif +#ifndef QT_NO_PROCESS + RenderProcess* mProcess; +#endif + enum CommandType + { + POVRAY_RENDER, + BLENDER_RENDER, + OPEN_IN_BLENDER + }; + QTimer mUpdateTimer; + QElapsedTimer mRenderTime; QFile mOutputFile; void* mOutputBuffer; QImage mImage; QStringList mStdErrList; + QStringList mStdOutList; + + int mWidth; + int mHeight; + int mPreviewWidth; + int mPreviewHeight; + int mCommand; + int mBlendProgValue; + int mBlendProgMax; + double mScale; + QString mImportModule; + QString mDataPath; Ui::lcRenderDialog* ui; }; diff --git a/qt/lc_renderdialog.ui b/qt/lc_renderdialog.ui index 71da3e50..dc40a2be 100644 --- a/qt/lc_renderdialog.ui +++ b/qt/lc_renderdialog.ui @@ -48,6 +48,16 @@ + + + + Blender addon settings + + + Settings... + + + @@ -110,6 +120,13 @@ + + + + + + + @@ -117,6 +134,16 @@ + + + + Open the standard output log + + + Output... + + + @@ -135,9 +162,13 @@ + WidthEdit + HeightEdit + RenderSettingsButton QualityComboBox OutputEdit OutputBrowseButton + RenderButton From e98e37598486adb2d5babbee5563ce112c010846 Mon Sep 17 00:00:00 2001 From: Trevor SANDY Date: Sun, 21 May 2023 11:17:00 +0200 Subject: [PATCH 10/14] Blender addon - render command --- common/lc_commands.cpp | 21 ++++++++++++++++++--- common/lc_commands.h | 4 +++- common/lc_mainwindow.cpp | 30 +++++++++++++++++++++++++----- common/lc_mainwindow.h | 2 +- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/common/lc_commands.cpp b/common/lc_commands.cpp index 8fcdc45d..c9424e7c 100644 --- a/common/lc_commands.cpp +++ b/common/lc_commands.cpp @@ -108,13 +108,28 @@ const lcCommand gCommands[] = QT_TRANSLATE_NOOP("Status", "Export the current model in Wavefront OBJ format"), "" }, - // LC_FILE_RENDER + // LC_FILE_RENDER_POVRAY { - QT_TRANSLATE_NOOP("Action", "File.Render"), - QT_TRANSLATE_NOOP("Menu", "&Render..."), + QT_TRANSLATE_NOOP("Action", "File.Render.POVRay"), + QT_TRANSLATE_NOOP("Menu", "&POV-Ray Render..."), QT_TRANSLATE_NOOP("Status", "Render the current model using POV-Ray"), "" }, + // LC_FILE_RENDER_BLENDER + { + QT_TRANSLATE_NOOP("Action", "File.Render.Blender"), + QT_TRANSLATE_NOOP("Menu", "&Bender Render..."), + QT_TRANSLATE_NOOP("Status", "Render the current model using Blender"), + "" + }, + + // LC_FILE_RENDER_OPEN_IN_BLENDER + { + QT_TRANSLATE_NOOP("Action", "File.Render.OpenInBlender"), + QT_TRANSLATE_NOOP("Menu", "&Open In Blender..."), + QT_TRANSLATE_NOOP("Status", "Open the current model in Blender"), + "" + }, // LC_FILE_INSTRUCTIONS { QT_TRANSLATE_NOOP("Action", "File.Instructions"), diff --git a/common/lc_commands.h b/common/lc_commands.h index 9dec3cef..f49d28fc 100644 --- a/common/lc_commands.h +++ b/common/lc_commands.h @@ -17,7 +17,9 @@ enum lcCommandId LC_FILE_EXPORT_CSV, LC_FILE_EXPORT_POVRAY, LC_FILE_EXPORT_WAVEFRONT, - LC_FILE_RENDER, + LC_FILE_RENDER_POVRAY, + LC_FILE_RENDER_BLENDER, + LC_FILE_RENDER_OPEN_IN_BLENDER, LC_FILE_INSTRUCTIONS, LC_FILE_PRINT, LC_FILE_PRINT_PREVIEW, diff --git a/common/lc_mainwindow.cpp b/common/lc_mainwindow.cpp index fac10cac..759e7871 100644 --- a/common/lc_mainwindow.cpp +++ b/common/lc_mainwindow.cpp @@ -251,6 +251,21 @@ void lcMainWindow::CreateActions() EditActionPanIcon.addFile(":/resources/action_pan_16.png"); mActions[LC_EDIT_ACTION_PAN]->setIcon(EditActionPanIcon); + QIcon FileRenderPOVRayIcon; + FileRenderPOVRayIcon.addFile(":/resources/file_render_povray.png"); + FileRenderPOVRayIcon.addFile(":/resources/file_render_povray_16.png"); + mActions[LC_FILE_RENDER_POVRAY]->setIcon(FileRenderPOVRayIcon); + + QIcon FileRenderBlenderIcon; + FileRenderBlenderIcon.addFile(":/resources/file_render_blender.png"); + FileRenderBlenderIcon.addFile(":/resources/file_render_blender_16.png"); + mActions[LC_FILE_RENDER_BLENDER]->setIcon(FileRenderBlenderIcon); + + QIcon FileOpenInBlenderIcon; + FileOpenInBlenderIcon.addFile(":/resources/file_render_open_in_blender.png"); + FileOpenInBlenderIcon.addFile(":/resources/file_render_open_in_blender_16.png"); + mActions[LC_FILE_RENDER_OPEN_IN_BLENDER]->setIcon(FileOpenInBlenderIcon); + mActions[LC_EDIT_ACTION_CAMERA]->setIcon(QIcon(":/resources/action_camera.png")); mActions[LC_EDIT_ACTION_ROTATE_VIEW]->setIcon(QIcon(":/resources/action_rotate_view.png")); mActions[LC_EDIT_ACTION_ROLL]->setIcon(QIcon(":/resources/action_roll.png")); @@ -443,7 +458,10 @@ void lcMainWindow::CreateMenus() ExportMenu->addAction(mActions[LC_FILE_EXPORT_POVRAY]); ExportMenu->addAction(mActions[LC_FILE_EXPORT_WAVEFRONT]); FileMenu->addSeparator(); - FileMenu->addAction(mActions[LC_FILE_RENDER]); + QMenu* RenderMenu = FileMenu->addMenu(tr("&Render")); + RenderMenu->addAction(mActions[LC_FILE_RENDER_POVRAY]); + RenderMenu->addAction(mActions[LC_FILE_RENDER_BLENDER]); + RenderMenu->addAction(mActions[LC_FILE_RENDER_OPEN_IN_BLENDER]); FileMenu->addAction(mActions[LC_FILE_INSTRUCTIONS]); FileMenu->addAction(mActions[LC_FILE_PRINT]); FileMenu->addAction(mActions[LC_FILE_PRINT_PREVIEW]); @@ -1264,9 +1282,9 @@ void lcMainWindow::ShowHTMLDialog() lcGetActiveProject()->ExportHTML(Options); } -void lcMainWindow::ShowRenderDialog() +void lcMainWindow::ShowRenderDialog(int Command) { - lcRenderDialog Dialog(this); + lcRenderDialog Dialog(this, Command); Dialog.exec(); } @@ -2603,8 +2621,10 @@ void lcMainWindow::HandleCommand(lcCommandId CommandId) lcGetActiveProject()->ExportWavefront(QString()); break; - case LC_FILE_RENDER: - ShowRenderDialog(); + case LC_FILE_RENDER_POVRAY: + case LC_FILE_RENDER_BLENDER: + case LC_FILE_RENDER_OPEN_IN_BLENDER: + ShowRenderDialog(CommandId - LC_FILE_RENDER_POVRAY); break; case LC_FILE_INSTRUCTIONS: diff --git a/common/lc_mainwindow.h b/common/lc_mainwindow.h index 4531a154..72ec97a0 100644 --- a/common/lc_mainwindow.h +++ b/common/lc_mainwindow.h @@ -326,7 +326,7 @@ protected: void ShowUpdatesDialog(); void ShowAboutDialog(); void ShowHTMLDialog(); - void ShowRenderDialog(); + void ShowRenderDialog(int Command); void ShowInstructionsDialog(); void ShowPrintDialog(); void CreatePreviewWidget(); From 7ded3a4899caf26d40ead8e6fe8bff15caf35fdb Mon Sep 17 00:00:00 2001 From: Leonardo Zide Date: Sun, 18 Jun 2023 18:04:08 -0700 Subject: [PATCH 11/14] Warning fixes. --- common/lc_blenderpreferences.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lc_blenderpreferences.cpp b/common/lc_blenderpreferences.cpp index c845397b..82033f3c 100644 --- a/common/lc_blenderpreferences.cpp +++ b/common/lc_blenderpreferences.cpp @@ -1405,7 +1405,7 @@ bool lcBlenderPreferences::GetBlenderAddon(const QString& BlenderDir) return false; } - Ba = QByteArray::fromRawData((const char*)File.mBuffer, File.GetLength()); + Ba = QByteArray::fromRawData((const char*)File.mBuffer, (int)File.GetLength()); if (Ba.isEmpty()) { ShowMessage(tr("Cannot read addon archive version file: %1.").arg(VersionFile)); @@ -3687,7 +3687,7 @@ bool lcBlenderPreferences::ExtractAddon(const QString FileName, QString& Result) ZipFile.ExtractFile(FileIdx, MemFile); - QByteArray const& ByteArray = QByteArray::fromRawData((const char*)MemFile.mBuffer, MemFile.GetLength()); + QByteArray const& ByteArray = QByteArray::fromRawData((const char*)MemFile.mBuffer, (int)MemFile.GetLength()); // symlinks if (FileInfo.isSymLink) From 7eb6142b3c7c9250e4621ef8c6e166e2bfe37590 Mon Sep 17 00:00:00 2001 From: Leonardo Zide Date: Sun, 18 Jun 2023 18:12:12 -0700 Subject: [PATCH 12/14] Fixed typo. --- common/lc_commands.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lc_commands.cpp b/common/lc_commands.cpp index c9424e7c..3d616a98 100644 --- a/common/lc_commands.cpp +++ b/common/lc_commands.cpp @@ -118,7 +118,7 @@ const lcCommand gCommands[] = // LC_FILE_RENDER_BLENDER { QT_TRANSLATE_NOOP("Action", "File.Render.Blender"), - QT_TRANSLATE_NOOP("Menu", "&Bender Render..."), + QT_TRANSLATE_NOOP("Menu", "&Blender Render..."), QT_TRANSLATE_NOOP("Status", "Render the current model using Blender"), "" }, From eade4bcb5f973d3ed755bc3875d52aac93b2d72b Mon Sep 17 00:00:00 2001 From: Leonardo Zide Date: Sun, 18 Jun 2023 18:37:59 -0700 Subject: [PATCH 13/14] Removed default parameters. --- common/lc_model.cpp | 14 +++++++------- common/lc_model.h | 4 ++-- common/project.cpp | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/common/lc_model.cpp b/common/lc_model.cpp index 754c6014..843119b5 100644 --- a/common/lc_model.cpp +++ b/common/lc_model.cpp @@ -263,7 +263,7 @@ void lcModel::UpdatePieceInfo(std::vector& UpdatedModels) mPieceInfo->SetBoundingBox(Min, Max); } -void lcModel::SaveLDraw(QTextStream& Stream, bool SelectedOnly, lcStep CurrentStep) const +void lcModel::SaveLDraw(QTextStream& Stream, bool SelectedOnly, lcStep LastStep) const { const QLatin1String LineEnding("\r\n"); @@ -273,14 +273,14 @@ void lcModel::SaveLDraw(QTextStream& Stream, bool SelectedOnly, lcStep CurrentSt lcStep Step = 1; int CurrentLine = 0; int AddedSteps = 0; - bool Saved = false; + bool SavedStep = false; for (lcPiece* Piece : mPieces) { if (SelectedOnly && !Piece->IsSelected()) continue; - if ((Saved = CurrentStep && Piece->GetStepShow() > CurrentStep)) + if (SavedStep = (LastStep != 0 && Piece->GetStepShow() > LastStep)) break; while (Piece->GetFileLine() > CurrentLine && CurrentLine < mFileLines.size()) @@ -395,7 +395,7 @@ void lcModel::SaveLDraw(QTextStream& Stream, bool SelectedOnly, lcStep CurrentSt Stream << QLatin1String("0 !LEOCAD SYNTH END\r\n"); } - while ( !Saved && CurrentLine < mFileLines.size()) + while (!SavedStep && CurrentLine < mFileLines.size()) { QString Line = mFileLines[CurrentLine]; QTextStream LineStream(&Line, QIODevice::ReadOnly); @@ -1107,7 +1107,7 @@ void lcModel::Copy() QByteArray File; QTextStream Stream(&File, QIODevice::WriteOnly); - SaveLDraw(Stream, true); + SaveLDraw(Stream, true, 0); gApplication->ExportClipboard(File); } @@ -1301,7 +1301,7 @@ QImage lcModel::GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundC if (Step == 0) GetPartsList(gDefaultColor, true, false, PartsList); else - GetPartsListForStep(Step, gDefaultColor, PartsList); + GetPartsListForStep(Step, gDefaultColor, PartsList, false); if (PartsList.empty()) return QImage(); @@ -1629,7 +1629,7 @@ void lcModel::SaveCheckpoint(const QString& Description) ModelHistoryEntry->Description = Description; QTextStream Stream(&ModelHistoryEntry->File); - SaveLDraw(Stream, false); + SaveLDraw(Stream, false, 0); mUndoHistory.insert(mUndoHistory.begin(), ModelHistoryEntry); for (lcModelHistoryEntry* Entry : mRedoHistory) diff --git a/common/lc_model.h b/common/lc_model.h index da4c7a0f..5ce41599 100644 --- a/common/lc_model.h +++ b/common/lc_model.h @@ -205,7 +205,7 @@ public: void RemoveFocusPieceFromGroup(); void ShowEditGroupsDialog(); - void SaveLDraw(QTextStream& Stream, bool SelectedOnly, lcStep CurrentStep = 0) const; + void SaveLDraw(QTextStream& Stream, bool SelectedOnly, lcStep LastStep) const; void LoadLDraw(QIODevice& Device, Project* Project); bool LoadBinary(lcFile* File); bool LoadLDD(const QString& FileData); @@ -263,7 +263,7 @@ public: bool GetVisiblePiecesBoundingBox(lcVector3& Min, lcVector3& Max) const; std::vector GetPiecesBoundingBoxPoints() const; void GetPartsList(int DefaultColorIndex, bool ScanSubModels, bool AddSubModels, lcPartsList& PartsList) const; - void GetPartsListForStep(lcStep Step, int DefaultColorIndex, lcPartsList& PartsList, bool Cumulative = false) const; + void GetPartsListForStep(lcStep Step, int DefaultColorIndex, lcPartsList& PartsList, bool Cumulative) const; void GetModelParts(const lcMatrix44& WorldMatrix, int DefaultColorIndex, std::vector& ModelParts) const; void GetSelectionInformation(int* Flags, lcArray& Selection, lcObject** Focus) const; lcArray GetSelectionModePieces(const lcPiece* SelectedPiece) const; diff --git a/common/project.cpp b/common/project.cpp index 89733b72..5d7cc23c 100644 --- a/common/project.cpp +++ b/common/project.cpp @@ -299,7 +299,7 @@ void Project::ShowModelListDialog() QByteArray File; QTextStream SaveStream(&File); - Source->SaveLDraw(SaveStream, false); + Source->SaveLDraw(SaveStream, false, 0); SaveStream.flush(); QBuffer Buffer(&File); @@ -517,7 +517,7 @@ bool Project::Save(QTextStream& Stream) if (MPD) Stream << QLatin1String("0 FILE ") << Model->GetProperties().mFileName << QLatin1String("\r\n"); - Model->SaveLDraw(Stream, false); + Model->SaveLDraw(Stream, false, 0); Model->SetSaved(); if (MPD) @@ -767,7 +767,7 @@ bool Project::ExportModel(const QString& FileName, lcModel* Model) const QTextStream Stream(&File); - Model->SaveLDraw(Stream, false); + Model->SaveLDraw(Stream, false, 0); return true; } From 7271679f9dd94f27971342a5c3d02911a1dca4cf Mon Sep 17 00:00:00 2001 From: Leonardo Zide Date: Sun, 18 Jun 2023 19:06:44 -0700 Subject: [PATCH 14/14] Small changes to match conventions. --- common/lc_blenderpreferences.cpp | 4 +-- qt/lc_renderdialog.cpp | 62 ++++++++++++++++---------------- qt/lc_renderdialog.h | 12 +++---- 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/common/lc_blenderpreferences.cpp b/common/lc_blenderpreferences.cpp index 82033f3c..d1ae0383 100644 --- a/common/lc_blenderpreferences.cpp +++ b/common/lc_blenderpreferences.cpp @@ -1620,7 +1620,7 @@ void lcBlenderPreferences::ShowResult() QString const& Title = tr ("%1 Blender Addon Install").arg(LC_PRODUCTNAME_STR); QString const& Header = "" + tr ("Addon install failed.") + ""; QString const& Body = tr ("LDraw addon install encountered one or more errors. See Show Details..."); - ShowMessage(Header, Title, Body, StdErrLog, MBB_OK, QMessageBox::Critical); + ShowMessage(Header, Title, Body, StdErrLog, MBB_OK, QMessageBox::Warning); } else { @@ -1883,7 +1883,7 @@ void lcBlenderPreferences::ReadStdOut() QString const StdOutLog = QDir::toNativeSeparators(QString("
- See %1/Blender/stdout-blender-addon-install") .arg(mDataDir)); - QMessageBox::Icon Icon = Error ? QMessageBox::Critical : QMessageBox::Warning; + QMessageBox::Icon Icon = QMessageBox::Warning; QString const& Items = Error ? tr("errors%1").arg(Warning ? tr(" and warnings") : "") : Warning ? tr("warnings") : ""; QString const& Title = tr ("%1 Blender Addon Install").arg(LC_PRODUCTNAME_STR); diff --git a/qt/lc_renderdialog.cpp b/qt/lc_renderdialog.cpp index 36138a45..4187ef7a 100644 --- a/qt/lc_renderdialog.cpp +++ b/qt/lc_renderdialog.cpp @@ -18,7 +18,7 @@ #define LC_POVRAY_MEMORY_MAPPED_FILE 1 #endif -static inline QString ElapsedTime(const qint64 &Duration) +static inline QString ElapsedTime(const qint64& Duration) { qint64 Elapsed = Duration; int Milliseconds = int(Elapsed % 1000); @@ -179,11 +179,17 @@ lcRenderDialog::~lcRenderDialog() delete ui; } -QString lcRenderDialog::GetOutputFileName(int StdErr) const +QString lcRenderDialog::GetStdOutFileName() const { - QString LogFile = mCommand == POVRAY_RENDER - ? StdErr ? QLatin1String("leocad-povray-render.err") : QLatin1String("leocad-povray-render.out") - : StdErr ? QLatin1String("leocad-blender-render.err") : QLatin1String("leocad-blender-render.out"); + QString LogFile = mCommand == POVRAY_RENDER ? QLatin1String("leocad-povray-render.out") : QLatin1String("leocad-blender-render.out"); + + return QDir(QDir::tempPath()).absoluteFilePath(LogFile); +} + +QString lcRenderDialog::GetStdErrFileName() const +{ + QString LogFile = mCommand == POVRAY_RENDER ? QLatin1String("leocad-povray-render.err") : QLatin1String("leocad-blender-render.err"); + return QDir(QDir::tempPath()).absoluteFilePath(LogFile); } @@ -221,7 +227,7 @@ void lcRenderDialog::CloseProcess() mOutputBuffer = nullptr; mOutputFile.close(); - QFile::remove(GetOutputFileName()); + QFile::remove(GetStdOutFileName()); #endif QFile::remove(GetPOVFileName()); @@ -322,7 +328,7 @@ void lcRenderDialog::on_RenderButton_clicked() Arguments.append("-O-"); #if LC_POVRAY_MEMORY_MAPPED_FILE - Arguments.append(QString::fromLatin1("+SM\"%1\"").arg(GetOutputFileName())); + Arguments.append(QString::fromLatin1("+SM\"%1\"").arg(GetStdOutFileName())); #endif int Quality = ui->QualityComboBox->currentIndex(); @@ -369,15 +375,14 @@ void lcRenderDialog::on_RenderButton_clicked() POVRayPath = QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/povray")); #endif - mProcess = new RenderProcess(this); + mProcess = new lcRenderProcess(this); #ifdef Q_OS_LINUX connect(mProcess, SIGNAL(readyReadStandardError()), this, SLOT(ReadStdErr())); #endif - int const StdErr = 1; QStringList POVEnv = QProcess::systemEnvironment(); POVEnv.prepend("POV_IGNORE_SYSCONF_MSG=1"); mProcess->setEnvironment(POVEnv); - mProcess->setStandardErrorFile(GetOutputFileName(StdErr)); + mProcess->setStandardErrorFile(GetStdErrFileName()); mProcess->start(POVRayPath, Arguments); mImage = QImage(ui->WidthEdit->text().toInt(), ui->HeightEdit->text().toInt(), QImage::Format_ARGB32); @@ -413,7 +418,7 @@ void lcRenderDialog::on_RenderButton_clicked() mBlendProgValue = 0; mBlendProgMax = 0; - QStringList const &DataPathList = QStandardPaths::standardLocations(QStandardPaths::DataLocation); + const QStringList DataPathList = QStandardPaths::standardLocations(QStandardPaths::DataLocation); mDataPath = DataPathList.first(); QString const DefaultBlendFile = QString("%1/blender/config/%2").arg(mDataPath).arg(LC_BLENDER_ADDON_BLEND_FILE); @@ -473,7 +478,7 @@ void lcRenderDialog::on_RenderButton_clicked() ScriptCommand = QString("%1 %2").arg(lcGetProfileString(LC_PROFILE_BLENDER_PATH)).arg(Arguments.join(" ")); if (mCommand == OPEN_IN_BLENDER) - ScriptCommand.append(QString(" > %1").arg(QDir::toNativeSeparators(GetOutputFileName()))); + ScriptCommand.append(QString(" > %1").arg(QDir::toNativeSeparators(GetStdOutFileName()))); const QLatin1String LineEnding("\r\n"); @@ -507,24 +512,23 @@ void lcRenderDialog::on_RenderButton_clicked() ShellProgram = QLatin1String(LC_UNIX_SHELL); #endif - mProcess = new RenderProcess(this); + mProcess = new lcRenderProcess(this); connect(mProcess, SIGNAL(readyReadStandardOutput()), this, SLOT(ReadStdOut())); QString const LDrawLibPath = QFileInfo(lcGetProfileString(LC_PROFILE_PARTS_LIBRARY)).absolutePath(); QStringList SystemEnvironment = QProcess::systemEnvironment(); SystemEnvironment.prepend("LDRAW_DIRECTORY=" + LDrawLibPath); - int const StdErr = 1; mProcess->setEnvironment(SystemEnvironment); mProcess->setWorkingDirectory(QDir::toNativeSeparators(QString("%1/blender").arg(mDataPath))); - mProcess->setStandardErrorFile(GetOutputFileName(StdErr)); + mProcess->setStandardErrorFile(GetStdErrFileName()); if (mCommand == OPEN_IN_BLENDER) { - QFileInfo Info(GetOutputFileName()); + QFileInfo Info(GetStdOutFileName()); if (Info.exists()) QFile::remove(Info.absoluteFilePath()); #ifdef Q_OS_WIN @@ -552,10 +556,10 @@ void lcRenderDialog::on_RenderButton_clicked() bool const Warning = QString(Ba).contains(QRegExp("(?:\\w)*WARNING: ", Qt::CaseInsensitive)); if (Error || Warning) { - QMessageBox::Icon Icon = Error ? QMessageBox::Critical : QMessageBox::Warning; - QString const &Items = Error ? tr("errors%1").arg(Warning ? tr(" and warnings") : "") : Warning ? tr("warnings") : ""; - QString const &Title = tr("Open in Blender output"); - QString const &Body = tr("Open in Blender encountered %1. See Show Details...").arg(Items); + QMessageBox::Icon Icon = QMessageBox::Warning; + const QString Items = Error ? tr("errors%1").arg(Warning ? tr(" and warnings") : "") : Warning ? tr("warnings") : ""; + const QString Title = tr("Open in Blender output"); + const QString Body = tr("Open in Blender encountered %1. See Show Details...").arg(Items); lcBlenderPreferences::ShowMessage(Body, Title, QString(), QString(Ba), 0, Icon); } } @@ -663,15 +667,14 @@ void lcRenderDialog::ReadStdOut() } } -QString lcRenderDialog::ReadStdErr(bool &HasError) const +QString lcRenderDialog::ReadStdErr(bool& HasError) const { HasError = mCommand == BLENDER_RENDER ? false : true; QFile File; QStringList returnLines; - int const StdErr = 1; - File.setFileName(GetOutputFileName(StdErr)); + File.setFileName(GetStdErrFileName()); if (! File.open(QFile::ReadOnly | QFile::Text)) { @@ -703,7 +706,7 @@ QString lcRenderDialog::ReadStdErr(bool &HasError) const void lcRenderDialog::WriteStdOut() { - QFile File(GetOutputFileName()); + QFile File(GetStdOutFileName()); if (File.open(QFile::WriteOnly | QIODevice::Truncate | QFile::Text)) { @@ -741,7 +744,7 @@ void lcRenderDialog::Update() #if LC_POVRAY_MEMORY_MAPPED_FILE if (!mOutputBuffer) { - mOutputFile.setFileName(GetOutputFileName()); + mOutputFile.setFileName(GetStdOutFileName()); if (!mOutputFile.open(QFile::ReadWrite)) return; @@ -817,8 +820,8 @@ void lcRenderDialog::ShowResult() ui->RenderProgress->setRange(0,1); ui->RenderProgress->setValue(0); - QString const &Title = mCommand == BLENDER_RENDER ? tr("Blender Render") : tr("POV-Ray Render"); - QString const &Body = tr ("An error occurred while rendering. See Show Details..."); + QString const Title = mCommand == BLENDER_RENDER ? tr("Blender Render") : tr("POV-Ray Render"); + QString const Body = tr ("An error occurred while rendering. See Show Details..."); lcBlenderPreferences::ShowMessage(Body, Title, QString(), StdErrLog, 0, QMessageBox::Warning); return; } @@ -891,7 +894,7 @@ void lcRenderDialog::on_OutputBrowseButton_clicked() void lcRenderDialog::on_RenderOutputButton_clicked() { QString RenderType = mCommand == POVRAY_RENDER ? QLatin1String("POV-Ray") : QLatin1String("Blender"); - QFileInfo FileInfo(GetOutputFileName()); + QFileInfo FileInfo(GetStdOutFileName()); if (!FileInfo.exists()) { QMessageBox::warning(this, tr("Error"), tr("%1 Standard output file not found: %2.") @@ -902,8 +905,7 @@ void lcRenderDialog::on_RenderOutputButton_clicked() QDesktopServices::openUrl(QUrl("file:///"+FileInfo.absoluteFilePath(), QUrl::TolerantMode)); } - -RenderProcess::~RenderProcess() +lcRenderProcess::~lcRenderProcess() { if(state() == QProcess::Running || state() == QProcess::Starting) { diff --git a/qt/lc_renderdialog.h b/qt/lc_renderdialog.h index a3d709a3..fb36c24e 100644 --- a/qt/lc_renderdialog.h +++ b/qt/lc_renderdialog.h @@ -6,17 +6,16 @@ namespace Ui { class lcRenderDialog; } -class QProcess; -class RenderProcess : public QProcess +class lcRenderProcess : public QProcess { Q_OBJECT public: - explicit RenderProcess(QObject* parent = nullptr) + explicit lcRenderProcess(QObject* parent = nullptr) : QProcess(parent) { } - ~RenderProcess(); + ~lcRenderProcess(); }; class lcRenderPreviewWidget : public QWidget @@ -66,7 +65,8 @@ protected slots: void UpdateElapsedTime() const; protected: - QString GetOutputFileName(int StdErr = 0) const; + QString GetStdOutFileName() const; + QString GetStdErrFileName() const; QString GetPOVFileName() const; QString ReadStdErr(bool& Error) const; void CloseProcess(); @@ -76,7 +76,7 @@ protected: int TerminateChildProcess(const qint64 Pid, const qint64 Ppid); #endif #ifndef QT_NO_PROCESS - RenderProcess* mProcess; + lcRenderProcess* mProcess; #endif enum CommandType {