38
38
# loaded by other add-ins
39
39
from .thomasa88lib import utils
40
40
from .thomasa88lib import events
41
- #from .thomasa88lib import timeline
42
41
from .thomasa88lib import manifest
43
42
from .thomasa88lib import error
43
+ from .thomasa88lib import settings
44
44
45
45
# Force modules to be fresh during development
46
46
import importlib
47
47
importlib .reload (thomasa88lib .utils )
48
48
importlib .reload (thomasa88lib .events )
49
- #importlib.reload(thomasa88lib.timeline)
50
49
importlib .reload (thomasa88lib .manifest )
51
50
importlib .reload (thomasa88lib .error )
51
+ importlib .reload (thomasa88lib .settings )
52
52
53
53
PANEL_ID = 'thomasa88_ThreadKeeperPanel'
54
54
DIRECTORY_CMD_DEF_ID = 'thomasa88_ThreadKeeperDirectory'
55
55
FUSION_DIRECTORY_CMD_DEF_ID = 'thomasa88_ThreadKeeperFusionDirectory'
56
56
FORCE_SYNC_CMD_DEF_ID = 'thomasa88_ThreadKeeperForceSync'
57
+ CHANGE_DIR_CMD_DEF_ID = 'thomasa88_ThreadKeeperChangeDirectory'
57
58
58
59
app_ = None
59
60
ui_ = None
60
61
panel_ = None
61
- local_thread_dir_ = None
62
62
fusion_thread_dir_ = None
63
63
platform_ = platform .system ()
64
+ default_thread_dir_ = str (pathlib .Path (thomasa88lib .utils .get_file_dir ()) / 'Threads' )
64
65
65
66
error_catcher_ = thomasa88lib .error .ErrorCatcher (msgbox_in_debug = False )
66
67
events_manager_ = thomasa88lib .events .EventsManager (error_catcher_ )
67
68
manifest_ = thomasa88lib .manifest .read ()
68
69
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 )
69
84
70
85
def run (context ):
71
86
global app_
72
87
global ui_
73
88
global panel_
74
- global local_thread_dir_
75
89
global fusion_thread_dir_
76
90
with error_catcher_ :
77
91
app_ = adsk .core .Application .get ()
78
92
ui_ = app_ .userInterface
79
93
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 )
83
97
84
98
# https://knowledge.autodesk.com/support/fusion-360/learn-explore/caas/sfdcarticles/sfdcarticles/Custom-Threads-in-Fusion-360.html
85
99
# Windows location:
@@ -111,7 +125,7 @@ def run(context):
111
125
'Put threads that you want to keep into this directory.' ,
112
126
'./resources/thread_folder' )
113
127
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 () ))
115
129
directory_control = panel_ .controls .addCommand (directory_cmd_def )
116
130
directory_control .isPromoted = True
117
131
directory_control .isPromotedByDefault = True
@@ -148,6 +162,22 @@ def run(context):
148
162
callback = force_sync_handler )
149
163
panel_ .controls .addCommand (force_sync_cmd_def )
150
164
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
+
151
181
# Using startupCompleted does not let the UI load before we run, so it's no point in using that.
152
182
# Just running sync directly. Also, the benefit is that threads are more likely to be ready
153
183
# when Fusion checks for them.
@@ -166,16 +196,17 @@ def open_folder(path):
166
196
subprocess .Popen (['open' , '--' , str (path )])
167
197
168
198
def sync (force = False , always_msgbox = False ):
169
- global local_thread_dir_
170
199
global fusion_thread_dir_
171
200
172
- files = local_thread_dir_ .glob ('**/*.xml' )
201
+ local_thread_dir = get_thread_dir ()
202
+ files = local_thread_dir .glob ('**/*.xml' )
173
203
174
204
restore_count = 0
175
205
for src_file in files :
176
206
dest_file = fusion_thread_dir_ / src_file .name
177
207
if force or not dest_file .exists ():
178
208
# shutil is extremely slow. Using shell instead.
209
+ # We must flatten the directory structure.
179
210
if platform_ == 'Windows' :
180
211
subprocess .check_call (f'copy "{ src_file } " "{ dest_file } "' , shell = True )
181
212
else :
@@ -192,4 +223,70 @@ def force_sync_handler(args: adsk.core.CommandCreatedEventArgs):
192
223
answer = ui_ .messageBox ('Any old thread files in the Fusion 360™ directory will be overwritten. Continue?' ,
193
224
f'{ NAME } Sync Confirmation' , adsk .core .MessageBoxButtonTypes .YesNoButtonType )
194
225
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 } \n New: { new_dir } ' , f'{ NAME } ' )
0 commit comments