├── .gitattributes └── code_editor.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /code_editor.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Code Editor", 21 | "location": "Text Editor > Righ Click Menu", 22 | "version": (0,1,0), 23 | "blender": (2,7,2), 24 | "description": "Better editor for coding", 25 | "author": "", 26 | "category": "Text Editor", 27 | } 28 | 29 | import bpy 30 | import bgl 31 | import blf 32 | import time 33 | import string 34 | import threading 35 | 36 | # ===================================================== 37 | # CODE EDITTING 38 | # ===================================================== 39 | 40 | def custom_home(line, cursor_loc): 41 | """Returns position of first character in line""" 42 | for id, ch in enumerate(line): 43 | if ch != ' ' or id >= cursor_loc-1 : 44 | return id 45 | return 0 46 | 47 | def smart_complete(input_string): 48 | """ 49 | Based on input: 50 | - Evaluates numeric expression 51 | - Puts quotes("") around 52 | - Makes a list from selection 53 | """ 54 | def handle_letters(input_text): 55 | if all([ch in string.ascii_letters + string.digits + '_' for ch in input_text]): 56 | return '"' + input_string + '"' 57 | if any([ch in '!#$%&\()*+,-/:;<=>?@[\\]^`{|}~' for ch in input_text]): 58 | return input_text 59 | else: 60 | input_text.split(" ") 61 | return '[' + ', '.join([ch for ch in input_text.split(" ") if ch!='']) + ']' 62 | 63 | # try numbers 64 | if not input_string: 65 | return "" 66 | elif not any([ch in string.digits for ch in input_string]): 67 | return handle_letters(input_string) 68 | else: 69 | try: 70 | e = 2.71828 # euler number 71 | phi = 1.61803 # golden ratio 72 | pi = 3.141592 # really? 73 | g = 9.80665 # gravitation accel 74 | answer = eval(input_string) 75 | return str(round(answer, 5)) 76 | except: 77 | return handle_letters(input_string) 78 | 79 | # ===================================================== 80 | # MINIMAP SYNTAX 81 | # ===================================================== 82 | 83 | class ThreadedSyntaxHighlighter(threading.Thread): 84 | """Breaks text into segments based on syntax for highlighting""" 85 | def __init__(self, text, output_list): 86 | threading.Thread.__init__(self) 87 | self.daemon = True 88 | self.output = output_list 89 | self.text = text 90 | self._restart = threading.Event() 91 | self._paused = threading.Event() 92 | self._state = threading.Condition() 93 | 94 | def restart(self, text): 95 | with self._state: 96 | self.text = text 97 | self._restart.set() 98 | self._paused.clear() 99 | self._state.notify() # unblock self if in paused mode 100 | 101 | def run(self): 102 | while True: 103 | with self._state: 104 | if self._paused.is_set(): 105 | self._state.wait() # wait until notify() - the thread is paused 106 | 107 | self._restart.clear() 108 | # do stuff 109 | self.highlight() 110 | 111 | def highlight(self): 112 | # approximate syntax higlighter, accuracy is traded for speed 113 | 114 | # start empty 115 | n_lines = len(self.text.lines) if self.text else 0 116 | data = {} 117 | data['plain'] = [[] for i in range(n_lines)] 118 | data['strings'] = [[] for i in range(n_lines)] 119 | data['comments'] = [[] for i in range(n_lines)] 120 | data['numbers'] = [[] for i in range(n_lines)] 121 | data["special"] = [[] for i in range(n_lines)] 122 | special_temp = [] 123 | data["builtin"] = [[] for i in range(n_lines)] 124 | data["prepro"] = [[] for i in range(n_lines)] 125 | data['tabs'] = [[] for i in range(n_lines)] 126 | 127 | # syntax element structure 128 | element = [0, # line id 129 | 0, # element start position 130 | 0, # element end position 131 | 0] # special block end line 132 | 133 | # this is used a lot for ending plain text segment 134 | def close_plain(element, w): 135 | """Ends non-highlighted text segment""" 136 | if element[1] < w: 137 | element[2] = w - 1 138 | data['plain'][element[0]].append([element[1], element[2]]) 139 | element[1] = w 140 | return 141 | 142 | # this ends collapsible code block 143 | def close_block(h, indent): 144 | for entry in list(special_temp): 145 | if entry[0] < h and entry[1] >= indent: 146 | data['special'][entry[0]].append([entry[1], entry[2], h-empty_lines]) 147 | special_temp.remove(entry) 148 | return 149 | 150 | # recognized tags definitions, only the most used 151 | builtin = ['return','break','continue','yield','with','while','for ', 'import ', 'from ', 152 | 'if ','elif ',' else','None','True','False','and ','not ','in ','is '] 153 | special = ['def ','class '] 154 | 155 | # flags of syntax state machine 156 | state = None 157 | empty_lines = 0 158 | timer = -1 # timer to skip characters and close segment at t=0 159 | 160 | # process the text if there is one 161 | for h, line in enumerate(self.text.lines if self.text else []): 162 | 163 | # new line new element, carry string flag 164 | element[0] = h 165 | element[1] = 0 166 | if state not in ['Q_STRING', 'A_STRING']: state = None 167 | indent = 0 168 | block_close = any([char not in string.whitespace for char in line.body]) 169 | 170 | # process each line and break into syntax elements 171 | for w, ch in enumerate(line.body): 172 | 173 | # end if restarted 174 | if self._restart.is_set(): 175 | return 176 | 177 | if timer > 0: 178 | timer -= 1 179 | 180 | elif timer < 0: 181 | # tabs 182 | if not state and line.body.startswith(' ', w): 183 | close_plain(element, w) 184 | state = 'TAB' 185 | timer = 3 186 | indent += 4 187 | elif state in ['Q_STRING', 'A_STRING'] and line.body.startswith(' ', w): 188 | element[1] = w + 4 189 | indent += 4 190 | 191 | # bilt-in 192 | if not state and ch in "rbcywfieNTFan": 193 | results = [line.body.startswith(x, w) for x in builtin] 194 | if any(results): 195 | close_plain(element, w) 196 | state = 'BUILTIN' 197 | timer = len(builtin[results.index(True)]) - 1 198 | 199 | # special 200 | if not state and ch in "dc": 201 | results = [line.body.startswith(x, w) for x in special] 202 | if any(results): 203 | close_plain(element, w) 204 | state = 'SPECIAL' 205 | timer = len(special[results.index(True)]) - 1 206 | 207 | # numbers 208 | if not state and ch in ['0','1','2','3','4','5','6','7','8','9']: 209 | close_plain(element, w) 210 | state = 'NUMBER' 211 | elif state == 'NUMBER' and ch not in ['0','1','2','3','4','5','6','7','8','9','.']: 212 | element[2] = w 213 | data['numbers'][element[0]].append([element[1], element[2]]) 214 | element[1] = w 215 | state = None 216 | 217 | # "" string 218 | if not state and ch == '"': 219 | close_plain(element, w) 220 | state = 'Q_STRING' 221 | elif state == 'Q_STRING' and ch == '"': 222 | if w > 1 and line.body[w-1]=='\\' and line.body[w-2]=='\\': 223 | timer = 0 224 | elif w > 0 and line.body[w-1]=='\\': 225 | pass 226 | else: 227 | timer = 0 228 | 229 | # '' string 230 | elif not state and ch == "'": 231 | close_plain(element, w) 232 | state = 'A_STRING' 233 | elif state == 'A_STRING' and ch == "'": 234 | if w > 1 and line.body[w-1]=='\\' and line.body[w-2]=='\\': 235 | timer = 0 236 | elif w > 0 and line.body[w-1]=='\\': 237 | pass 238 | else: 239 | timer = 0 240 | 241 | # comment 242 | elif not state and ch == '#': 243 | close_plain(element, w) 244 | state = 'COMMENT' 245 | # close code blocks 246 | if block_close: 247 | for i, j in enumerate(line.body): 248 | if i > 0 and j != " ": 249 | close_block(h, 4*int(i/4)) 250 | break 251 | 252 | # preprocessor 253 | elif not state and ch == '@': 254 | close_plain(element, w) 255 | state = 'PREPRO' 256 | # close code blocks 257 | if block_close: 258 | close_block(h, indent) 259 | break 260 | 261 | # close special blocks 262 | if state != 'TAB' and block_close: 263 | block_close = False 264 | close_block(h, indent) 265 | 266 | # write element when timer 0 267 | if timer == 0: 268 | element[2] = w 269 | if state == 'TAB': 270 | data['tabs'][element[0]].append([element[1], element[2]]) 271 | element[1] = w + 1 272 | elif state in ['Q_STRING', 'A_STRING']: 273 | data['strings'][element[0]].append([element[1], element[2]]) 274 | element[1] = w + 1 275 | elif state == 'BUILTIN': 276 | data['builtin'][element[0]].append([element[1], element[2]]) 277 | element[1] = w + 1 278 | elif state == 'SPECIAL': 279 | special_temp.append(element.copy()) 280 | element[1] = w + 1 281 | state = None 282 | timer = -1 283 | 284 | # count empty lines 285 | empty_lines = 0 if any([ch not in string.whitespace for ch in line.body]) else empty_lines + 1 286 | 287 | # handle line ends 288 | if not state: 289 | element[2] = len(line.body) 290 | data['plain'][element[0]].append([element[1], element[2]]) 291 | elif state == 'COMMENT': 292 | element[2] = len(line.body) 293 | data['comments'][element[0]].append([element[1], element[2]]) 294 | elif state == 'PREPRO': 295 | element[2] = len(line.body) 296 | data['prepro'][element[0]].append([element[1], element[2]]) 297 | elif state in ['Q_STRING', 'A_STRING']: 298 | element[2] = len(line.body) 299 | data['strings'][element[0]].append([element[1], element[2]]) 300 | elif state == 'NUMBER': 301 | element[2] = len(line.body) 302 | data['numbers'][element[0]].append([element[1], element[2]]) 303 | 304 | # close all remaining blocks 305 | for entry in special_temp: 306 | data['special'][entry[0]].append([entry[1], entry[2], h+1-empty_lines]) 307 | 308 | # done 309 | self.output[0]['elements'] = data['plain'] 310 | self.output[1]['elements'] = data['strings'] 311 | self.output[2]['elements'] = data['comments'] 312 | self.output[3]['elements'] = data['numbers'] 313 | self.output[4]['elements'] = data["builtin"] 314 | self.output[5]['elements'] = data['prepro'] 315 | self.output[6]['elements'] = data["special"] 316 | self.output[7]['elements'] = data['tabs'] 317 | 318 | # enter sleep 319 | with self._state: 320 | self._paused.set() # enter sleep mode 321 | return 322 | 323 | # ===================================================== 324 | # OPENGL DRAWCALS 325 | # ===================================================== 326 | 327 | def draw_callback_px(self, context): 328 | """Draws Code Editors Minimap and indentation marks""" 329 | 330 | def draw_line(origin, length, thickness, vertical = False): 331 | """Drawing lines with polys, its faster""" 332 | x = (origin[0] + thickness) if vertical else (origin[0] + length) 333 | y = (origin[1] + length) if vertical else (origin[1] + thickness) 334 | bgl.glBegin(bgl.GL_QUADS) 335 | for v1, v2 in [origin, (x, origin[1]), (x, y), (origin[0], y)]: 336 | bgl.glVertex2i(v1, v2) 337 | bgl.glEnd() 338 | return 339 | 340 | # abort if another text editor 341 | if self.area == context.area and self.window == context.window: 342 | bgl.glEnable(bgl.GL_BLEND) 343 | else: 344 | return 345 | 346 | start = time.clock() 347 | 348 | # init params 349 | font_id = 0 350 | self.width = next(region.width for region in context.area.regions if region.type=='WINDOW') 351 | self.height = next(region.height for region in context.area.regions if region.type=='WINDOW') 352 | dpi_r = context.user_preferences.system.dpi / 72.0 353 | self.left_edge = self.width - round(dpi_r*(self.width+5*self.minimap_width)/10.0) 354 | self.right_edge = self.width - round(dpi_r*15) 355 | self.opacity = min(max(0,(self.width-self.min_width) / 100.0),1) 356 | 357 | # compute character dimensions 358 | mcw = dpi_r * self.minimap_symbol_width # minimap char width 359 | mlh = round(dpi_r * self.minimap_line_height) # minimap line height 360 | fs = context.space_data.font_size 361 | cw = round(dpi_r * round(2 + 0.6 * (fs - 4))) # char width 362 | ch = round(dpi_r * round(2 + 1.3 * (fs - 2) + ((fs % 10) == 0))) # char height 363 | 364 | # panel background box 365 | self.tab_width = round(dpi_r * 25) if (self.tabs and len(bpy.data.texts) > 1) else 0 366 | bgl.glColor4f(self.background.r, self.background.g, self.background.b, (1-self.bg_opacity)*self.opacity) 367 | bgl.glBegin(bgl.GL_QUADS) 368 | for x, y in [(self.left_edge-self.tab_width, self.height), 369 | (self.right_edge, self.height), 370 | (self.right_edge, 0), 371 | (self.left_edge-self.tab_width, 0)]: 372 | bgl.glVertex2i(x, y) 373 | bgl.glEnd() 374 | 375 | # line numbers background 376 | space = context.space_data 377 | if space.text: 378 | lines = len(space.text.lines) 379 | lines_digits = len(str(lines)) if space.show_line_numbers else 0 380 | self.line_bar_width = int(dpi_r*5)+cw*(lines_digits) 381 | bgl.glColor4f(self.background.r, self.background.g, self.background.b, 1) 382 | bgl.glBegin(bgl.GL_QUADS) 383 | for x, y in [(0, self.height), (self.line_bar_width, self.height), (self.line_bar_width, 0), (0, 0)]: 384 | bgl.glVertex2i(x, y) 385 | bgl.glEnd() 386 | # shadow 387 | bgl.glLineWidth(1.0 * dpi_r) 388 | for id, intensity in enumerate([0.2,0.1,0.07,0.05,0.03,0.02,0.01]): 389 | bgl.glColor4f(0.0, 0.0, 0.0, intensity) 390 | bgl.glBegin(bgl.GL_LINE_STRIP) 391 | for x, y in [(self.line_bar_width+id, 0), 392 | (self.line_bar_width+id, self.height)]: 393 | bgl.glVertex2i(x, y) 394 | bgl.glEnd() 395 | 396 | # minimap shadow 397 | for id, intensity in enumerate([0.2,0.1,0.07,0.05,0.03,0.02,0.01]): 398 | bgl.glColor4f(0.0, 0.0, 0.0, intensity*self.opacity) 399 | bgl.glBegin(bgl.GL_LINE_STRIP) 400 | for x, y in [(self.left_edge-id-self.tab_width, 0), 401 | (self.left_edge-id-self.tab_width, self.height)]: 402 | bgl.glVertex2i(x, y) 403 | bgl.glEnd() 404 | 405 | # divider 406 | if self.tab_width: 407 | bgl.glColor4f(0.0, 0.0, 0.0, 0.2*self.opacity) 408 | bgl.glBegin(bgl.GL_LINE_STRIP) 409 | for x, y in [(self.left_edge, 0), 410 | (self.left_edge, self.height)]: 411 | bgl.glVertex2i(x, y) 412 | bgl.glEnd() 413 | 414 | # if there is text in window 415 | if space.text and self.opacity: 416 | 417 | # minimap horizontal sliding based on text block length 418 | max_slide = max(0, mlh*(lines+self.height/ch) - self.height) 419 | self.slide = int(max_slide * space.top / lines) 420 | minimap_top_line = int(self.slide/mlh) 421 | minimap_bot_line = int((self.height+self.slide)/mlh) 422 | 423 | # draw minimap visible box 424 | if self.in_minimap: 425 | bgl.glColor4f(1.0, 1.0, 1.0, 0.1*self.opacity) 426 | else: 427 | bgl.glColor4f(1.0, 1.0, 1.0, 0.07*self.opacity) 428 | bgl.glBegin(bgl.GL_QUADS) 429 | for x, y in [(self.left_edge, self.height-mlh*space.top + self.slide), 430 | (self.right_edge, self.height-mlh*space.top + self.slide), 431 | (self.right_edge, self.height-mlh*(space.top+space.visible_lines) + self.slide), 432 | (self.left_edge, self.height-mlh*(space.top+space.visible_lines) + self.slide)]: 433 | bgl.glVertex2i(x, y) 434 | bgl.glEnd() 435 | 436 | # draw minimap code 437 | for segment in self.segments[:-1]: 438 | bgl.glColor4f(segment['col'][0], segment['col'][1], segment['col'][2], 0.4*self.opacity) 439 | for id, element in enumerate(segment['elements'][minimap_top_line:minimap_bot_line]): 440 | loc_y = mlh*(id+minimap_top_line+3) - self.slide 441 | for sub_element in element: 442 | draw_line((self.left_edge+int(mcw*(sub_element[0]+4)), self.height-loc_y), 443 | int(mcw*(sub_element[1]-sub_element[0])), 444 | int(0.5 * mlh)) 445 | 446 | # minimap code marks 447 | bgl.glColor4f(self.segments[-2]['col'][0], 448 | self.segments[-2]['col'][1], 449 | self.segments[-2]['col'][2], 450 | 0.3*self.block_trans*self.opacity) 451 | for id, element in enumerate(self.segments[-2]['elements']): 452 | for sub_element in element: 453 | if sub_element[2] >= space.top or id < space.top+space.visible_lines: 454 | draw_line((self.left_edge+int(mcw*(sub_element[0]+4)), self.height-mlh*(id+3)+self.slide), 455 | -int(mlh*(sub_element[2]-id-1)), 456 | int(0.5 * mlh), 457 | True) 458 | 459 | # draw dotted indentation marks 460 | bgl.glLineWidth(1.0 * dpi_r) 461 | if space.text: 462 | bgl.glColor4f(self.segments[0]['col'][0], 463 | self.segments[0]['col'][1], 464 | self.segments[0]['col'][2], 465 | self.indent_trans) 466 | for id, element in enumerate(self.segments[-1]['elements'][space.top:space.top+space.visible_lines]): 467 | loc_y = id 468 | for sub_element in element: 469 | draw_line((int(dpi_r*10)+cw*(lines_digits+sub_element[0]+4), self.height-ch*(loc_y)), 470 | -ch, 471 | int(1 * dpi_r), 472 | True) 473 | 474 | # draw code block marks 475 | bgl.glColor4f(self.segments[-2]['col'][0], 476 | self.segments[-2]['col'][1], 477 | self.segments[-2]['col'][2], 478 | self.block_trans) 479 | for id, element in enumerate(self.segments[-2]['elements']): 480 | for sub_element in element: 481 | if sub_element[2] >= space.top or id < space.top+space.visible_lines: 482 | bgl.glBegin(bgl.GL_LINE_STRIP) 483 | bgl.glVertex2i(int(dpi_r*10+cw*(lines_digits+sub_element[0])), 484 | self.height-ch*(id+1-space.top)) 485 | bgl.glVertex2i(int(dpi_r*10+cw*(lines_digits+sub_element[0])), 486 | self.height-int(ch*(sub_element[2]-space.top))) 487 | bgl.glVertex2i(int(dpi_r*10+cw*(lines_digits+sub_element[0]+1)), 488 | self.height-int(ch*(sub_element[2]-space.top))) 489 | bgl.glEnd() 490 | 491 | # tab dividers 492 | if self.tab_width and self.opacity: 493 | self.tab_height = min(200, int(self.height/len(bpy.data.texts))) 494 | y_loc = self.height-5 495 | for text in bpy.data.texts: 496 | # tab selection 497 | if text.name == self.in_tab: 498 | bgl.glColor4f(1.0, 1.0, 1.0, 0.05*self.opacity) 499 | bgl.glBegin(bgl.GL_QUADS) 500 | for x, y in [(self.left_edge-self.tab_width, y_loc), 501 | (self.left_edge, y_loc), 502 | (self.left_edge, y_loc-self.tab_height), 503 | (self.left_edge-self.tab_width, y_loc-self.tab_height)]: 504 | bgl.glVertex2i(x, y) 505 | bgl.glEnd() 506 | # tab active 507 | if context.space_data.text and text.name == context.space_data.text.name: 508 | bgl.glColor4f(1.0, 1.0, 1.0, 0.05*self.opacity) 509 | bgl.glBegin(bgl.GL_QUADS) 510 | for x, y in [(self.left_edge-self.tab_width, y_loc), 511 | (self.left_edge, y_loc), 512 | (self.left_edge, y_loc-self.tab_height), 513 | (self.left_edge-self.tab_width, y_loc-self.tab_height)]: 514 | bgl.glVertex2i(x, y) 515 | bgl.glEnd() 516 | bgl.glColor4f(0.0, 0.0, 0.0, 0.2*self.opacity) 517 | y_loc -= self.tab_height 518 | bgl.glBegin(bgl.GL_LINE_STRIP) 519 | for x, y in [(self.left_edge-self.tab_width, y_loc), 520 | (self.left_edge, y_loc)]: 521 | bgl.glVertex2i(x, y) 522 | bgl.glEnd() 523 | 524 | # draw fps 525 | # bgl.glColor4f(1, 1, 1, 0.2) 526 | # blf.size(font_id, fs, int(dpi_r*72)) 527 | # blf.position(font_id, self.left_edge-50, 5, 0) 528 | # blf.draw(font_id, str(round(1/(time.clock() - start),3))) 529 | 530 | # draw line numbers 531 | if space.text: 532 | bgl.glColor4f(self.segments[0]['col'][0], 533 | self.segments[0]['col'][1], 534 | self.segments[0]['col'][2], 535 | 0.5) 536 | for id in range(space.top, min(space.top+space.visible_lines+1, lines+1)): 537 | if self.in_line_bar and self.segments[-2]['elements'][id-1]: 538 | bgl.glColor4f(self.segments[-2]['col'][0], 539 | self.segments[-2]['col'][1], 540 | self.segments[-2]['col'][2], 541 | 1) 542 | blf.position(font_id, 2+int(0.5*cw*(len(str(lines))-1)), self.height-ch*(id-space.top)+3, 0) 543 | #blf.draw(font_id, '→') 544 | blf.draw(font_id, '↓') 545 | bgl.glColor4f(self.segments[0]['col'][0], 546 | self.segments[0]['col'][1], 547 | self.segments[0]['col'][2], 548 | 0.5) 549 | else: 550 | blf.position(font_id, 2+int(0.5*cw*(len(str(lines))-len(str(id)))), self.height-ch*(id-space.top)+3, 0) 551 | blf.draw(font_id, str(id)) 552 | 553 | # draw file names 554 | if self.tab_width: 555 | blf.enable(font_id, blf.ROTATION) 556 | blf.rotation(font_id, 1.570796) 557 | y_loc = self.height 558 | for text in bpy.data.texts: 559 | text_max_length = max(2,int((self.tab_height - 40)/cw)) 560 | name = text.name[:text_max_length] 561 | if text_max_length < len(text.name): 562 | name += '...' 563 | bgl.glColor4f(self.segments[0]['col'][0], 564 | self.segments[0]['col'][1], 565 | self.segments[0]['col'][2], 566 | (0.7 if text.name == self.in_tab else 0.4)*self.opacity) 567 | blf.position(font_id, 568 | self.left_edge-round((self.tab_width-ch)/2.0)-5, 569 | round(y_loc-(self.tab_height/2)-cw*len(name)/2), 570 | 0) 571 | blf.draw(font_id, name) 572 | y_loc -= self.tab_height 573 | 574 | # restore opengl defaults 575 | bgl.glColor4f(0, 0, 0, 1) 576 | bgl.glLineWidth(1.0) 577 | bgl.glDisable(bgl.GL_BLEND) 578 | blf.disable(font_id, blf.ROTATION) 579 | return 580 | 581 | # ===================================================== 582 | # OPERATORS 583 | # ===================================================== 584 | 585 | class CodeEditor(bpy.types.Operator): 586 | """Modal operator running Code Editor""" 587 | bl_idname = "code_editor.start" 588 | bl_label = "Code Editor" 589 | 590 | # minimap scrolling 591 | def scroll(self, context, event): 592 | if context.space_data.text: 593 | # dpi_ratio for ui scale 594 | dpi_r = context.user_preferences.system.dpi / 72.0 595 | mlh = round(dpi_r * self.minimap_line_height) # minimap line height 596 | # box center in px 597 | box_center = self.height - mlh * (context.space_data.top + context.space_data.visible_lines/2) 598 | self.to_box_center = box_center + self.slide - event.mouse_region_y 599 | # scroll will scroll 3 lines thus * 0.333 600 | nlines = 0.333 * self.to_box_center / mlh 601 | bpy.ops.text.scroll(lines=round(nlines)) 602 | return 603 | 604 | def modal(self, context, event): 605 | # function only if in area invoked in 606 | if (context.space_data and 607 | context.space_data.type == 'TEXT_EDITOR' and 608 | self.area == context.area and 609 | self.window == context.window): 610 | 611 | # does not work with word wrap 612 | context.space_data.show_line_numbers = True 613 | context.space_data.show_word_wrap = False 614 | context.space_data.show_syntax_highlight = True 615 | context.area.tag_redraw() 616 | 617 | # minimap scrolling and tab clicking 618 | if event.type == 'MOUSEMOVE': 619 | if ((0 < event.mouse_region_x < self.width) and 620 | (0 < event.mouse_region_y < self.height)): 621 | self.in_area = True 622 | else: 623 | self.in_area = False 624 | 625 | if ((0 < event.mouse_region_x < self.line_bar_width) and 626 | (0 < event.mouse_region_y < self.height)): 627 | self.in_line_bar = True 628 | else: 629 | self.in_line_bar = False 630 | 631 | if self.drag: 632 | self.scroll(context, event) 633 | if ((self.left_edge < event.mouse_region_x < self.right_edge) and 634 | (0 < event.mouse_region_y < self.height)): 635 | self.in_minimap = True 636 | else: 637 | self.in_minimap = False 638 | 639 | if ((self.left_edge - self.tab_width < event.mouse_region_x < self.left_edge) and 640 | (0 < event.mouse_region_y < self.height)): 641 | tab_id = int((self.height-event.mouse_region_y) / self.tab_height) 642 | if tab_id < len(bpy.data.texts): 643 | self.in_tab = bpy.data.texts[tab_id].name 644 | else: 645 | self.in_tab = None 646 | else: 647 | self.in_tab = None 648 | 649 | if self.in_minimap and self.opacity and event.type == 'LEFTMOUSE' and event.value == 'PRESS': 650 | self.drag = True 651 | self.scroll(context, event) 652 | return {'RUNNING_MODAL'} 653 | if self.in_line_bar and event.type == 'LEFTMOUSE' and event.value == 'PRESS': 654 | return {'RUNNING_MODAL'} 655 | if self.in_tab and self.opacity and event.type == 'LEFTMOUSE' and event.value == 'PRESS': 656 | context.space_data.text = bpy.data.texts[self.in_tab] 657 | return {'RUNNING_MODAL'} 658 | if self.opacity and event.type == 'LEFTMOUSE' and event.value == 'RELEASE': 659 | self.thread.restart(context.space_data.text) 660 | self.drag = False 661 | 662 | # typing characters - update minimap when whitespace 663 | if self.opacity and (event.unicode == ' ' or event.type in {'RET', 'NUMPAD_ENTER', 'TAB'}): 664 | self.thread.restart(context.space_data.text) 665 | 666 | # custom home handling 667 | if self.in_area and event.type == 'HOME' and event.value == 'PRESS' and not event.ctrl: 668 | if event.alt: 669 | bpy.ops.text.move(type='LINE_BEGIN') 670 | return {'RUNNING_MODAL'} 671 | line = context.space_data.text.current_line.body 672 | cursor_loc = context.space_data.text.select_end_character 673 | new_loc = custom_home(line, cursor_loc) 674 | for x in range(cursor_loc - new_loc): 675 | # this is awful but blender ops suck balls 676 | if event.shift: 677 | bpy.ops.text.move_select(type='PREVIOUS_CHARACTER') 678 | else: 679 | bpy.ops.text.move(type='PREVIOUS_CHARACTER') 680 | return {'RUNNING_MODAL'} 681 | 682 | # custom brackets etc. handling 683 | elif self.in_area and event.unicode in ['(', '"', "'", '[', '{'] and event.value == 'PRESS': 684 | select_loc = context.space_data.text.select_end_character 685 | cursor_loc = context.space_data.text.current_character 686 | line = context.space_data.text.current_line.body 687 | if cursor_loc == select_loc and cursor_loc < len(line) and line[cursor_loc] not in " \n\t\r)]},.+-*/": 688 | return {'PASS_THROUGH'} 689 | if event.unicode == '(': bpy.ops.text.insert(text='()') 690 | if event.unicode == '"': bpy.ops.text.insert(text='""') 691 | if event.unicode == "'": bpy.ops.text.insert(text="''") 692 | if event.unicode == "[": bpy.ops.text.insert(text="[]") 693 | if event.unicode == "{": bpy.ops.text.insert(text="{}") 694 | bpy.ops.text.move(type='PREVIOUS_CHARACTER') 695 | return {'RUNNING_MODAL'} 696 | 697 | # smart complete ALT-C 698 | elif self.in_area and event.unicode == 'c' and event.value == 'PRESS' and event.alt: 699 | if context.space_data.text.select_end_character == context.space_data.text.current_character: 700 | bpy.ops.text.select_word() 701 | bpy.ops.text.copy() 702 | clipboard = bpy.data.window_managers[0].clipboard 703 | context.window_manager.clipboard = smart_complete(clipboard) 704 | bpy.ops.text.paste() 705 | context.window_manager.clipboard = "" 706 | return {'RUNNING_MODAL'} 707 | 708 | # comment ALT-D 709 | elif self.in_area and event.unicode == 'd' and event.value == 'PRESS' and event.alt: 710 | if context.space_data.text.select_end_character == context.space_data.text.current_character: 711 | bpy.ops.text.select_word() 712 | bpy.ops.text.comment() 713 | return {'RUNNING_MODAL'} 714 | 715 | # end by button and code editor cleanup 716 | if str(context.area) not in context.window_manager.code_editors: 717 | del self.thread 718 | bpy.types.SpaceTextEditor.draw_handler_remove(self._handle, 'WINDOW') 719 | return {'FINISHED'} 720 | 721 | # end by F8 for reloading addons 722 | if event.type == 'F8': 723 | editors = context.window_manager.code_editors.split('&') 724 | editors.remove(str(context.area)) 725 | context.window_manager.code_editors = '&'.join(editors) 726 | del self.thread 727 | bpy.types.SpaceTextEditor.draw_handler_remove(self._handle, 'WINDOW') 728 | return {'FINISHED'} 729 | 730 | return {'PASS_THROUGH'} 731 | 732 | def invoke(self, context, event): 733 | # Version with one 'invoke scene' operator handling multiple text editor areas has the same performance 734 | # as one operator for eatch area - version 2 is implemented for simplicity 735 | if context.area.type != 'TEXT_EDITOR': 736 | self.report({'WARNING'}, "Text Editor not found, cannot run operator") 737 | return {'CANCELLED'} 738 | 739 | # init handlers 740 | args = (self, context) 741 | self._handle = bpy.types.SpaceTextEditor.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL') 742 | context.window_manager.modal_handler_add(self) 743 | 744 | # register operator in winman prop 745 | if not context.window_manager.code_editors: 746 | context.window_manager.code_editors = str(context.area) 747 | else: 748 | editors = context.window_manager.code_editors.split('&') 749 | editors.append(str(context.area)) 750 | context.window_manager.code_editors = '&'.join(editors) 751 | 752 | # user controllable in addon preferneces 753 | addon_prefs = context.user_preferences.addons[__name__].preferences 754 | self.bg_opacity = addon_prefs.opacity 755 | self.tabs = addon_prefs.show_tabs 756 | self.minimap_width = addon_prefs.minimap_width 757 | self.min_width = addon_prefs.window_min_width 758 | self.minimap_symbol_width = addon_prefs.symbol_width 759 | self.minimap_line_height = addon_prefs.line_height 760 | context.space_data.show_margin = addon_prefs.show_margin 761 | context.space_data.margin_column = addon_prefs.margin_column 762 | self.block_trans = 1-addon_prefs.block_trans 763 | self.indent_trans = 1-addon_prefs.indent_trans 764 | 765 | # init operator params 766 | self.area = context.area 767 | self.window = context.window 768 | self.in_area = True 769 | self.opacity = 1.0 770 | self.text_name = None 771 | self.width = next(region.width for region in context.area.regions if region.type=='WINDOW') 772 | self.height = next(region.height for region in context.area.regions if region.type=='WINDOW') 773 | dpi_r = context.user_preferences.system.dpi / 72.0 774 | self.left_edge = self.width - round(dpi_r*(self.width+5*self.minimap_width)/10.0) 775 | self.right_edge = self.width - round(dpi_r*15) 776 | self.in_minimap = False 777 | self.in_tab = None 778 | self.tab_width = 0 779 | self.tab_height = 0 780 | self.drag = False 781 | self.to_box_center = 0 782 | self.slide = 0 783 | self.line_bar_width = 0 784 | self.in_line_bar = False 785 | 786 | # get theme colors 787 | current_theme = bpy.context.user_preferences.themes.items()[0][0] 788 | tex_ed = bpy.context.user_preferences.themes[current_theme].text_editor 789 | self.background = tex_ed.space.back 790 | 791 | # get dpi, font size 792 | self.dpi = bpy.context.user_preferences.system.dpi 793 | 794 | #temp folder for autosave - TODO 795 | #self.temp = bpy.context.user_preferences.filepaths.temporary_directory 796 | 797 | # list to hold text info 798 | self.segments = [] 799 | self.segments.append({'elements': [], 'col': tex_ed.space.text}) 800 | self.segments.append({'elements': [], 'col': tex_ed.syntax_string}) 801 | self.segments.append({'elements': [], 'col': tex_ed.syntax_comment}) 802 | self.segments.append({'elements': [], 'col': tex_ed.syntax_numbers}) 803 | self.segments.append({'elements': [], 'col': tex_ed.syntax_builtin}) 804 | self.segments.append({'elements': [], 'col': tex_ed.syntax_preprocessor}) 805 | self.segments.append({'elements': [], 'col': tex_ed.syntax_special}) 806 | self.segments.append({'elements': [], 'col': (1,0,0)}) # Indentation marks 807 | 808 | # list to hold gui areas 809 | self.clickable = [] 810 | 811 | # threaded syntax highlighter 812 | self.thread = ThreadedSyntaxHighlighter(context.space_data.text, self.segments) 813 | self.thread.start() 814 | 815 | return {'RUNNING_MODAL'} 816 | 817 | class CodeEditorPrefs(bpy.types.AddonPreferences): 818 | """Code Editors Preferences Panel""" 819 | bl_idname = __name__ 820 | 821 | opacity = bpy.props.FloatProperty( 822 | name="Panel Background transparency", 823 | description="0 - fully opaque, 1 - fully transparent", 824 | min=0.0, 825 | max=1.0, 826 | default=0.2) 827 | 828 | show_tabs = bpy.props.BoolProperty( 829 | name="Show Tabs in Panel when multiple text blocks", 830 | description="Show opened textblock in tabs next to minimap", 831 | default=True) 832 | 833 | minimap_width = bpy.props.IntProperty( 834 | name="Minimap panel width", 835 | description="Minimap base width in px", 836 | min=25, 837 | max=400, 838 | default=100) 839 | 840 | window_min_width = bpy.props.IntProperty( 841 | name="Hide Panel when area width less than", 842 | description="Set 0 to deactivate side panel hiding, set huge to disable panel", 843 | min=0, 844 | max=4096, 845 | default=600) 846 | 847 | symbol_width = bpy.props.FloatProperty( 848 | name="Minimap character width", 849 | description="Minimap character width in px", 850 | min=1.0, 851 | max=10.0, 852 | default=1.0) 853 | 854 | line_height = bpy.props.IntProperty( 855 | name="Minimap line spacing", 856 | description="Minimap line spacign in px", 857 | min=2, 858 | max=10, 859 | default=2) 860 | 861 | block_trans = bpy.props.FloatProperty( 862 | name="Code block markings transparency", 863 | description="0 - fully opaque, 1 - fully transparent", 864 | min=0.0, 865 | max=1.0, 866 | default=0.6) 867 | 868 | indent_trans = bpy.props.FloatProperty( 869 | name="Indentation markings transparency", 870 | description="0 - fully opaque, 1 - fully transparent", 871 | min=0.0, 872 | max=1.0, 873 | default=0.9) 874 | 875 | show_margin = bpy.props.BoolProperty( 876 | name="Activate global Text Margin marker", 877 | default = True) 878 | 879 | margin_column = bpy.props.IntProperty( 880 | name="Margin Column", 881 | description="Column number to show marker at", 882 | min=0, 883 | max=1024, 884 | default=120) 885 | 886 | def draw(self, context): 887 | layout = self.layout 888 | row = layout.row() 889 | col = row.column(align=True) 890 | col.prop(self, "opacity") 891 | col.prop(self, "show_tabs", toggle=True) 892 | col.prop(self, "window_min_width") 893 | col = row.column(align=True) 894 | col.prop(self, "minimap_width") 895 | col.prop(self, "symbol_width") 896 | col.prop(self, "line_height") 897 | row = layout.row(align=True) 898 | row.prop(self, "show_margin", toggle=True) 899 | row.prop(self, "margin_column") 900 | row = layout.row(align=True) 901 | row.prop(self, "block_trans") 902 | row.prop(self, "indent_trans") 903 | 904 | class CodeEditorEnd(bpy.types.Operator): 905 | """Removes reference of Code Editors Area ending its modal operator""" 906 | bl_idname = "code_editor.end" 907 | bl_label = "" 908 | 909 | def execute(self, context): 910 | if str(context.area) in context.window_manager.code_editors: 911 | editors = context.window_manager.code_editors.split('&') 912 | editors.remove(str(context.area)) 913 | context.window_manager.code_editors = '&'.join(editors) 914 | return {'FINISHED'} 915 | 916 | # ===================================================== 917 | # REGISTER 918 | # ===================================================== 919 | 920 | def menu_entry(self, context): 921 | layout = self.layout 922 | layout.operator_context = 'INVOKE_DEFAULT' 923 | if str(context.area) in context.window_manager.code_editors: 924 | layout.operator("code_editor.end", text="Exit Code Editor", icon="GO_LEFT") 925 | else: 926 | layout.operator("code_editor.start", text="Start Code Editor", icon='FONTPREVIEW') 927 | 928 | def register(): 929 | bpy.utils.register_module(__name__) 930 | bpy.types.WindowManager.code_editors = bpy.props.StringProperty(default="") 931 | bpy.types.TEXT_MT_toolbox.prepend(menu_entry) 932 | 933 | def unregister(): 934 | bpy.utils.unregister_module(__name__) 935 | bpy.types.TEXT_MT_toolbox.remove(menu_entry) 936 | 937 | if __name__ == "__main__": 938 | register() --------------------------------------------------------------------------------