# THIS FILE IS A PART OF VCStudio # PYTHON 3 import os import cairo #UI modules from UI import UI_elements from UI import UI_color #settings from settings import talk #studio from studio import analytics from studio import history def get_list(filepath): # This fucntion converts text documents. (.progress) into a more machine # friendly recursive dict. # In the original organizer evaluation of the checklist was done separatelly # in a different function. Which made it very messy for recursive stuff. # I will attemp to combine it. And make it more clean as a result. checklist = { "fraction":0.0, # The percentage of a checklist. From 0.0 to 1.0 "string":filepath, "editing":False,# Whether the string is during editing. UI. "selected":False,# Whether the current task is selected. "open":True, # Whether to draw the suptasks. UI. "subtasks":[] # List of subtastks. (In the same format as the checklist) } data = open(filepath) data = data.read() data = data.split("\n") # Let's filter out all the comments. Lines starting with #. For some reason # in the old organizer. I just thought it wasn't important. LOL. And just # started reading from the 10th line. # Here is an example of the first 9 lines. # 1 #### Blender orgainizer checklist format # 2 #### INDINTATION (4 SPACES LONG) # 3 #### STR means Start date of the ASSET # 4 #### FIN means Finish deadline of the asset # 5 #### [ ] means that task is on list # 6 #### [V] means that tast is finished # 7 #### DO NOT USE EMPTY LINES # 8 STR 24/02/2020 # 9 FIN 01/05/2021 # You can see a trace from a very long time ago. From the first versions # of the blender organizer. The STR and FIN values. Which are start and # deadline of the project. # I guess we need to filter it out a bit differently. Checking whether a # given line starts with a [ or with a number of spaces and [. This will # make the file a little more open to editing by hand. Without too much # worrying that something will break. cleandata = [] for line in data: # So not to mangle the line. tmp = line while tmp.startswith(" "): tmp = tmp[1:] # Checking if tmp.startswith("[ ]") or tmp.startswith("[V]"): cleandata.append(line) # Now since we have the cleandata. We can try to parse it somehow into a # checklist thing. For this we need a reqursion. I gonna use a method from # the blender-organizer. By running the function with in itself. It's not # very wise. I know. In python3 it will give you no more then 996 recursions # untill it will declare an error. BUT. It's 996 layers of a subtaks. # For now I don't see a need in so many subtasks. Let's think of layers of # subtasks as of memory. This laptop has only 8 GB of RAM. I can't put more # data into it even if I wanted. def convert(part, indent=0): # If a thing have subtasks it should not even be a checkbox. So trying # To evaluate it's fraction by whether it's a [ ] or [V] shoun't be # done. subtask = [] for num, line in enumerate(part): # Let's get the NEXT line. I'm not kidding. We gonna work with the # next line to see whether to count this lines [V] if line[indent:].startswith("["): thisline = { "fraction":0.0, "string":line[line.find("]")+2:], "editing":False, "selected":False, "open":False, "subtasks":[] } try: nextline = part[num+1] except: nextline = "" if not line[line.find("]")+1] == ".": thisline["open"] = True if nextline.find("[")-1 <= indent: if line[indent:].startswith("[V]"): thisline["fraction"] = 1.0 else: subpart = [] subdent = indent for n, l in enumerate(part[num+1:]): if n == 0: subdent = l.find("[") if l.find("[")-1 <= indent: break else: subpart.append(l) #print(subpart) thisline["subtasks"] = convert(subpart, subdent) fracs = [] for task in thisline["subtasks"]: if not task["string"].startswith("#"): fracs.append(task["fraction"]) try: thisline["fraction"] = sum(fracs) / len(fracs) except: thisline["fraction"] = 0.0 # Sometime it was showing 99% when infect it's 100% if thisline["fraction"] == 0.9999: thisline["fraction"] = 1.0 subtask.append(thisline) return subtask checklist["subtasks"] = convert(cleandata) fracs = [] for task in checklist["subtasks"]: if not task["string"].startswith("#"): fracs.append(task["fraction"]) try: checklist["fraction"] = sum(fracs) / len(fracs) except: checklist["fraction"] = 0.0 # Sometime it was showing 99% when infect it's 100% if checklist["fraction"] == 0.9999: checklist["fraction"] = 1.0 return checklist def get_fraction(win, path): ############################################################################ # This function will return a fraction of a given checklist. It will ignore # complitelly what is the asset / shot / project the checklist is from. # For sake of making this function actually somewhat useful, aka not bloated # I will use it to cashe the whole checklist data objects. So they could be # easily accesable later on. # This is usefull so I would not need to create 2 cash data structures. One # for fractions and for the checklists them selves. ############################################################################ if path not in win.checklists: # Let's check if path exists in the project first. if os.path.exists(win.project+"/"+path): win.checklists[path] = get_list(win.project+"/"+path) else: win.checklists[path] = get_list(path) # Let's now return back the fraction return win.checklists[path]["fraction"] def get_task_by_path(tasks, path, p=[]): ############################################################################ # This function will give the information about a given task from a non # recursive URL such as ["Task", "Sub-task", "Sub-task2"]. This will look # the recursive list and output the given task. In this case "Sub-task2" # with all the information inside it. ############################################################################ for task in tasks: pa = p.copy() pa.append(" "+task["string"]) if pa == path: return task if task["subtasks"]: t = get_task_by_path(task["subtasks"], path, pa) if t: return t return False def filter_tasks(data): ############################################################################ # This function going to remove all selections and all edits from a given # checklist. This is nessesary for being able to select one task without # manually deselcting the other. And since the checklist is recursive it's # not a trivial task. ############################################################################ data["selected"] = False data["editing"] = False if data["subtasks"]: for i in data["subtasks"]: filter_tasks(i) def save(path, data): ############################################################################ # This funtion will save the checklist into a .progress file. Formatted # similarly as in the old Blender-Organizer. But as easy to understand. # # NOTE: I will remove some stuff from the file. Mainly the comments in the # beginning and the STR and END data. Which are no longer needed in a # VCStudio project. # # But since some part of Legacy Organizer relies on those, it's not adviced # to open the projects that you are planning to work on with Blender-Organizer # in VCStudio. ############################################################################ indent = [0] lines = [] def writetasks(tasks): for task in tasks: if task["fraction"] and not task["subtasks"]: v = "[V]" else: v = "[ ]" if task["open"]: o = " " else: o = "." lines.append(" "*indent[0]+v+o+task["string"]) if task["subtasks"]: indent[0] = indent[0] + 4 writetasks(task["subtasks"]) indent[0] = indent[0] - 4 writetasks(data["subtasks"]) # Writting to file w = open(path, "w") for i in lines: w.write(i+"\n") w.close() def draw(outlayer, win, path, back="story_editor"): ############################################################################ # This function will draw the checklists to the screen. # You probably noticed that there is no x, y, width or height values that # you can input. This due to one idea that I have in mind. # I'm planning to make scheduling a kind of interactive process rather then # a boring date selection. Something that's going to be somewhat fun to do. # The idea is to grab the task and to drug it outside the checklist. Which # will call for an analytics window to show up, where you can put the # schedules into a given date. # For this reason the checklist should always be in the same spot on the # screen. Which is a (window width) / 4 at the right side. ############################################################################ if path not in win.checklists: # Let's check if path exists in the project first. if os.path.exists(win.project+"/"+path): win.checklists[path] = get_list(win.project+"/"+path) elif os.path.exists(win.project+"/rnd"+path): win.checklists[path] = get_list(win.project+"/rnd"+path) else: win.checklists[path] = get_list(path) x = win.current["w"] / 4 * 3 + 10 y = 10 width = win.current["w"] / 4 - 20 height = win.current["h"] - 20 # Now let's make a layer. # Making the layer surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(width), int(height)) layer = cairo.Context(surface) layer.select_font_face("Monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) # Clip UI_elements.roundrect(layer, win, 0, 0, width, height, 10, fill=False) layer.clip() # Checklist icon. UI_color.set(layer, win, "node_background") UI_elements.roundrect(layer, win, 0, 0, width, 50, 10) UI_elements.image(layer, win, "settings/themes/"+win.settings["Theme"]+"/icons/checklist.png", 5, 5, 40, 40) # Fraction fraction = win.checklists[path]["fraction"] UI_color.set(layer, win, "progress_background") UI_elements.roundrect(layer, win, 50, 17, width - 60, 0, 7) UI_color.set(layer, win, "progress_active") UI_elements.roundrect(layer, win, 50, 17, (width - 60 )*fraction, 0, 7) # Clip UI_elements.roundrect(layer, win, 0, 60, width, height, 10, fill=False) layer.clip() ########################################################################### # NOW THIS IS THE HARD RECURSION PART! THERE IS GOING TO BE SOME REDIC! ########################################################################### tileX = 0 current_Y = 70 # There is some bullshit regarding the Global variables. I gonna do a bit # trickier. Since lists are only links to lists. Let's do this. cXY = [tileX, current_Y] if "checklist" not in win.scroll: win.scroll["checklist"] = 0 if "moving_task_now" not in win.current: win.current["moving_task_now"] = False def draw_tasks(tasks, cXY, schedulep): ######################################### # # # THIS IS THE RECURSIVE FUNCTION # # # ######################################### # I will try to explain what is going on here. But it will require me # understanding the code myself. And it's a bit redic. There was a hell # of a lot of copy-paste, edit value, continue. for num , task in enumerate(tasks): # This is the code that will be done for each task in the list. Or # subtask if a task have them. # Let's get a schedule path. A folder like structure. # Exacmple: # Instead of: # Task # Sub-Task # Sub-Task 2 # We get: # [" Task", " Sub-Task", " Sub-Task 2"] schedulepath = schedulep.copy() schedulepath.append(" "+task["string"]) ###### SCHEDULING STUFF BACKWARD ###### # This is a set of stuff to make schedules work with checklists. if "schedule_task_selected" not in win.current: win.current["schedule_task_selected"] = False if win.current["schedule_task_selected"] and win.cur == win.current["schedule_task_selected"][-1]: csl = win.current["schedule_task_selected"][0][0][4] if " "+task["string"] in csl and not task["open"]: task["open"] = True if schedulepath == csl and not task["selected"]: filter_tasks(win.checklists[path]) task["selected"] = True win.scroll["checklist"] = 0 - cXY[1] + height/2 #### DELETE SHORT KEY #### # There is probably a reason to put it here. Probably something # breaks if you put it anywhere else. IDK actually. Test it. I # don't remember if 65535 in win.current["keys"] and task["selected"] and not win.current["schedule_task_selected"]: del tasks[num] win.current["keys"] = [] # Saving save(path, win.checklists[path]) win.checklists = {} win.assets = {} win.analytics = analytics.load(win.project) win.multiuser["last_request"] = "" #### THE GRABBING FUNCTION #### # This is the activation of the grab tool. It uses the same # win.current["tool"] = "grab" as the story editor. Because it makes # sense to reuse it if it already exists. For sake of conviniense. # It doesn't mean that it uses the same code. It's a bit different. # due to the nature of lists vs free positioning items. # Now let's set up a few positional variables. So I could move the tasks # while moving the currently selected task. And in the same time will not # screw the data. # So I copy the X and the Y locations like this. sx = cXY[0] sy = win.scroll["checklist"] + cXY[1] grabY = 40 if task["subtasks"]: grabY = 60 # Grab if win.current["LMB"]\ and int(win.current["LMB"][0]) in range(int(x+sx), int(x+sx+width))\ and int(win.current["LMB"][1]) in range(int(y+sy), int(y+sy+grabY))\ and win.current["tool"] == "selection"\ and int(win.current["LMB"][0]) not in range(int(win.current["mx"]-2), int(win.current["mx"]+2))\ and int(win.current["LMB"][1]) not in range(int(win.current["my"]-2), int(win.current["my"]+2)): # So here we set up the grab tool and creating a little variable. # This variable (moving_task_now) will consist of 2 parts on release. # 1. The entire task ( with subtasks ) using pop. Which is not simply # a copy. But also removing the task from the checklist's list. # 2. The current frame of poping. So to offset insertion for 1 frame. # This insures that you insert the task in the correct spot. filter_tasks(win.checklists[path]) task["selected"] = True win.current["schedule_task_selected"] = False win.current["tool"] = "grab" win.current["moving_task_now"] = False # Now let's actually setup the pop. So when the mouse is now pressed, but # was on a previous framae, and our current tool is grab. if not win.current["LMB"] and win.previous["LMB"] and win.current["tool"] == "grab"\ and task["selected"]: # We remove the task from the list. And write it to the variable. With # the current frame. win.current["moving_task_now"] = [tasks.pop(num), win.current["frame"]] # Now I can touch the sx and sy without screwing up the cXY or scroll. thismoving = False # This is going to be True is this is the task that's # moving currently somemoving = False # This is going to be True if any task is moving someXY = [0,0] # And this is the mouse position translated to location # of the checklist. (x, y coordinates of the whole # widget). Or in other words position of the task. # Then comes some logic to determen those 3 values. if win.current["tool"] == "grab": # Okay so here. If grab first we assume that it's some other # task. And now currently selected one. somemoving = True someXY = [ win.current["mx"]-x, win.current["my"]-y ] # Now this is the editing of the sx and sy values. To be at # the position of the mouse. if task["selected"]: # Calculating so the mouse will end up about in the centre # of the task while the task is at motion. sx = win.current["mx"] - x - (width - cXY[0])/2 sy = win.current["my"] - y - 10 task["open"] = False thismoving = True somemoving = False # Now if the mouse is outside the frame of the checklist # it will activate the scheduling. ############################################################ # SCHEDULING PART # ############################################################ if win.current["mx"] < x: win.url = "analytics" win.current["grab_data"] = [path, back, win.cur, schedulepath, win.settings["Username"]] win.current["tool"] = "schedule" # # # # SEE studio/studio_analyticsLayer.py # for more details about scheduling # # And if back into frame it will return to normal grab mode. elif win.current["tool"] == "schedule" and win.current["mx"] > x: win.url = back win.current["tool"] = "grab" #################################################################### # SCHEDULING PART END # #################################################################### inside = False between = False if sy-10 < someXY[1] < sy+10 and somemoving and not task["selected"]: if win.current["moving_task_now"] and win.current["moving_task_now"][1] != win.current["frame"]: tasks.insert(num, win.current["moving_task_now"][0]) win.current["tool"] = "selection" win.current["LMB"] = False win.previous["LMB"] = False # Saving save(path, win.checklists[path]) win.checklists = {} win.assets = {} win.analytics = analytics.load(win.project) win.multiuser["last_request"] = "" elif win.current["LMB"]: for line in range(int(cXY[0]/20)): line = line * 20 + 10 UI_color.set(layer, win, "node_background") layer.move_to(line, win.scroll["checklist"] + cXY[1]-10) layer.line_to(line, win.scroll["checklist"] + cXY[1]+40) layer.stroke() cXY[1] = cXY[1] + 50 sx = cXY[0] sy = win.scroll["checklist"] + cXY[1] between = True somemoving = False elif sy < someXY[1] < sy + 40 and somemoving and not task["selected"]: if win.current["moving_task_now"] and win.current["moving_task_now"][1] != win.current["frame"]: task["subtasks"].append(win.current["moving_task_now"][0]) win.current["tool"] = "selection" win.current["LMB"] = False win.previous["LMB"] = False # Saving save(path, win.checklists[path]) win.checklists = {} win.assets = {} win.analytics = analytics.load(win.project) win.multiuser["last_request"] = "" elif win.current["LMB"]: inside = True # Selection if win.textactive != "editing_task" and win.current["tool"] == "selection": def do(): ed = task["selected"] and not task["editing"] filter_tasks(win.checklists[path]) win.current["schedule_task_selected"] = False task["selected"] = True if ed: task["editing"] = True UI_elements.roundrect(layer, win, sx+40, sy, width - cXY[0]-40, grabY, 10, button=do, offset=[x,y], fill=False) layer.stroke() # Background if not task["subtasks"]: UI_color.set(layer, win, "node_background") if task["string"].startswith("#"): UI_color.set(layer, win, "dark_overdrop") UI_elements.roundrect(layer, win, sx, sy, width - cXY[0], 40, 10) if inside: UI_color.set(layer, win, "progress_background") UI_elements.roundrect(layer, win, sx, sy, width - cXY[0], 40, 10, fill=False) layer.stroke() if task["selected"]: UI_color.set(layer, win, "text_normal") UI_elements.roundrect(layer, win, sx, sy, width - cXY[0], 40, 10, fill=False) layer.stroke() # Line to see how deep you are in for line in range(int(cXY[0]/20)): line = line * 20 + 10 UI_color.set(layer, win, "node_background") layer.move_to(line, win.scroll["checklist"] + cXY[1]-10) layer.line_to(line, win.scroll["checklist"] + cXY[1]+40) layer.stroke() if not task["string"].startswith("#"): if task["fraction"]: im = "checked" else: im = "unchecked" # CHECK BUTTON def do(): if task["fraction"]: task["fraction"] = 0.0 history.record(win, path, "[Un-Checked]", schedulepath) else: task["fraction"] = 1.0 history.record(win, path, "[Checked]", schedulepath) # Saving save(path, win.checklists[path]) win.checklists = {} win.assets = {} win.multiuser["last_request"] = "" #win.analytics = analytics.load(win.project) UI_elements.roundrect(layer, win, sx, sy, 40, 40, 10, button=do, icon=im, offset=[x,y]) else: UI_color.set(layer, win, "node_background") if task["string"].startswith("#"): UI_color.set(layer, win, "dark_overdrop") UI_elements.roundrect(layer, win, sx, sy, width - cXY[0], 60, 10) if inside: UI_color.set(layer, win, "progress_background") UI_elements.roundrect(layer, win, sx, sy, width - cXY[0], 60, 10, fill=False) layer.stroke() if task["selected"]: UI_color.set(layer, win, "text_normal") UI_elements.roundrect(layer, win, sx, sy, width - cXY[0], 60, 10, fill=False) layer.stroke() # Line to see how deep you are in for line in range(int(cXY[0]/20)): line = line * 20 + 10 UI_color.set(layer, win, "node_background") layer.move_to(line, win.scroll["checklist"] + cXY[1]-10) layer.line_to(line, win.scroll["checklist"] + cXY[1]+60) layer.stroke() # Fraction if not task["string"].startswith("#"): fraction = task["fraction"] UI_color.set(layer, win, "progress_background") UI_elements.roundrect(layer, win, sx+10, sy+45, width-20 - cXY[0], 0, 5) UI_color.set(layer, win, "progress_active") UI_elements.roundrect(layer, win, sx+10, sy+45, (width -20 - cXY[0])*fraction, 0, 5) if task["open"]: im = "open" else: im = "closed" def do(): task["open"] = not task["open"] win.current["schedule_task_selected"] = False # Saving save(path, win.checklists[path]) win.multiuser["last_request"] = "" UI_elements.roundrect(layer, win, sx, sy, 40, 60, 10, button=do, icon=im, offset=[x,y]) # TEXT # ECS if 65307 in win.current["keys"] and task["editing"]: # It's here because Text entry has it's own ESC win.textactive = "" task["editing"] = False del win.text["editing_task"] win.current["keys"] = [] # Saving save(path, win.checklists[path]) win.checklists = {} win.assets = {} win.analytics = analytics.load(win.project) win.multiuser["last_request"] = "" if not task["editing"]: layer.set_font_size(20) layer.move_to( sx+50, sy+25 ) if task["string"].startswith("#"): UI_color.set(layer, win, "progress_background") if not task["subtasks"]: layer.move_to( sx+10, sy+25 ) layer.show_text(task["string"][1:]) else: UI_color.set(layer, win, "text_normal") layer.show_text(task["string"]) else: UI_elements.text(layer, win, "editing_task", sx+39, sy, width - cXY[0] - 40, 40, set_text=task["string"], offset=[x,y]) win.textactive = "editing_task" # Assigning the text def do(): task["string"] = win.text["editing_task"]["text"] win.textactive = "" filter_tasks(win.checklists[path]) del win.text["editing_task"] # Saving save(path, win.checklists[path]) win.multiuser["last_request"] = "" def button(): do() win.checklists = {} win.assets = {} UI_elements.roundrect(layer, win, sx+39 + width - cXY[0] - 80, sy, 40, 40, 10, button=button, icon="ok", tip=talk.text("checked"), offset=[x,y]) # Enter if 65293 in win.current["keys"]: button() win.current["keys"] = [] # Tab if 65289 in win.current["keys"]: do() tasks.append( { "fraction":0.0, "string":"", "editing":True, "selected":True, "open":False, "subtasks":[] } ) win.current["keys"] = [] # RECURSION if not thismoving: if not task["subtasks"]: cXY[1] = cXY[1] + 50 else: cXY[1] = cXY[1] + 70 cXY[0] = cXY[0] + 20 # THERE IS YOUR RECURSION if task["open"]: draw_tasks(task["subtasks"], cXY, schedulepath) cXY[0] = cXY[0] - 20 # Adding subtasks if ((task["subtasks"] and task["open"] and task["selected"])\ or (task["selected"] and not task["subtasks"]))\ and win.textactive != "editing_task"\ and win.current["tool"] == "selection": cXY[0] = cXY[0] + 20 def do(): task["open"] = True filter_tasks(win.checklists[path]) task["subtasks"].append( { "fraction":0.0, "string":"", "editing":True, "selected":True, "open":False, "subtasks":[] } ) UI_elements.roundrect(layer, win, cXY[0], win.scroll["checklist"] + cXY[1], width - cXY[0], 40, 10, button=do, icon="new", offset=[x,y]) UI_color.set(layer, win, "progress_background") UI_elements.roundrect(layer, win, cXY[0], win.scroll["checklist"] + cXY[1], width - cXY[0], 40, 10, fill=False) layer.stroke() layer.set_font_size(20) layer.move_to( cXY[0]+50, win.scroll["checklist"] + cXY[1]+25 ) layer.show_text(talk.text("add_new_subtask")) for line in range(int(cXY[0]/20)): line = line * 20 + 10 UI_color.set(layer, win, "node_background") layer.move_to(line, win.scroll["checklist"] + cXY[1]-10) layer.line_to(line, win.scroll["checklist"] + cXY[1]+40) layer.stroke() cXY[0] = cXY[0] - 20 cXY[1] = cXY[1] + 50 schedulepath = [] draw_tasks(win.checklists[path]["subtasks"], cXY, schedulepath) # Go to the bottom. if win.current["tool"] == "grab"\ and win.current["my"] - y > win.scroll["checklist"] + cXY[1]: if win.current["moving_task_now"] and win.current["moving_task_now"][1] != win.current["frame"]: win.checklists[path]["subtasks"].append(win.current["moving_task_now"][0]) win.current["tool"] = "selection" win.current["LMB"] = False win.previous["LMB"] = False # Saving save(path, win.checklists[path]) win.checklists = {} win.assets = {} win.analytics = analytics.load(win.project) win.multiuser["last_request"] = "" # Adding a task. if win.textactive != "editing_task"\ and win.current["tool"] == "selection": def do(): filter_tasks(win.checklists[path]) win.checklists[path]["subtasks"].append( { "fraction":0.0, "string":"", "editing":True, "selected":True, "open":False, "subtasks":[] } ) UI_elements.roundrect(layer, win, cXY[0], win.scroll["checklist"] + cXY[1], width - cXY[0], 40, 10, button=do, icon="new", offset=[x,y]) UI_color.set(layer, win, "progress_background") UI_elements.roundrect(layer, win, cXY[0], win.scroll["checklist"] + cXY[1], width - cXY[0], 40, 10, fill=False) layer.stroke() layer.set_font_size(20) layer.move_to( cXY[0]+50, win.scroll["checklist"] + cXY[1]+25 ) layer.show_text(talk.text("add_new_task")) tileX, current_Y = cXY # So there would not be jumps of stuff. Let's add heigh to the scroll while # in grab. if win.current["tool"] == "grab": current_Y = current_Y + height # Outputting the layer outlayer.set_source_surface(surface, x, y) outlayer.paint() # Scroll UI_elements.scroll_area(outlayer, win, "checklist", x+0, y+50, width, height-50, current_Y, bar=True, mmb=True)