Improve assets folder selection and fix macOS tkinter crash
- Replace tkinter with PyQt5 QFileDialog as primary folder selector to fix macOS crashes - Add specialized assets_folder property handling in dashboard with validation - Integrate improved folder dialog utility with ExactModelNode - Provide detailed validation feedback and user-friendly tooltips - Maintain backward compatibility with tkinter as fallback 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
48acae9c74
commit
ec940c3f2f
@ -305,47 +305,46 @@ class ExactModelNode(BaseNode):
|
|||||||
print(f"Warning: Could not setup custom property handlers: {e}")
|
print(f"Warning: Could not setup custom property handlers: {e}")
|
||||||
|
|
||||||
def select_assets_folder(self):
|
def select_assets_folder(self):
|
||||||
"""Method to open folder selection dialog for assets folder using tkinter."""
|
"""Method to open folder selection dialog for assets folder using improved utility."""
|
||||||
if not NODEGRAPH_AVAILABLE:
|
if not NODEGRAPH_AVAILABLE:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import tkinter as tk
|
from utils.folder_dialog import select_assets_folder
|
||||||
from tkinter import filedialog
|
|
||||||
|
|
||||||
# Create a root window but keep it hidden
|
# Get current folder path as initial directory
|
||||||
root = tk.Tk()
|
current_folder = ""
|
||||||
root.withdraw() # Hide the main window
|
try:
|
||||||
root.attributes('-topmost', True) # Bring dialog to front
|
current_folder = self.get_property('assets_folder') or ""
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# Open folder selection dialog
|
# Use the specialized assets folder dialog with validation
|
||||||
folder_path = filedialog.askdirectory(
|
result = select_assets_folder(initial_dir=current_folder)
|
||||||
title="Select Assets Folder",
|
|
||||||
initialdir="",
|
|
||||||
mustexist=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Destroy the root window
|
if result['path']:
|
||||||
root.destroy()
|
# Set the property
|
||||||
|
if NODEGRAPH_AVAILABLE:
|
||||||
if folder_path:
|
self.set_property('assets_folder', result['path'])
|
||||||
# Validate the selected folder structure
|
|
||||||
if self._validate_assets_folder(folder_path):
|
# Print validation results
|
||||||
# Set the property
|
if result['valid']:
|
||||||
if NODEGRAPH_AVAILABLE:
|
print(f"✓ Valid Assets folder set to: {result['path']}")
|
||||||
self.set_property('assets_folder', folder_path)
|
if 'details' in result and 'available_series' in result['details']:
|
||||||
print(f"Assets folder set to: {folder_path}")
|
series = result['details']['available_series']
|
||||||
return folder_path
|
print(f" Available series: {', '.join(series)}")
|
||||||
else:
|
else:
|
||||||
print(f"Warning: Selected folder does not have the expected structure")
|
print(f"⚠ Assets folder set to: {result['path']}")
|
||||||
print("Expected structure: Assets/Firmware/ and Assets/Models/ with series subfolders")
|
print(f" Warning: {result['message']}")
|
||||||
# Still set it, but warn user
|
print(" Expected structure: Assets/Firmware/ and Assets/Models/ with series subfolders")
|
||||||
if NODEGRAPH_AVAILABLE:
|
|
||||||
self.set_property('assets_folder', folder_path)
|
return result['path']
|
||||||
return folder_path
|
else:
|
||||||
|
print("No folder selected")
|
||||||
|
return ""
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("tkinter not available, falling back to simple input")
|
print("utils.folder_dialog not available, falling back to simple input")
|
||||||
# Fallback to manual input
|
# Fallback to manual input
|
||||||
folder_path = input("Enter Assets folder path: ").strip()
|
folder_path = input("Enter Assets folder path: ").strip()
|
||||||
if folder_path and NODEGRAPH_AVAILABLE:
|
if folder_path and NODEGRAPH_AVAILABLE:
|
||||||
|
|||||||
193
mutliseries.py
193
mutliseries.py
@ -1,193 +0,0 @@
|
|||||||
import kp
|
|
||||||
from collections import defaultdict
|
|
||||||
from typing import Union
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import queue
|
|
||||||
import numpy as np
|
|
||||||
import cv2
|
|
||||||
|
|
||||||
# PWD = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
# sys.path.insert(1, os.path.join(PWD, '..'))
|
|
||||||
IMAGE_FILE_PATH = r"c:\Users\mason\Downloads\kneron_plus_v3.1.2\kneron_plus\res\images\people_talk_in_street_640x640.bmp"
|
|
||||||
LOOP_TIME = 100
|
|
||||||
|
|
||||||
|
|
||||||
def _image_send_function(_device_group: kp.DeviceGroup,
|
|
||||||
_loop_time: int,
|
|
||||||
_generic_inference_input_descriptor: kp.GenericImageInferenceDescriptor,
|
|
||||||
_image: Union[bytes, np.ndarray],
|
|
||||||
_image_format: kp.ImageFormat) -> None:
|
|
||||||
for _loop in range(_loop_time):
|
|
||||||
try:
|
|
||||||
_generic_inference_input_descriptor.inference_number = _loop
|
|
||||||
_generic_inference_input_descriptor.input_node_image_list = [kp.GenericInputNodeImage(
|
|
||||||
image=_image,
|
|
||||||
image_format=_image_format,
|
|
||||||
resize_mode=kp.ResizeMode.KP_RESIZE_ENABLE,
|
|
||||||
padding_mode=kp.PaddingMode.KP_PADDING_CORNER,
|
|
||||||
normalize_mode=kp.NormalizeMode.KP_NORMALIZE_KNERON
|
|
||||||
)]
|
|
||||||
|
|
||||||
kp.inference.generic_image_inference_send(device_group=device_groups[1],
|
|
||||||
generic_inference_input_descriptor=_generic_inference_input_descriptor)
|
|
||||||
except kp.ApiKPException as exception:
|
|
||||||
print(' - Error: inference failed, error = {}'.format(exception))
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
def _result_receive_function(_device_group: kp.DeviceGroup,
|
|
||||||
_loop_time: int,
|
|
||||||
_result_queue: queue.Queue) -> None:
|
|
||||||
_generic_raw_result = None
|
|
||||||
|
|
||||||
for _loop in range(_loop_time):
|
|
||||||
try:
|
|
||||||
_generic_raw_result = kp.inference.generic_image_inference_receive(device_group=device_groups[1])
|
|
||||||
|
|
||||||
if _generic_raw_result.header.inference_number != _loop:
|
|
||||||
print(' - Error: incorrect inference_number {} at frame {}'.format(
|
|
||||||
_generic_raw_result.header.inference_number, _loop))
|
|
||||||
|
|
||||||
print('.', end='', flush=True)
|
|
||||||
|
|
||||||
except kp.ApiKPException as exception:
|
|
||||||
print(' - Error: inference failed, error = {}'.format(exception))
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
_result_queue.put(_generic_raw_result)
|
|
||||||
|
|
||||||
model_path = ["C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\models\\KL520\\yolov5-noupsample_w640h640_kn-model-zoo\\kl520_20005_yolov5-noupsample_w640h640.nef", r"C:\Users\mason\Downloads\kneron_plus_v3.1.2\kneron_plus\res\models\KL720\yolov5-noupsample_w640h640_kn-model-zoo\kl720_20005_yolov5-noupsample_w640h640.nef"]
|
|
||||||
SCPU_FW_PATH_520 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL520\\fw_scpu.bin"
|
|
||||||
NCPU_FW_PATH_520 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL520\\fw_ncpu.bin"
|
|
||||||
SCPU_FW_PATH_720 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL720\\fw_scpu.bin"
|
|
||||||
NCPU_FW_PATH_720 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL720\\fw_ncpu.bin"
|
|
||||||
device_list = kp.core.scan_devices()
|
|
||||||
|
|
||||||
grouped_devices = defaultdict(list)
|
|
||||||
|
|
||||||
for device in device_list.device_descriptor_list:
|
|
||||||
grouped_devices[device.product_id].append(device.usb_port_id)
|
|
||||||
|
|
||||||
print(f"Found device groups: {dict(grouped_devices)}")
|
|
||||||
|
|
||||||
device_groups = []
|
|
||||||
|
|
||||||
for product_id, usb_port_id in grouped_devices.items():
|
|
||||||
try:
|
|
||||||
group = kp.core.connect_devices(usb_port_id)
|
|
||||||
device_groups.append(group)
|
|
||||||
print(f"Successfully connected to group for product ID {product_id} with ports{usb_port_id}")
|
|
||||||
except kp.ApiKPException as e:
|
|
||||||
print(f"Failed to connect to group for product ID {product_id}: {e}")
|
|
||||||
|
|
||||||
print(device_groups)
|
|
||||||
|
|
||||||
print('[Set Device Timeout]')
|
|
||||||
kp.core.set_timeout(device_group=device_groups[0], milliseconds=5000)
|
|
||||||
kp.core.set_timeout(device_group=device_groups[1], milliseconds=5000)
|
|
||||||
print(' - Success')
|
|
||||||
|
|
||||||
try:
|
|
||||||
print('[Upload Firmware]')
|
|
||||||
kp.core.load_firmware_from_file(device_group=device_groups[0],
|
|
||||||
scpu_fw_path=SCPU_FW_PATH_520,
|
|
||||||
ncpu_fw_path=NCPU_FW_PATH_520)
|
|
||||||
kp.core.load_firmware_from_file(device_group=device_groups[1],
|
|
||||||
scpu_fw_path=SCPU_FW_PATH_720,
|
|
||||||
ncpu_fw_path=NCPU_FW_PATH_720)
|
|
||||||
print(' - Success')
|
|
||||||
except kp.ApiKPException as exception:
|
|
||||||
print('Error: upload firmware failed, error = \'{}\''.format(str(exception)))
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
print('[Upload Model]')
|
|
||||||
model_nef_descriptors = []
|
|
||||||
# for group in device_groups:
|
|
||||||
model_nef_descriptor = kp.core.load_model_from_file(device_group=device_groups[0], file_path=model_path[0])
|
|
||||||
model_nef_descriptors.append(model_nef_descriptor)
|
|
||||||
model_nef_descriptor = kp.core.load_model_from_file(device_group=device_groups[1], file_path=model_path[1])
|
|
||||||
model_nef_descriptors.append(model_nef_descriptor)
|
|
||||||
print(' - Success')
|
|
||||||
|
|
||||||
"""
|
|
||||||
prepare the image
|
|
||||||
"""
|
|
||||||
print('[Read Image]')
|
|
||||||
img = cv2.imread(filename=IMAGE_FILE_PATH)
|
|
||||||
img_bgr565 = cv2.cvtColor(src=img, code=cv2.COLOR_BGR2BGR565)
|
|
||||||
print(' - Success')
|
|
||||||
|
|
||||||
"""
|
|
||||||
prepare generic image inference input descriptor
|
|
||||||
"""
|
|
||||||
print(model_nef_descriptors)
|
|
||||||
generic_inference_input_descriptor = kp.GenericImageInferenceDescriptor(
|
|
||||||
model_id=model_nef_descriptors[1].models[0].id,
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
starting inference work
|
|
||||||
"""
|
|
||||||
print('[Starting Inference Work]')
|
|
||||||
print(' - Starting inference loop {} times'.format(LOOP_TIME))
|
|
||||||
print(' - ', end='')
|
|
||||||
result_queue = queue.Queue()
|
|
||||||
|
|
||||||
send_thread = threading.Thread(target=_image_send_function, args=(device_groups[1],
|
|
||||||
LOOP_TIME,
|
|
||||||
generic_inference_input_descriptor,
|
|
||||||
img_bgr565,
|
|
||||||
kp.ImageFormat.KP_IMAGE_FORMAT_RGB565))
|
|
||||||
|
|
||||||
receive_thread = threading.Thread(target=_result_receive_function, args=(device_groups[1],
|
|
||||||
LOOP_TIME,
|
|
||||||
result_queue))
|
|
||||||
|
|
||||||
start_inference_time = time.time()
|
|
||||||
|
|
||||||
send_thread.start()
|
|
||||||
receive_thread.start()
|
|
||||||
|
|
||||||
try:
|
|
||||||
while send_thread.is_alive():
|
|
||||||
send_thread.join(1)
|
|
||||||
|
|
||||||
while receive_thread.is_alive():
|
|
||||||
receive_thread.join(1)
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
|
||||||
print('\n - Received keyboard interrupt, quitting threads.')
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
end_inference_time = time.time()
|
|
||||||
time_spent = end_inference_time - start_inference_time
|
|
||||||
|
|
||||||
try:
|
|
||||||
generic_raw_result = result_queue.get(timeout=3)
|
|
||||||
except Exception as exception:
|
|
||||||
print('Error: Result queue is empty !')
|
|
||||||
exit(0)
|
|
||||||
print()
|
|
||||||
|
|
||||||
print('[Result]')
|
|
||||||
print(" - Total inference {} images".format(LOOP_TIME))
|
|
||||||
print(" - Time spent: {:.2f} secs, FPS = {:.1f}".format(time_spent, LOOP_TIME / time_spent))
|
|
||||||
|
|
||||||
"""
|
|
||||||
retrieve inference node output
|
|
||||||
"""
|
|
||||||
print('[Retrieve Inference Node Output ]')
|
|
||||||
inf_node_output_list = []
|
|
||||||
for node_idx in range(generic_raw_result.header.num_output_node):
|
|
||||||
inference_float_node_output = kp.inference.generic_inference_retrieve_float_node(node_idx=node_idx,
|
|
||||||
generic_raw_result=generic_raw_result,
|
|
||||||
channels_ordering=kp.ChannelOrdering.KP_CHANNEL_ORDERING_CHW)
|
|
||||||
inf_node_output_list.append(inference_float_node_output)
|
|
||||||
|
|
||||||
print(' - Success')
|
|
||||||
|
|
||||||
print('[Result]')
|
|
||||||
print(inf_node_output_list)
|
|
||||||
@ -43,6 +43,7 @@ except ImportError:
|
|||||||
|
|
||||||
from config.theme import HARMONIOUS_THEME_STYLESHEET
|
from config.theme import HARMONIOUS_THEME_STYLESHEET
|
||||||
from config.settings import get_settings
|
from config.settings import get_settings
|
||||||
|
from utils.folder_dialog import select_assets_folder
|
||||||
try:
|
try:
|
||||||
from core.nodes import (
|
from core.nodes import (
|
||||||
InputNode, ModelNode, PreprocessNode, PostprocessNode, OutputNode,
|
InputNode, ModelNode, PreprocessNode, PostprocessNode, OutputNode,
|
||||||
@ -1323,8 +1324,74 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
if hasattr(node, '_property_options') and prop_name in node._property_options:
|
if hasattr(node, '_property_options') and prop_name in node._property_options:
|
||||||
prop_options = node._property_options[prop_name]
|
prop_options = node._property_options[prop_name]
|
||||||
|
|
||||||
# Check for file path properties first (from prop_options or name pattern)
|
# Special handling for assets_folder property
|
||||||
if (prop_options and isinstance(prop_options, dict) and prop_options.get('type') == 'file_path') or \
|
if prop_name == 'assets_folder':
|
||||||
|
# Assets folder property with validation and improved dialog
|
||||||
|
display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select Assets Folder...'
|
||||||
|
widget = QPushButton(display_text)
|
||||||
|
|
||||||
|
# Set fixed width and styling to prevent expansion
|
||||||
|
widget.setMaximumWidth(250)
|
||||||
|
widget.setMinimumWidth(200)
|
||||||
|
widget.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
text-align: left;
|
||||||
|
padding: 5px 8px;
|
||||||
|
background-color: #45475a;
|
||||||
|
color: #cdd6f4;
|
||||||
|
border: 1px solid #585b70;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #585b70;
|
||||||
|
border-color: #a6e3a1;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #313244;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Store full path for tooltip and internal use
|
||||||
|
full_path = str(prop_value) if prop_value else ''
|
||||||
|
widget.setToolTip(f"Full path: {full_path}\n\nClick to browse for Assets folder\n(Should contain Firmware/ and Models/ subfolders)")
|
||||||
|
|
||||||
|
def browse_assets_folder():
|
||||||
|
# Use the specialized assets folder dialog with validation
|
||||||
|
result = select_assets_folder(initial_dir=full_path or '')
|
||||||
|
|
||||||
|
if result['path']:
|
||||||
|
# Update button text with truncated path
|
||||||
|
truncated_text = self.truncate_path_smart(result['path'])
|
||||||
|
widget.setText(truncated_text)
|
||||||
|
|
||||||
|
# Create detailed tooltip with validation results
|
||||||
|
tooltip_lines = [f"Full path: {result['path']}"]
|
||||||
|
if result['valid']:
|
||||||
|
tooltip_lines.append("✓ Valid Assets folder structure detected")
|
||||||
|
if 'details' in result and 'available_series' in result['details']:
|
||||||
|
series = result['details']['available_series']
|
||||||
|
tooltip_lines.append(f"Available series: {', '.join(series)}")
|
||||||
|
else:
|
||||||
|
tooltip_lines.append(f"⚠ {result['message']}")
|
||||||
|
|
||||||
|
tooltip_lines.append("\nClick to browse for Assets folder")
|
||||||
|
widget.setToolTip('\n'.join(tooltip_lines))
|
||||||
|
|
||||||
|
# Set property with full path
|
||||||
|
if hasattr(node, 'set_property'):
|
||||||
|
node.set_property(prop_name, result['path'])
|
||||||
|
|
||||||
|
# Show validation message to user
|
||||||
|
if not result['valid']:
|
||||||
|
QMessageBox.warning(self, "Assets Folder Validation",
|
||||||
|
f"Selected folder may not have the expected structure:\n\n{result['message']}\n\n"
|
||||||
|
"Expected structure:\nAssets/\n├── Firmware/\n│ └── KL520/, KL720/, etc.\n└── Models/\n └── KL520/, KL720/, etc.")
|
||||||
|
|
||||||
|
widget.clicked.connect(browse_assets_folder)
|
||||||
|
|
||||||
|
# Check for file path properties (from prop_options or name pattern)
|
||||||
|
elif (prop_options and isinstance(prop_options, dict) and prop_options.get('type') == 'file_path') or \
|
||||||
prop_name in ['model_path', 'source_path', 'destination']:
|
prop_name in ['model_path', 'source_path', 'destination']:
|
||||||
# File path property with smart truncation and width limits
|
# File path property with smart truncation and width limits
|
||||||
display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select File...'
|
display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select File...'
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Folder selection utilities using tkinter
|
Folder selection utilities using PyQt5 as primary, tkinter as fallback
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import filedialog
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
def select_folder(title="Select Folder", initial_dir="", must_exist=True):
|
def select_folder(title="Select Folder", initial_dir="", must_exist=True):
|
||||||
"""
|
"""
|
||||||
Open a folder selection dialog using tkinter
|
Open a folder selection dialog using PyQt5 (preferred) or tkinter (fallback)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
title (str): Dialog window title
|
title (str): Dialog window title
|
||||||
@ -18,33 +16,70 @@ def select_folder(title="Select Folder", initial_dir="", must_exist=True):
|
|||||||
Returns:
|
Returns:
|
||||||
str: Selected folder path, or empty string if cancelled
|
str: Selected folder path, or empty string if cancelled
|
||||||
"""
|
"""
|
||||||
|
# Try PyQt5 first (more reliable on macOS)
|
||||||
try:
|
try:
|
||||||
# Create a root window but keep it hidden
|
from PyQt5.QtWidgets import QApplication, QFileDialog
|
||||||
root = tk.Tk()
|
import sys
|
||||||
root.withdraw() # Hide the main window
|
|
||||||
root.attributes('-topmost', True) # Bring dialog to front
|
# Create QApplication if it doesn't exist
|
||||||
|
app = QApplication.instance()
|
||||||
|
if app is None:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
# Set initial directory
|
# Set initial directory
|
||||||
if not initial_dir:
|
if not initial_dir:
|
||||||
initial_dir = os.getcwd()
|
initial_dir = os.getcwd()
|
||||||
|
elif not os.path.exists(initial_dir):
|
||||||
|
initial_dir = os.getcwd()
|
||||||
|
|
||||||
# Open folder selection dialog
|
# Open folder selection dialog
|
||||||
folder_path = filedialog.askdirectory(
|
folder_path = QFileDialog.getExistingDirectory(
|
||||||
title=title,
|
None,
|
||||||
initialdir=initial_dir,
|
title,
|
||||||
mustexist=must_exist
|
initial_dir,
|
||||||
|
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
|
||||||
)
|
)
|
||||||
|
|
||||||
# Destroy the root window
|
|
||||||
root.destroy()
|
|
||||||
|
|
||||||
return folder_path if folder_path else ""
|
return folder_path if folder_path else ""
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("tkinter not available")
|
print("PyQt5 not available, trying tkinter...")
|
||||||
return ""
|
|
||||||
|
# Fallback to tkinter
|
||||||
|
try:
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import filedialog
|
||||||
|
|
||||||
|
# Create a root window but keep it hidden
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw() # Hide the main window
|
||||||
|
root.attributes('-topmost', True) # Bring dialog to front
|
||||||
|
|
||||||
|
# Set initial directory
|
||||||
|
if not initial_dir:
|
||||||
|
initial_dir = os.getcwd()
|
||||||
|
|
||||||
|
# Open folder selection dialog
|
||||||
|
folder_path = filedialog.askdirectory(
|
||||||
|
title=title,
|
||||||
|
initialdir=initial_dir,
|
||||||
|
mustexist=must_exist
|
||||||
|
)
|
||||||
|
|
||||||
|
# Destroy the root window
|
||||||
|
root.destroy()
|
||||||
|
|
||||||
|
return folder_path if folder_path else ""
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("tkinter also not available")
|
||||||
|
return ""
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error opening tkinter folder dialog: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error opening folder dialog: {e}")
|
print(f"Error opening PyQt5 folder dialog: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def select_assets_folder(initial_dir=""):
|
def select_assets_folder(initial_dir=""):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user