Skip to content

Commit 28ee455

Browse files
committed
Option to change threads folder
1 parent 226370c commit 28ee455

File tree

2 files changed

+109
-11
lines changed

2 files changed

+109
-11
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
32x32-disabled.png
44
release
55
/Threads
6+
settings.json

ThreadKeeper.py

Lines changed: 108 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,48 +38,62 @@
3838
# loaded by other add-ins
3939
from .thomasa88lib import utils
4040
from .thomasa88lib import events
41-
#from .thomasa88lib import timeline
4241
from .thomasa88lib import manifest
4342
from .thomasa88lib import error
43+
from .thomasa88lib import settings
4444

4545
# Force modules to be fresh during development
4646
import importlib
4747
importlib.reload(thomasa88lib.utils)
4848
importlib.reload(thomasa88lib.events)
49-
#importlib.reload(thomasa88lib.timeline)
5049
importlib.reload(thomasa88lib.manifest)
5150
importlib.reload(thomasa88lib.error)
51+
importlib.reload(thomasa88lib.settings)
5252

5353
PANEL_ID = 'thomasa88_ThreadKeeperPanel'
5454
DIRECTORY_CMD_DEF_ID = 'thomasa88_ThreadKeeperDirectory'
5555
FUSION_DIRECTORY_CMD_DEF_ID = 'thomasa88_ThreadKeeperFusionDirectory'
5656
FORCE_SYNC_CMD_DEF_ID = 'thomasa88_ThreadKeeperForceSync'
57+
CHANGE_DIR_CMD_DEF_ID = 'thomasa88_ThreadKeeperChangeDirectory'
5758

5859
app_ = None
5960
ui_ = None
6061
panel_ = None
61-
local_thread_dir_ = None
6262
fusion_thread_dir_ = None
6363
platform_ = platform.system()
64+
default_thread_dir_ = str(pathlib.Path(thomasa88lib.utils.get_file_dir()) / 'Threads')
6465

6566
error_catcher_ = thomasa88lib.error.ErrorCatcher(msgbox_in_debug=False)
6667
events_manager_ = thomasa88lib.events.EventsManager(error_catcher_)
6768
manifest_ = thomasa88lib.manifest.read()
6869

70+
settings_ = thomasa88lib.settings.SettingsManager({
71+
# Storing None, for the unlikely case that the add-ins directory
72+
# is changed in a newer Fusion 360 version
73+
'thread_directory': None
74+
})
75+
76+
def get_thread_dir():
77+
thread_dir = settings_['thread_directory']
78+
if thread_dir is None:
79+
thread_dir = default_thread_dir_
80+
return pathlib.Path(thread_dir)
81+
82+
def set_thread_dir(path):
83+
settings_['thread_directory'] = str(path)
6984

7085
def run(context):
7186
global app_
7287
global ui_
7388
global panel_
74-
global local_thread_dir_
7589
global fusion_thread_dir_
7690
with error_catcher_:
7791
app_ = adsk.core.Application.get()
7892
ui_ = app_.userInterface
7993

80-
local_thread_dir_ = pathlib.Path(thomasa88lib.utils.get_file_dir()) / 'Threads'
81-
if not os.path.exists(local_thread_dir_):
82-
os.mkdir(local_thread_dir_)
94+
local_thread_dir = get_thread_dir()
95+
if not os.path.exists(local_thread_dir):
96+
os.mkdir(local_thread_dir)
8397

8498
# https://knowledge.autodesk.com/support/fusion-360/learn-explore/caas/sfdcarticles/sfdcarticles/Custom-Threads-in-Fusion-360.html
8599
# Windows location:
@@ -111,7 +125,7 @@ def run(context):
111125
'Put threads that you want to keep into this directory.',
112126
'./resources/thread_folder')
113127
events_manager_.add_handler(directory_cmd_def.commandCreated,
114-
callback=lambda args: open_folder(local_thread_dir_))
128+
callback=lambda args: open_folder(get_thread_dir()))
115129
directory_control = panel_.controls.addCommand(directory_cmd_def)
116130
directory_control.isPromoted = True
117131
directory_control.isPromotedByDefault = True
@@ -148,6 +162,22 @@ def run(context):
148162
callback=force_sync_handler)
149163
panel_.controls.addCommand(force_sync_cmd_def)
150164

165+
panel_.controls.addSeparator()
166+
167+
change_dir_cmd_def = ui_.commandDefinitions.itemById(CHANGE_DIR_CMD_DEF_ID)
168+
if change_dir_cmd_def:
169+
change_dir_cmd_def.deleteMe()
170+
change_dir_cmd_def = ui_.commandDefinitions.addButtonDefinition(CHANGE_DIR_CMD_DEF_ID,
171+
'Change ThreadKeeper directory...',
172+
f'Change which directory {NAME} uses to store '
173+
'a backup of your threads.\n\n'
174+
f'Current {NAME} directory content will be '
175+
'copied to the new directory.',
176+
'')
177+
events_manager_.add_handler(change_dir_cmd_def.commandCreated,
178+
callback=change_dir_handler)
179+
panel_.controls.addCommand(change_dir_cmd_def)
180+
151181
# Using startupCompleted does not let the UI load before we run, so it's no point in using that.
152182
# Just running sync directly. Also, the benefit is that threads are more likely to be ready
153183
# when Fusion checks for them.
@@ -166,16 +196,17 @@ def open_folder(path):
166196
subprocess.Popen(['open', '--', str(path)])
167197

