1 Star 0 Fork 0

sephirothalucard/myblendercontrib

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
system_time_tracker.py 16.20 KB
一键复制 编辑 原始数据 按行查看 历史
sambler 提交于 2019-07-31 11:20 . updates from external repos
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
bl_info = {"name": "Time Tracker",
"description": "Track time spent in blender. Writes data to .csv and provides summary sorted by project (directory name).",
"author": "Jakub Uhlik",
"version": (0, 2, 0),
"blender": (2, 80, 0),
"location": "main: Preferences > Addons > System > Time Tracker, quick enable/disable: View3d > Properties > Time Tracker",
"warning": "Beta",
"wiki_url": "",
"tracker_url": "",
"category": "System", }
import os
import datetime
import csv
import platform
import subprocess
import time
import shutil
import bpy
from bpy.types import Panel, Operator, AddonPreferences
from bpy.props import BoolProperty, StringProperty, IntProperty
from bpy.app.handlers import persistent
# TODO try to use timers for something? https://docs.blender.org/api/blender2.8/bpy.app.timers.html
DEBUG = False
def log(msg, indent=0, ):
m = "{0}> {1}".format(" " * indent, msg)
if(DEBUG):
print(m)
class Runtime():
start = datetime.datetime.now()
path_message = ""
summary_message = ""
modified = -1
summary = None
level = -1
update_last = -1
update_step = 60
class Utils():
@staticmethod
def format_stamp(t):
return t.strftime('%Y.%m.%d-%H.%M.%S')
@staticmethod
def format_time(d):
return '{:02}:{:02}:{:02}'.format(d // 3600, d % 3600 // 60, d % 60)
@staticmethod
def format_time_summary(d):
return '{:02}h {:02}m'.format(d // 3600, d % 3600 // 60)
@staticmethod
def format_time_summary_seconds(d):
return '{:02}h {:02}m {:02}s'.format(d // 3600, d % 3600 // 60, d % 60)
@staticmethod
def get_default_csv_path():
return os.path.join(os.path.dirname(os.path.realpath(__file__)), "{0}.csv".format(os.path.splitext(os.path.split(os.path.realpath(__file__))[1])[0]))
@staticmethod
def get_preferences():
a = os.path.splitext(os.path.split(os.path.realpath(__file__))[1])[0]
p = bpy.context.preferences.addons[a].preferences
return p
@staticmethod
def find_handlers():
l = -1
s = -1
u = -1
h = bpy.app.handlers
for i in range(len(h.load_post)):
if(h.load_post[i].__name__ == "TIME_TRACKER_load_handler"):
l = i
for i in range(len(h.save_post)):
if(h.save_post[i].__name__ == "TIME_TRACKER_save_handler"):
s = i
for i in range(len(h.depsgraph_update_post)):
if(h.depsgraph_update_post[i].__name__ == "TIME_TRACKER_update_handler"):
u = i
return l, s, u
def summary():
prefs = Utils.get_preferences()
p = prefs.csv_path
if(not os.path.exists(p)):
Runtime.summary_message = "File {} does not exist.".format(p)
return []
else:
Runtime.summary_message = ""
# get modified time of csv
tm = os.path.getmtime(p)
if(Runtime.summary is not None):
if(tm == Runtime.modified and prefs.level == Runtime.level):
# return already parsed results
return Runtime.summary
Runtime.modified = tm
Runtime.level = prefs.level
db = []
with open(p, encoding='utf-8', ) as f:
reader = csv.reader(f)
for i, r in enumerate(reader):
if(i == 0):
continue
t = int(r[2])
if(r[4] != "" and r[5] != "" and t != 0):
a = [r[0], r[1], t, r[3], r[4], r[5]]
db.append(a)
def proj(path, level, ):
# split path until level is reached
def slice_last_dir(p):
return os.path.split(p)[0]
h, t = os.path.split(path)
pp = "{0}".format(h)
for i in range(level):
pp = slice_last_dir(pp)
proj = os.path.split(pp)[1]
return proj
# split to projects
dbp = {}
for r in db:
p = proj(r[5], int(prefs.level))
try:
dbp[p].append(r)
except Exception:
dbp[p] = []
dbp[p].append(r)
# sum projects
a = []
for proj, ls in dbp.items():
s = 0
for r in ls:
s += r[2]
d = os.path.split(ls[0][5])[0]
n = len(ls)
if(proj == ''):
# substitute no directory by '/'
proj = '/'
if(prefs.debug_show_seconds_in_summary):
a.append([proj, Utils.format_time_summary_seconds(s), d])
else:
a.append([proj, Utils.format_time_summary(s), d])
# sort by project name
a.sort(key=lambda v: v[0])
# and make strings
r = []
for i, l in enumerate(a):
r.append(["project '{0}': {1}".format(l[0], l[1]), l[2]])
# store results, so it will not be calculated on each ui redraw
Runtime.summary = r
return r
def update(self, context):
prefs = Utils.get_preferences()
current = prefs.csv_path
previous = prefs.previous_csv_path
if(current == previous):
# no change
return
# remove message at this point. earlier will not be visible
Runtime.path_message = ""
if(current == ""):
# no path, use default
current = Utils.get_default_csv_path()
if(os.path.isdir(current)):
# is directory, add default file name
current = os.path.join(current, os.path.split(Utils.get_default_csv_path())[1])
# add correct extension
current = bpy.path.ensure_ext(current, ".csv", case_sensitive=True, )
# check if directory is writeable
d = os.path.split(current)[0]
if(not os.access(d, os.W_OK)):
# if not, put default path and write message
current = Utils.get_default_csv_path()
Runtime.path_message = "Location '{0}' is not writable.".format(d)
# check if path has changed, and if so, write previous .csv to it if there is none (if it exists too)
if(current != previous and not os.path.exists(current) and os.path.exists(previous)):
with open(current, mode='w', encoding='utf-8') as f:
with open(previous, encoding='utf-8') as o:
c = "".join(o.readlines())
f.write(c)
prefs.previous_csv_path = current
# update after previous path to prevent recursion
prefs.csv_path = current
def scene_update_update(self, context):
# great function name, isn't it?
prefs = Utils.get_preferences()
Runtime.update_step = prefs.update_interval
# force summary update
Runtime.summary = None
if(prefs.scene_update):
_, _, u = Utils.find_handlers()
h = bpy.app.handlers
if(u == -1):
h.depsgraph_update_post.append(TIME_TRACKER_update_handler)
else:
_, _, u = Utils.find_handlers()
h = bpy.app.handlers
if(u != -1):
del h.depsgraph_update_post[u]
class TIME_TRACKER_preferences(AddonPreferences):
bl_idname = __name__
csv_first_line: bpy.props.StringProperty(name=".csv First Line", description=".csv first line to be written, contains field names.", default="timestamp (YYYY.MM.DD-HH.MM.SS),event,time (seconds),time (formatted),file name,file path", )
previous_csv_path: bpy.props.StringProperty(name="Previous CSV Path", description="Used to detect path change and to copy old csv from on change.", default=Utils.get_default_csv_path(), maxlen=1024, subtype='FILE_PATH', )
enabled: bpy.props.BoolProperty(name="Tracking Enabled", description="When enabled, loading and saving of files is logged.", default=True, )
level: bpy.props.IntProperty(name="Project Directory Level", description="Which level is considered as project directory. 0 is current directory, 1 is directory enclosing current directory, etc.", default=0, min=0, max=100, )
csv_path: bpy.props.StringProperty(name="CSV Path", description="Location of .csv with tracking data.", default=Utils.get_default_csv_path(), update=update, maxlen=1024, subtype='FILE_PATH', )
summary: bpy.props.BoolProperty(name="Show Summary", description="When enabled, shows tracked data bellow in simple format (project name - total time spent).", default=False, )
show_advanced: bpy.props.BoolProperty(name="Show Advanced Options", description="", default=False, )
scene_update: bpy.props.BoolProperty(name="Track Scene Update", description="Track time spent on files closed without saving using depsgraph_update_post handler.", update=scene_update_update, default=True, )
update_interval: bpy.props.IntProperty(name="Update Interval In Seconds", description="Interval at which depsgraph_update_post handler is processed.", update=scene_update_update, default=60, min=1, max=60 * 10, )
debug_show_seconds_in_summary: bpy.props.BoolProperty(name="Show Seconds In Summary", description="", update=scene_update_update, default=False, )
def draw(self, context):
l = self.layout
r = l.row()
s = r.split(factor=0.5)
c = s.column()
c.prop(self, "enabled")
c = s.column()
rr = c.row()
rr.operator("time_tracker.open_csv")
rr.operator("time_tracker.clear_data", )
r = l.row()
r.prop(self, "csv_path")
if(Runtime.path_message is not ""):
r = l.row()
r.label(text=Runtime.path_message, icon='ERROR', )
r = l.row()
s = r.split(factor=0.75)
c = s.column()
c.prop(self, "summary", )
if(self.summary is True):
c = s.column()
c.prop(self, "level")
if(Runtime.summary_message is not ""):
r = l.row()
r.label(text=Runtime.summary_message, icon='ERROR', )
a = summary()
if(len(a) == 0):
r = l.row()
r.label(text="No data tracked yet.", icon='ERROR', )
else:
r = l.row()
b = r.box()
for i in a:
r = b.row()
s = r.split(factor=0.75)
c = s.column()
c.label(text=i[0], icon='TIME', )
c = s.column()
c.operator("time_tracker.show_project_directory", ).directory = i[1]
r = l.row()
r.prop(self, "show_advanced")
if(self.show_advanced):
r = l.row()
r.prop(self, "debug_show_seconds_in_summary")
r.prop(self, "scene_update")
r.prop(self, "update_interval")
class TIME_TRACKER_OT_show_project_directory(Operator):
bl_idname = "time_tracker.show_project_directory"
bl_label = "Show Project Directory"
bl_description = "Show project directory."
directory: StringProperty()
def execute(self, context):
d = self.directory
if(not os.path.exists(d)):
self.report({'ERROR'}, "The directory {0} does not exist.".format(d))
return {'FINISHED'}
p = platform.system()
if(p == 'Windows'):
os.startfile(os.path.normpath(d))
elif(p == 'Darwin'):
subprocess.Popen(['open', d], )
elif(p == 'Linux'):
subprocess.Popen(['xdg-open', d], )
else:
raise OSError("Unknown platform: {}.".format(p))
return {'FINISHED'}
class TIME_TRACKER_OT_clear_data(Operator):
bl_idname = "time_tracker.clear_data"
bl_label = "Clear Data"
bl_description = "Removes all tracked data."
def execute(self, context):
prefs = Utils.get_preferences()
p = prefs.csv_path
with open(p, mode='w', encoding='utf-8') as f:
f.write("{0}\n".format(prefs.csv_first_line))
return {'FINISHED'}
class TIME_TRACKER_OT_open_csv(Operator):
bl_idname = "time_tracker.open_csv"
bl_label = "Open CSV"
bl_description = "Open CSV with tracking data."
def execute(self, context):
prefs = Utils.get_preferences()
csv = prefs.csv_path
if(not os.path.exists(csv)):
self.report({'ERROR'}, "No such file: {}".format(csv))
return {'FINISHED'}
p = platform.system()
if(p == 'Windows'):
os.startfile(os.path.normpath(csv))
elif(p == 'Darwin'):
subprocess.Popen(['open', csv], )
elif(p == 'Linux'):
subprocess.Popen(['xdg-open', csv], )
else:
raise OSError("Unknown platform: {}.".format(csv))
return {'FINISHED'}
@persistent
def TIME_TRACKER_load_handler(null):
track("load")
@persistent
def TIME_TRACKER_save_handler(null):
track("save")
@persistent
def TIME_TRACKER_update_handler(null):
t = time.time()
if(t < Runtime.update_last + Runtime.update_step):
return
Runtime.update_last = t
track("update")
def start():
log("start")
prefs = Utils.get_preferences()
p = prefs.csv_path
# write starting csv if there is none
if(not os.path.exists(p)):
with open(p, mode='w', encoding='utf-8') as f:
f.write("{0}\n".format(prefs.csv_first_line))
# set handlers
l, s, u = Utils.find_handlers()
h = bpy.app.handlers
if(l == -1):
h.load_post.append(TIME_TRACKER_load_handler)
if(s == -1):
h.save_post.append(TIME_TRACKER_save_handler)
if(prefs.scene_update):
if(u == -1):
h.depsgraph_update_post.append(TIME_TRACKER_update_handler)
def track(e):
log(e)
prefs = Utils.get_preferences()
if(not prefs.enabled):
return
p = bpy.data.filepath
n = datetime.datetime.now()
d = n - Runtime.start
h, t = os.path.split(p)
l = "{0},{1},{2},{3},{4},{5}\n".format(Utils.format_stamp(n), e, d.seconds, Utils.format_time(d.seconds), t, p, )
with open(prefs.csv_path, mode='a', encoding='utf-8') as f:
f.write(l)
Runtime.start = n
def stop():
log("stop")
l, s, u = Utils.find_handlers()
h = bpy.app.handlers
if(l != -1):
del h.load_post[l]
if(s != -1):
del h.save_post[s]
if(u != -1):
del h.depsgraph_update_post[u]
class TIME_TRACKER_PT_panel(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "View"
bl_label = "Time Tracker"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
prefs = Utils.get_preferences()
l = self.layout
l.prop(prefs, 'enabled', toggle=True, text='Enabled', )
if(DEBUG):
l.separator()
c = l.column()
c.label(text="start: {}".format(Runtime.start))
c.label(text="path_message: {}".format(Runtime.path_message))
c.label(text="summary_message: {}".format(Runtime.summary_message))
c.label(text="modified: {}".format(Runtime.modified))
c.label(text="summary: {}".format(Runtime.summary))
c.label(text="level: {}".format(Runtime.level))
c.label(text="update_last: {}".format(Runtime.update_last))
c.label(text="update_step: {}".format(Runtime.update_step))
c.scale_y = 0.5
classes = (
TIME_TRACKER_preferences,
TIME_TRACKER_OT_show_project_directory,
TIME_TRACKER_OT_clear_data,
TIME_TRACKER_OT_open_csv,
TIME_TRACKER_PT_panel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
Runtime.start = datetime.datetime.now()
start()
def unregister():
stop()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == '__main__':
register()
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/sephirothalucard/myblendercontrib.git
git@gitee.com:sephirothalucard/myblendercontrib.git
sephirothalucard
myblendercontrib
myblendercontrib
master

搜索帮助