extensions_framework: prefer user config and scripts dirs, if set, to save addon...
[blender-staging.git] / release / scripts / modules / extensions_framework / util.py
1 # -*- coding: utf8 -*-
2 #
3 # ***** BEGIN GPL LICENSE BLOCK *****
4 #
5 # --------------------------------------------------------------------------
6 # Blender 2.5 Extensions Framework
7 # --------------------------------------------------------------------------
8 #
9 # Authors:
10 # Doug Hammond
11 #
12 # This program is free software; you can redistribute it and/or
13 # modify it under the terms of the GNU General Public License
14 # as published by the Free Software Foundation; either version 2
15 # of the License, or (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software Foundation,
24 # Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
25 #
26 # ***** END GPL LICENCE BLOCK *****
27 #
28 import configparser
29 import datetime
30 import os
31 import tempfile
32 import threading
33
34 import bpy
35
36 """List of possibly appropriate paths to load/save addon config from/to"""
37 config_paths = []
38 if bpy.utils.user_resource('CONFIG') != "": config_paths.append(bpy.utils.user_resource('CONFIG'))
39 if bpy.utils.user_resource('SCRIPTS') != "": config_paths.append(bpy.utils.user_resource('SCRIPTS'))
40 for pth in bpy.utils.script_paths():
41         if pth != "": config_paths.append(pth)
42
43 """This path is set at the start of export, so that calls to
44 path_relative_to_export() can make all exported paths relative to
45 this one.
46 """
47 export_path = '';
48
49 def path_relative_to_export(p):
50         """Return a path that is relative to the export path"""
51         global export_path
52         p = filesystem_path(p)
53         try:
54                 relp = os.path.relpath(p, os.path.dirname(export_path))
55         except ValueError: # path on different drive on windows
56                 relp = p
57         
58         return relp.replace('\\', '/')
59
60 def filesystem_path(p):
61         """Resolve a relative Blender path to a real filesystem path"""
62         if p.startswith('//'):
63                 pout = bpy.path.abspath(p)
64         else:
65                 pout = os.path.realpath(p)
66         
67         return pout.replace('\\', '/')
68
69 # TODO: - somehow specify TYPES to get/set from config
70
71 def find_config_value(module, section, key, default):
72         """Attempt to find the configuration value specified by string key
73         in the specified section of module's configuration file. If it is
74         not found, return default.
75         
76         """
77         global config_paths
78         fc = []
79         for p in config_paths:
80                 if os.path.exists(p) and os.path.isdir(p) and os.access(p, os.W_OK):
81                         fc.append( '/'.join([p, '%s.cfg' % module]))
82         
83         if len(fc) < 1:
84                 print('Cannot find %s config file path' % module)
85                 return default
86                 
87         cp = configparser.SafeConfigParser()
88         
89         cfg_files = cp.read(fc)
90         if len(cfg_files) > 0:
91                 try:
92                         val = cp.get(section, key)
93                         if val == 'true':
94                                 return True
95                         elif val == 'false':
96                                 return False
97                         else:
98                                 return val
99                 except:
100                         return default
101         else:
102                 return default
103
104 def write_config_value(module, section, key, value):
105         """Attempt to write the configuration value specified by string key
106         in the specified section of module's configuration file.
107         
108         """
109         global config_paths
110         fc = []
111         for p in config_paths:
112                 if os.path.exists(p) and os.path.isdir(p) and os.access(p, os.W_OK):
113                         fc.append( '/'.join([p, '%s.cfg' % module]))
114         
115         if len(fc) < 1:
116                 raise Exception('Cannot find a writable path to store %s config file' %
117                         module)
118                 
119         cp = configparser.SafeConfigParser()
120         
121         cfg_files = cp.read(fc)
122         
123         if not cp.has_section(section):
124                 cp.add_section(section)
125                 
126         if value == True:
127                 cp.set(section, key, 'true')
128         elif value == False:
129                 cp.set(section, key, 'false')
130         else:
131                 cp.set(section, key, value)
132         
133         if len(cfg_files) < 1:
134                 cfg_files = fc
135         
136         fh=open(cfg_files[0],'w')
137         cp.write(fh)
138         fh.close()
139         
140         return True
141
142 def scene_filename():
143         """Construct a safe scene filename, using 'untitled' instead of ''"""
144         filename = os.path.splitext(os.path.basename(bpy.data.filepath))[0]
145         if filename == '':
146                 filename = 'untitled'
147         return bpy.path.clean_name(filename)
148
149 def temp_directory():
150         """Return the system temp directory"""
151         return tempfile.gettempdir()
152
153 def temp_file(ext='tmp'):
154         """Get a temporary filename with the given extension. This function
155         will actually attempt to create the file."""
156         tf, fn = tempfile.mkstemp(suffix='.%s'%ext)
157         os.close(tf)
158         return fn
159
160 class TimerThread(threading.Thread):
161         """Periodically call self.kick(). The period of time in seconds
162         between calling is given by self.KICK_PERIOD, and the first call
163         may be delayed by setting self.STARTUP_DELAY, also in seconds.
164         self.kick() will continue to be called at regular intervals until
165         self.stop() is called. Since this is a thread, calling self.join()
166         may be wise after calling self.stop() if self.kick() is performing
167         a task necessary for the continuation of the program.
168         The object that creates this TimerThread may pass into it data
169         needed during self.kick() as a dict LocalStorage in __init__().
170         
171         """
172         STARTUP_DELAY = 0
173         KICK_PERIOD = 8
174         
175         active = True
176         timer = None
177         
178         LocalStorage = None
179         
180         def __init__(self, LocalStorage=dict()):
181                 threading.Thread.__init__(self)
182                 self.LocalStorage = LocalStorage
183         
184         def set_kick_period(self, period):
185                 """Adjust the KICK_PERIOD between __init__() and start()"""
186                 self.KICK_PERIOD = period + self.STARTUP_DELAY
187         
188         def stop(self):
189                 """Stop this timer. This method does not join()"""
190                 self.active = False
191                 if self.timer is not None:
192                         self.timer.cancel()
193                         
194         def run(self):
195                 """Timed Thread loop"""
196                 while self.active:
197                         self.timer = threading.Timer(self.KICK_PERIOD, self.kick_caller)
198                         self.timer.start()
199                         if self.timer.isAlive(): self.timer.join()
200         
201         def kick_caller(self):
202                 """Intermediary between the kick-wait-loop and kick to allow
203                 adjustment of the first KICK_PERIOD by STARTUP_DELAY
204                 
205                 """
206                 if self.STARTUP_DELAY > 0:
207                         self.KICK_PERIOD -= self.STARTUP_DELAY
208                         self.STARTUP_DELAY = 0
209                 
210                 self.kick()
211         
212         def kick(self):
213                 """Sub-classes do their work here"""
214                 pass
215
216 def format_elapsed_time(t):
217         """Format a duration in seconds as an HH:MM:SS format time"""
218         
219         td = datetime.timedelta(seconds=t)
220         min = td.days*1440  + td.seconds/60.0
221         hrs = td.days*24        + td.seconds/3600.0
222         
223         return '%i:%02i:%02i' % (hrs, min%60, td.seconds%60)