studio_gtk.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. # THIS FILE IS A PART OF VCStudio
  2. # PYTHON 3
  3. import os
  4. import datetime
  5. # GTK module ( Graphical interface
  6. import gi
  7. gi.require_version('Gtk', '3.0')
  8. from gi.repository import Gtk
  9. from gi.repository import Gdk
  10. import cairo
  11. import datetime
  12. import threading
  13. # Own modules
  14. from settings import settings
  15. from settings import talk
  16. from project_manager import pm_project
  17. # Studio
  18. from studio import analytics
  19. from studio import story
  20. from studio import studio_storyDeletionLayer
  21. from studio import studio_storyLayer
  22. from studio import studio_settingsLayer
  23. from studio import studio_assetLayer
  24. from studio import studio_analyticsLayer
  25. from studio import studio_scriptLayer
  26. from studio import studio_multiuserLayer
  27. from studio import studio_dialogs
  28. from troubleshooter import error_notify
  29. # UI modules
  30. from UI import UI_testing
  31. from UI import UI_color
  32. from UI import UI_elements
  33. # Network
  34. from network import network_renders
  35. from network import network_multiuser
  36. from network import multiuser_terminal
  37. def previous(win):
  38. win.previous = {}
  39. for i in win.current:
  40. if type(win.current[i]) == list or type(win.current[i]) is dict:
  41. win.previous[i] = win.current[i].copy()
  42. else:
  43. win.previous[i] = win.current[i]
  44. # OK let's make a window
  45. def run(project, win):
  46. # In the Blender-Organizer I was putting the version into the title. Not cool.
  47. # Because if you would snap it to the sidebar in Ubuntu. On mouse over it would
  48. # show the first ever version. So there will be a better way to see version.
  49. # I think let's do that like in Blender. Drawn with in the window somewhere.
  50. try:
  51. win.destroy()
  52. except:
  53. pass
  54. # Sometimes the projects could be corrupted. So we need to
  55. # insure that they are alright.
  56. fn = project
  57. try: os.mkdir(fn)
  58. except: pass
  59. try: os.mkdir(fn+"/rnd")
  60. except: pass
  61. try: os.mkdir(fn+"/dev")
  62. except: pass
  63. try: os.mkdir(fn+"/ast")
  64. except: pass
  65. try: os.mkdir(fn+"/pln")
  66. except: pass
  67. try: os.mkdir(fn+"/mus")
  68. except: pass
  69. try: os.mkdir(fn+"/set")
  70. except: pass
  71. for f in ["chr","loc","veh","obj"]:
  72. try: os.mkdir(fn+"/ast/"+f)
  73. except: pass
  74. try: os.mkdir(fn+"/dev/"+f)
  75. except: pass
  76. # Setting up the window
  77. win = Gtk.Window()
  78. win.set_default_size(1200,720)
  79. win.set_position(Gtk.WindowPosition.CENTER)
  80. win.maximize()
  81. win.connect("destroy", Gtk.main_quit)
  82. win.set_title("Blender-Pipeline")
  83. win.set_default_icon_from_file("tinyicon.png")
  84. # Setting up the events ( mouse and keyboard handling )
  85. win.connect("button-press-event", mouse_button_press, win)
  86. win.connect("button-release-event", mouse_button_release, win)
  87. win.connect("key-press-event", key_press, win)
  88. win.connect("key-release-event", key_release, win)
  89. # Guess what. The entire time on Blender-Organizer 4 ( 2018 -2020 ) and
  90. # few days of trying connecting the scroll event directly to window or to
  91. # the drawing area. And just now I finally made it work. BY DOING THIS
  92. # Why scroll event is only on ScrolledWindow ? OMG !!!
  93. scroll = Gtk.ScrolledWindow()
  94. scroll.connect("scroll-event", scrolling, win)
  95. # Setting up the global variables. (kinda)
  96. win.animations = {}
  97. win.floaters = [] # Currently active floating animations.
  98. win.previous = {}
  99. win.current = {}
  100. win.images = {}
  101. win.imageload = 0
  102. win.text = {}
  103. win.textactive = ""
  104. win.scroll = {}
  105. win.FPS = 0
  106. win.url = "story_editor"
  107. win.cur = "" # This will be used to get precicelly what asset / scene / shot we are in
  108. win.update = {"versions":{}}
  109. win.project = project
  110. win.out_dots = {}
  111. win.assets = {}
  112. win.szone = [[100,100],[100,100]] # Square drawn if selected more then one node.
  113. win.surround = { # And this is the list of the squares. Because it's not
  114. "frame":0, # as easy to do. See UI / UI_math / rectangle_surround()
  115. "rects":[] # for details of this implementation.
  116. }
  117. win.calllayer = False
  118. win.layercashe = {} # Here I gonna store layers that are inactive to speed up stuff
  119. win.checklists = {}
  120. win.blink = False # Cursor blinking thing.
  121. win.renders = {} # List of current active renders.
  122. win.undo_history = []
  123. win.undo_index = 0
  124. win.multiuser = {
  125. "server":False, # Whether we are connected to the server. And the server ID
  126. "userid":"", # The id of current user in the server
  127. "last_request": "", # The last request send to the server. ( for stopping bloat )
  128. "curs":{}, # The information about various items on other users machines.
  129. "request":[["story"]], # Requests done in UI space
  130. "users":{}, # List of users information Names, IPs etc.
  131. "messages":[], # List of messages
  132. "unread":0 , # Amount of unread messages
  133. "terminal":[], # The outputs from the server
  134. "asset_list_check":False, # Whether the initial update happened when connecting to the server
  135. "story_check":False, # Whether the first story check was done.
  136. "analytics_check":False
  137. }
  138. if pm_project.is_legacy(project):
  139. win.story = story.get_legacy(project)
  140. win.analytics = analytics.get_legacy(project)
  141. else:
  142. win.story = story.load(project)
  143. win.analytics = analytics.load(project)
  144. # Try to put a name of the project at the title
  145. win.set_title("Blender-Pipeline : "+win.analytics["name"])
  146. # Cashed tables
  147. win.color = UI_color.get_table()
  148. win.settings = settings.load_all()
  149. # I will initilize the remote server settings here, because I need to make it
  150. # as early as possible. Since I will need it in many places.
  151. if "from-remote-server" not in win.analytics:
  152. win.analytics["from-remote-server"] = False
  153. if "remote-server-url" not in win.analytics:
  154. win.analytics["remote-server-url"] = ""
  155. # Default values
  156. win.current["frame"] = 0
  157. win.current["testing"] = False
  158. win.current["LMB"] = False
  159. win.current["MMB"] = False
  160. win.current["RMB"] = False
  161. win.current["keys"] = []
  162. win.current["key_letter"] = ""
  163. win.current["scroll"] = [0,0]
  164. win.current["project"] = ""
  165. win.current["tool"] = "selection"
  166. win.current["draw_dot"] = "end"
  167. win.current["calls"] = {} # Calls. See sutdio/studio_dialogs.py
  168. win.current["script_find"] = [0,0]
  169. win.current["camera_arrived"] = False
  170. # Remote server stuff
  171. win.current["http-server"] = {"message":"",
  172. "fileprog": 0}
  173. win.current["in-asset-remote-server"] = True
  174. if "pointers" not in win.story:
  175. win.story["pointers"] = {} # List of text pointers per scene
  176. new_date_format = "%Y/%m/%d"
  177. today = datetime.datetime.strftime(datetime.datetime.today(), new_date_format)
  178. win.current["date"] = today # Don't even ask. I'm litteraly tired already.
  179. previous(win)
  180. # Version of the software
  181. win.version = 0.0
  182. try:
  183. vfile = open("settings/update.data")
  184. vfile = vfile.read()
  185. vfile = vfile.split("\n")
  186. for line in vfile:
  187. if line.startswith("VERSION "):
  188. win.version = float(line.replace("VERSION ", ""))
  189. break
  190. except:
  191. win.version = 0.0
  192. # FPS
  193. win.sFPS = datetime.datetime.now()
  194. # Setting the drawable
  195. pmdraw = Gtk.DrawingArea()
  196. pmdraw.set_size_request(1040, 720)
  197. scroll.set_size_request(1040, 720) # This step is because GTK developers are
  198. win.add(scroll) # well. A good, nice, people who knows
  199. scroll.add_with_viewport(pmdraw) # what they are doing. Really.
  200. pmdraw.connect("draw", pmdrawing, win)
  201. # # Before we run the UI. Let's run the mutliuser client.
  202. # multiuser = threading.Thread(target=network_multiuser.client, args=(win,))
  203. # multiuser.setDaemon(True)
  204. # multiuser.start()
  205. # # And let's start also the multiuser_terminal
  206. # multiuser_term = threading.Thread(target=multiuser_terminal.listen, args=(win,))
  207. # multiuser_term.setDaemon(True)
  208. # multiuser_term.start()
  209. #run
  210. win.show_all()
  211. Gtk.main()
  212. def pmdrawing(pmdrawing, main_layer, win):
  213. # This function draws the actuall image. I'm doing full frames redraws. It's
  214. # a bit simpler then making some kind of dynamic draw call system that might
  215. # be used in such an application. But to hell with it. I did the same on the
  216. # Blender-Organizer altho with way less cairo. And it works well enought.
  217. try:
  218. # FPS counter
  219. win.fFPS = datetime.datetime.now()
  220. win.tFPS = win.fFPS - win.sFPS
  221. if win.current["frame"] % 5 == 0:
  222. win.blink = not win.blink # Iterating the blink
  223. if win.current["frame"] % 10 == 0:
  224. win.FPS = int ( 1.0 / ( win.tFPS.microseconds /1000000))
  225. if "Auto_De-Blur" not in win.settings:
  226. win.settings["Auto_De-Blur"] = True
  227. # Fail switch for Graphics.
  228. if win.FPS < 10 and win.settings["Auto_De-Blur"]:
  229. win.settings["Blur"] = False
  230. win.sFPS = datetime.datetime.now()
  231. # Current frame (for animations and things like this)
  232. win.current["frame"] += 1
  233. if win.current["frame"] == 10 and not win.render_runtime.get("to_render"):
  234. win.cur = "/set"
  235. win.url = "analytics"
  236. elif win.current["frame"] == 10 and win.renders:
  237. def after(win, var):
  238. pass
  239. studio_dialogs.render(win, "current_renders", after)
  240. if not win.url == "assets" and not win.url == "http-server":
  241. win.current["in-asset-remote-server"] = True
  242. if not "scale" in win.settings:
  243. settings.write("scale", 1) # Writing to file
  244. win.settings = settings.load_all()
  245. # Getting data about the frame
  246. win.current['mx'] = int(round(win.get_pointer()[0] / win.settings["scale"]))
  247. win.current['my'] = int(round(win.get_pointer()[1] / win.settings["scale"]))
  248. win.current['w'] = int(round(win.get_size()[0] / win.settings["scale"] ))
  249. win.current['h'] = int(round(win.get_size()[1] / win.settings["scale"] ))
  250. if 65451 in win.current["keys"]:
  251. settings.write("scale", win.settings["scale"]+0.2)
  252. win.settings = settings.load_all()
  253. win.current["keys"] = []
  254. elif 65453 in win.current["keys"]:
  255. settings.write("scale", win.settings["scale"]-0.2)
  256. win.settings = settings.load_all()
  257. win.current["keys"] = []
  258. win.cursors = {
  259. "arrow":Gdk.Cursor.new(Gdk.CursorType.ARROW),
  260. "watch":Gdk.Cursor.new(Gdk.CursorType.WATCH),
  261. "text" :Gdk.Cursor.new(Gdk.CursorType.XTERM),
  262. "hand" :Gdk.Cursor.new(Gdk.CursorType.HAND1),
  263. "cross":Gdk.Cursor.new(Gdk.CursorType.CROSS)
  264. }
  265. win.current["cursor"] = win.cursors["arrow"]
  266. # Attemt to make things straight when pressing Ctrl.
  267. if 65507 in win.current["keys"] and win.current["LMB"]:
  268. # Let's see what's got more distance. X or Y motion of the mouse.
  269. dx = win.current["LMB"][0] - win.current["mx"]
  270. dx = max(dx, dx*-1)
  271. dy = win.current["LMB"][1] - win.current["my"]
  272. dy = max(dy, dy*-1)
  273. # If X has more ditance. Then Y should be the same as begining.
  274. if dx > dy:
  275. win.current["my"] = win.current["LMB"][1]
  276. else:
  277. win.current["mx"] = win.current["LMB"][0]
  278. # Attempt at making the mouse cursor a bit more usefull for stuff.
  279. if win.previous["LMB"] and len(win.previous["LMB"]) > 2 and win.current["LMB"]:
  280. win.current["LMB"] = win.previous["LMB"]
  281. #Background color
  282. UI_color.set(main_layer, win, "background")
  283. main_layer.rectangle(
  284. 0,
  285. 0,
  286. win.current['w']*win.settings["scale"],
  287. win.current['h']*win.settings["scale"])
  288. main_layer.fill()
  289. # Tooltips and other junk has to be defined here. And then drawn later to
  290. # the screen. So here we get a special layer. That will be drawn to during
  291. # the time of drawing. And later composeted over everything.
  292. win.tooltip_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, win.current['w'],
  293. win.current['h'])
  294. win.tooltip = cairo.Context(win.tooltip_surface)
  295. win.tooltip.select_font_face("Monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
  296. # Layers. Order of them matter
  297. Layers = []
  298. if win.url == "story_editor":
  299. Layers.append([studio_storyLayer.layer(win),"story_editor"])
  300. if "story_editor" in win.layercashe:
  301. del win.layercashe["story_editor"]
  302. else:
  303. if "story_editor" not in win.layercashe:
  304. win.layercashe["story_editor" ] = studio_storyLayer.layer(win)
  305. Layers.append([win.layercashe["story_editor" ],"story_editor"])
  306. if win.url == "story_deletion_dialog":
  307. Layers.append([studio_storyDeletionLayer.layer(win),"story_deletion_dialog"])
  308. elif win.url == "settings_layer":
  309. Layers.append([studio_settingsLayer.layer(win),"settings_layer"])
  310. elif win.url == "analytics":
  311. Layers.append([studio_analyticsLayer.layer(win),"analytics"])
  312. elif win.url == "assets":
  313. Layers.append([studio_assetLayer.layer(win),"assets"])
  314. elif win.url == "script":
  315. Layers.append([studio_scriptLayer.layer(win), "script"])
  316. elif win.url == "multiuser":
  317. Layers.append([studio_multiuserLayer.layer(win), "multiuser"])
  318. # Call layers. See studio/studio_dialogs.py for explanation. It's wild.
  319. win.calllayer = False
  320. remlater = []
  321. for call in list(win.current["calls"].keys()):
  322. if win.current["calls"][call]["var"] == None:
  323. Layers.append([win.current["calls"][call]["draw"](win, call)])
  324. win.url = win.current["calls"][call]["url"]
  325. win.calllayer = True
  326. else:
  327. win.url = win.current["calls"][call]["back"]
  328. win.current["calls"][call]["call"](win, win.current["calls"][call]["var"])
  329. win.textactive = ""
  330. remlater.append(call)
  331. for call in remlater:
  332. try:
  333. del win.current["calls"][call]
  334. except:
  335. pass
  336. Layers.append([UI_testing.layer(win)])
  337. Layers.append([win.tooltip_surface])
  338. if win.settings["scale"] != 1:
  339. main_layer.scale(win.settings["scale"],
  340. win.settings["scale"])
  341. # Combining layers
  342. for layer in Layers:
  343. if len(layer) > 1:
  344. layer, url = layer
  345. blur = UI_elements.animate(url+"_blur", win, 50)
  346. if (win.url != url or win.calllayer):
  347. if win.current["tool"] not in ["schedule", "grab"]:
  348. blur = UI_elements.animate(url+"_blur", win, blur, 50, 2, True)
  349. else:
  350. blur = UI_elements.animate(url+"_blur", win, 50, 50, 0, True)
  351. else:
  352. if win.current["tool"] not in ["schedule", "grab"]:
  353. blur = UI_elements.animate(url+"_blur", win, blur, 0, 2, True)
  354. else:
  355. blur = UI_elements.animate(url+"_blur", win, 0, 0, 0, True)
  356. layer = UI_elements.blur(layer, win, blur)
  357. else:
  358. layer = layer[0]
  359. main_layer.set_source_surface(layer, 0 , 0)
  360. main_layer.paint()
  361. # Floaters
  362. fdels = []
  363. for n, floaty in enumerate(win.floaters):
  364. fx = floaty["fx"] # From X
  365. fy = floaty["fy"] # From Y
  366. tx = floaty["tx"] # To X
  367. ty = floaty["ty"] # To Y
  368. ff = floaty["ff"] # From Frame
  369. tf = floaty["tf"] # To Frame
  370. sf = floaty["sf"] # Surface
  371. total_frames = tf - ff
  372. passed_frames = win.current["frame"] - ff
  373. frames_fraction = passed_frames / total_frames
  374. if frames_fraction == 1:
  375. fdels.append(n)
  376. x = fx + ( (tx - fx) * frames_fraction )
  377. y = fy + ( (ty - fy) * frames_fraction )
  378. main_layer.set_source_surface(sf, x , y)
  379. main_layer.paint_with_alpha(frames_fraction*-1+1)
  380. for n in fdels:
  381. try:
  382. del win.floaters[n]
  383. except:
  384. pass
  385. win.get_root_window().set_cursor(win.current["cursor"])
  386. # If you press ESC you get back from any window to the main menu.
  387. if 65307 in win.current["keys"] and win.url != "install_updates":
  388. win.url = "story_editor"
  389. win.story["selected"] = []
  390. win.current["tool"] = "selection"
  391. win.current["keys"] = []
  392. win.textactive = ""
  393. # For the rendering I will need to read render info at each frame and parse
  394. # it in various windows.
  395. network_renders.read_renders(win)
  396. # There is a but in the Gnome I guess. That autopresses the Shift and Ctrl
  397. # keys when you scroll to different windows. So here is a little fix.
  398. if not win.is_active():
  399. win.current["keys"] = []
  400. elif "is_active" in win.previous and not win.previous["is_active"]:
  401. win.multiuser["last_request"] = ""
  402. UI_elements.reload_images(win)
  403. win.current["is_active"] = win.is_active()
  404. # Saving data about this frame for the next one. A bit hard to get WTF am I
  405. # doing here. Basically trying to avoid current and previous data to be links
  406. # of the same data.
  407. previous(win) # Moved it into a seprate function for obvoius reasons
  408. # Refreshing those that need to be refrashed
  409. win.current["scroll"] = [0,0]
  410. # Refreshing the frame automatically
  411. pmdrawing.queue_draw()
  412. except:
  413. Gtk.main_quit()
  414. error_notify.show()
  415. # This program will have things like mouse and keyboard input. And this setup
  416. # Will be done in both PM and the actuall Project window. ( Also in the render
  417. # Window. Basically all separate windows will have to have this setup separatelly.
  418. # Mouse
  419. def mouse_button_press(widget, event, win):
  420. # This function marks activation of the button. Not it's deactivation.
  421. # I'm going to attempt something quite disturbing. Basically I want to save
  422. # the state of the mouse as the press begun untill it's released. And I'm
  423. # going to do in a slightly weird way. Because I'm bored I guess. The prob-
  424. # lem is that it will require to check whether the data even exists in the
  425. # first place. If x. Before parsing it. Because it might be False.
  426. for i, button in enumerate(["LMB", "MMB", "RMB"]):
  427. if i+1 == int(event.get_button()[1]):
  428. win.current[button] = [event.x / win.settings["scale"],
  429. event.y / win.settings["scale"]]
  430. # If you folowed the code. By checking for example if win.current["LMB"]
  431. # You can know if it's even pressed to begin with. Because if it's not
  432. # It's False.
  433. def mouse_button_release(widget, event, win):
  434. # This function reverses the effects of the mouse_button_press() function.
  435. for i, button in enumerate(["LMB", "MMB", "RMB"]):
  436. if i+1 == int(event.get_button()[1]):
  437. win.current[button] = False
  438. # I guess it's time to make something similar for the keyboard keys as well.
  439. # I'm going to reuse the old system from the Blender-Organizer. Just a list of
  440. # pressed keys. Maybe as well a strting thingy. Because I want to type in this
  441. # app.
  442. def key_press(widget, event, win):
  443. if event.keyval not in win.current["keys"]:
  444. win.current["keys"].append(event.keyval)
  445. win.current["key_letter"] = event.string
  446. def key_release(widget, event, win):
  447. try:
  448. win.current["keys"].remove(event.keyval)
  449. except:
  450. win.current["keys"] = []
  451. # I also want to clean the key letter. Because other wise in the
  452. # script writer I had weird key presses all the time.
  453. if not win.current["keys"]:
  454. win.current["key_letter"] = ""
  455. def scrolling(widget, event, win):
  456. e, x, y = event.get_scroll_deltas()
  457. win.current["scroll"] = [x,y]