168198
def sync(force=False, always_msgbox=False):
169-
global local_thread_dir_
170199
global fusion_thread_dir_
171200

172-
files = local_thread_dir_.glob('**/*.xml')
201+
local_thread_dir = get_thread_dir()
202+
files = local_thread_dir.glob('**/*.xml')
173203

174204
restore_count = 0
175205
for src_file in files:
176206
dest_file = fusion_thread_dir_ / src_file.name
177207
if force or not dest_file.exists():
178208
# shutil is extremely slow. Using shell instead.
209+
# We must flatten the directory structure.
179210
if platform_ == 'Windows':
180211
subprocess.check_call(f'copy "{src_file}" "{dest_file}"', shell=True)
181212
else:
@@ -192,4 +223,70 @@ def force_sync_handler(args: adsk.core.CommandCreatedEventArgs):
192223
answer = ui_.messageBox('Any old thread files in the Fusion 360™ directory will be overwritten. Continue?',
193224
f'{NAME} Sync Confirmation', adsk.core.MessageBoxButtonTypes.YesNoButtonType)
194225
if answer == adsk.core.DialogResults.DialogYes:
195-
sync(force=True, always_msgbox=True)
226+
sync(force=True, always_msgbox=True)
227+
228+
def change_dir_handler(args: adsk.core.CommandCreatedEventArgs):
229+
dialog = ui_.createFolderDialog()
230+
old_dir = get_thread_dir()
231+
dialog.initialDirectory = str(old_dir)
232+
dialog.title = f'Choose {NAME} directory'
233+
folder_answer = dialog.showDialog()
234+
if folder_answer == adsk.core.DialogResults.DialogOK:
235+
new_dir = pathlib.Path(dialog.folder)
236+
237+
if new_dir == old_dir:
238+
# TODO: Prompt user to select again?
239+
return
240+
241+
# Gave up on moving the files as we needed to call a separate remove command on windows
242+
# and we don't want to accidentally remove the wrong files. Maybe use Python pathlib/os
243+
# functions in the future...
244+
if len(os.listdir(old_dir)) != 0:
245+
move_answer = ui_.messageBox("Do you want to copy your existing thread definitions to the new directory?\n\n"
246+
"Existing files in the new directory will be overwritten.", f"{NAME}",
247+
adsk.core.MessageBoxButtonTypes.YesNoCancelButtonType)
248+
else:
249+
move_answer = adsk.core.DialogResults.DialogNo
250+
251+
if move_answer == adsk.core.DialogResults.DialogCancel:
252+
return
253+
254+
if not os.path.exists(new_dir):
255+
os.mkdir(new_dir)
256+
257+
set_thread_dir(new_dir)
258+
259+
if move_answer == adsk.core.DialogResults.DialogYes:
260+
# In case of move: new_dir might already have existed, so we can't just move
261+
# the old_dir and rename it
262+
263+
if old_dir in new_dir.parents or new_dir in old_dir.parents:
264+
ui_.messageBox('The new directory is a child or parent of the old directory. No copy will be done. '
265+
'You have to copy the files manually.', NAME)
266+
return
267+
268+
### TODO: Progress dialog. Looks like we must return to the main loop for it to
269+
# interact correctly with MessageBox: Completion message shows before progress.hide(),
270+
# at least on Mac.
271+
#progress = ui_.createProgressDialog()
272+
#progress.show(NAME, "Copying thread definitions...", 0, 100, 1)
273+
274+
if platform_ == 'Windows':
275+
# "move" does not handle "move a\* b" and it also scream when moving directories
276+
# on top of each other.
277+
# "robocopy" copies files and then deletes the source, but we could live with that
278+
# as Python file operations are so slow. It seems to be a tool similar to rsync.
279+
# "robocopy" removes the source directory when using /MOVE and in case something is
280+
# using it, we get a ghost folder that reports Access Denied, that we cannot
281+
# replace...
282+
# f'robocopy "{old_dir}" "{new_dir}" /MOVE /E /IS /IT /IM'
283+
# So going for old trusted xcopy instead.
284+
subprocess.check_call(f'xcopy /e /i /y "{old_dir}" "{new_dir}\"', shell=True, timeout=30)
285+
else:
286+
subprocess.check_call(f'cp -r -- "{old_dir}/"* "{new_dir}/"', shell=True, timeout=30)
287+
288+
#progress.progressValue = 100
289+
#progress.hide()
290+
291+
ui_.messageBox(f'Thread directory changed!\n\n'
292+
f'Old: {old_dir}\nNew: {new_dir}', f'{NAME}')

0 commit comments

Comments
 (0)