@@ -305,6 +305,7 @@ def init(self):
305305 self .liststore = None
306306 self .text_length = 0
307307 self .chart_data = None
308+ self .converting_tabs = False
308309 self .gui .WIDGET = self .build_gui ()
309310 self .gui .get_container_widget ().remove (self .gui .textview )
310311 self .gui .get_container_widget ().add (self .gui .WIDGET )
@@ -397,6 +398,8 @@ def build_gui(self):
397398 )
398399 self .ebuf = UndoableBuffer ()
399400 self .editor_textview .set_buffer (self .ebuf )
401+ self .ebuf .connect_after ("insert-text" , self .on_insert_text_after )
402+ self .ebuf .connect ("modified-changed" , self .on_modified_changed )
400403 self .keyword_tag = self .ebuf .create_tag (
401404 "keyword" , foreground = "blue" , weight = 700
402405 )
@@ -500,8 +503,13 @@ def build_gui(self):
500503
501504 def update_filename_label (self ):
502505 name = os .path .basename (self .last_filename ) if self .last_filename else _ ("Untitled" )
506+ if self .ebuf .get_modified ():
507+ name = "*" + name
503508 self .filename_label .set_text (name )
504509
510+ def on_modified_changed (self , buffer ):
511+ self .update_filename_label ()
512+
505513 def check_unsaved_changes (self , proceed ):
506514 """
507515 If the script has unsaved changes, ask the user whether to save,
@@ -692,6 +700,21 @@ def on_buffer_changed(self, buffer):
692700 self .highlight_syntax ()
693701 self .completion .on_buffer_changed ()
694702
703+ def on_insert_text_after (self , buffer , text_iter , text , length ):
704+ if "\t " not in text or self .converting_tabs :
705+ return
706+ self .converting_tabs = True
707+ try :
708+ end_offset = text_iter .get_offset ()
709+ start_offset = end_offset - len (text )
710+ start = buffer .get_iter_at_offset (start_offset )
711+ end = buffer .get_iter_at_offset (end_offset )
712+ new_text = text .replace ("\t " , " " )
713+ buffer .delete (start , end )
714+ buffer .insert (buffer .get_iter_at_offset (start_offset ), new_text )
715+ finally :
716+ self .converting_tabs = False
717+
695718 def highlight_syntax (self ):
696719 start_iter = self .ebuf .get_start_iter ()
697720 end_iter = self .ebuf .get_end_iter ()
@@ -932,38 +955,147 @@ def on_editor_focus_out(self, widget, event):
932955 return False
933956
934957 def on_key_press (self , textview , event ):
958+ keyval = event .keyval
959+ shift_tab = keyval == Gdk .KEY_ISO_Left_Tab or (
960+ keyval == Gdk .KEY_Tab and (event .state & Gdk .ModifierType .SHIFT_MASK )
961+ )
962+
963+ if shift_tab :
964+ self .dedent_selection ()
965+ return True
966+
967+ if keyval == Gdk .KEY_Tab and self .ebuf .get_has_selection ():
968+ self .indent_selection ()
969+ return True
970+
935971 if self .completion .on_key_press (event ):
936972 return True
937973
938- if event . keyval == Gdk .KEY_Tab :
974+ if keyval == Gdk .KEY_Tab :
939975 # buffer = textview.get_buffer()
940976 iter_ = self .ebuf .get_iter_at_mark (self .ebuf .get_insert ())
941977 self .ebuf .insert (iter_ , " " ) # Insert 4 spaces
942978 return True
943979
944- elif event .keyval == Gdk .KEY_Return and (
945- event .state & Gdk .ModifierType .MOD1_MASK
946- ):
980+ elif keyval == Gdk .KEY_Return and (event .state & Gdk .ModifierType .MOD1_MASK ):
947981 self .apply_button .emit ("clicked" )
948982 return True
949983
950- elif event .keyval == Gdk .KEY_c and (event .state & Gdk .ModifierType .MOD1_MASK ):
984+ elif keyval in (Gdk .KEY_Return , Gdk .KEY_KP_Enter ):
985+ self .insert_auto_indent_newline ()
986+ return True
987+
988+ elif keyval == Gdk .KEY_c and (event .state & Gdk .ModifierType .MOD1_MASK ):
951989 self .copy_selected_text ()
952990 return True
953991
954- elif (Gdk .keyval_name (event . keyval ) == "Z" ) and match_primary_mask (
992+ elif (Gdk .keyval_name (keyval ) == "Z" ) and match_primary_mask (
955993 event .get_state (), Gdk .ModifierType .SHIFT_MASK
956994 ):
957995 self .redo ()
958996 return True
959- elif (Gdk .keyval_name (event . keyval ) == "z" ) and match_primary_mask (
997+ elif (Gdk .keyval_name (keyval ) == "z" ) and match_primary_mask (
960998 event .get_state ()
961999 ):
9621000 self .undo ()
9631001 return True
9641002
1003+ elif keyval == Gdk .KEY_slash and match_primary_mask (event .get_state ()):
1004+ self .toggle_comment_selection ()
1005+ return True
1006+
9651007 return False
9661008
1009+ def compute_indent_for_new_line (self , text_before_cursor ):
1010+ stripped = text_before_cursor .rstrip ()
1011+ indent = re .match (r"[ \t]*" , text_before_cursor ).group (0 ).replace ("\t " , " " )
1012+ if stripped .endswith (":" ):
1013+ indent += " "
1014+ elif re .match (r"^[ \t]*(return|pass|break|continue|raise)\b" , stripped ):
1015+ if indent .endswith (" " ):
1016+ indent = indent [:- 4 ]
1017+ return indent
1018+
1019+ def insert_auto_indent_newline (self ):
1020+ buf = self .ebuf
1021+ it = buf .get_iter_at_mark (buf .get_insert ())
1022+ line_start = it .copy ()
1023+ line_start .set_line_offset (0 )
1024+ text_before_cursor = buf .get_text (line_start , it , True )
1025+ indent = self .compute_indent_for_new_line (text_before_cursor )
1026+ buf .insert_at_cursor ("\n " + indent )
1027+
1028+ def selection_line_bounds (self ):
1029+ buf = self .ebuf
1030+ if buf .get_has_selection ():
1031+ sel_start , sel_end = buf .get_selection_bounds ()
1032+ else :
1033+ it = buf .get_iter_at_mark (buf .get_insert ())
1034+ sel_start = sel_end = it
1035+ start = buf .get_iter_at_line (sel_start .get_line ())
1036+ end_line = sel_end .get_line ()
1037+ if end_line > sel_start .get_line () and sel_end .get_line_offset () == 0 :
1038+ # A drag-selection ending at column 0 of a line usually means
1039+ # the user didn't mean to touch that line.
1040+ end_line -= 1
1041+ end = buf .get_iter_at_line (end_line )
1042+ end .forward_to_line_end ()
1043+ return start , end
1044+
1045+ def reindent_selection (self , transform ):
1046+ buf = self .ebuf
1047+ start , end = self .selection_line_bounds ()
1048+ start_offset = start .get_offset ()
1049+ text = buf .get_text (start , end , True )
1050+ new_text = "\n " .join (transform (line ) for line in text .split ("\n " ))
1051+ if new_text == text :
1052+ return
1053+ buf .delete (start , end )
1054+ buf .insert (buf .get_iter_at_offset (start_offset ), new_text )
1055+ new_start = buf .get_iter_at_offset (start_offset )
1056+ new_end = buf .get_iter_at_offset (start_offset + len (new_text ))
1057+ buf .select_range (new_start , new_end )
1058+
1059+ def indent_selection (self ):
1060+ self .reindent_selection (lambda line : " " + line )
1061+
1062+ def dedent_selection (self ):
1063+ def dedent (line ):
1064+ if line .startswith (" " ):
1065+ return line [4 :]
1066+ if line .startswith ("\t " ):
1067+ return line [1 :]
1068+ return line .lstrip (" " )
1069+
1070+ self .reindent_selection (dedent )
1071+
1072+ def toggle_comment_selection (self ):
1073+ buf = self .ebuf
1074+ start , end = self .selection_line_bounds ()
1075+ text = buf .get_text (start , end , True )
1076+ code_lines = [line for line in text .split ("\n " ) if line .strip ()]
1077+ all_commented = bool (code_lines ) and all (
1078+ line .lstrip ().startswith ("#" ) for line in code_lines
1079+ )
1080+
1081+ def comment (line ):
1082+ if not line .strip ():
1083+ return line
1084+ stripped = line .lstrip (" " )
1085+ indent = line [: len (line ) - len (stripped )]
1086+ return indent + "# " + stripped
1087+
1088+ def uncomment (line ):
1089+ stripped = line .lstrip (" " )
1090+ indent = line [: len (line ) - len (stripped )]
1091+ if stripped .startswith ("# " ):
1092+ return indent + stripped [2 :]
1093+ if stripped .startswith ("#" ):
1094+ return indent + stripped [1 :]
1095+ return line
1096+
1097+ self .reindent_selection (uncomment if all_commented else comment )
1098+
9671099 def undo (self ):
9681100 self .ebuf .undo ()
9691101 self .text_length = len (self .get_text ())
@@ -1120,25 +1252,30 @@ def on_draw(self, widget, cr):
11201252 min_val = min (data )
11211253 if max_val == min_val :
11221254 return
1123- interval = (max_val - min_val ) / self .chart_data [2 ]
1124- buckets = [0 ] * (int (max_val / interval ) + 1 )
1255+ num_buckets = max (1 , int (self .chart_data [2 ]))
1256+ interval = (max_val - min_val ) / num_buckets
1257+ buckets = [0 ] * num_buckets
11251258 for value in data :
1126- if value > max_val :
1127- buckets [int (max_val / interval )] += 1
1128- else :
1129- buckets [int (value / interval )] += 1
1259+ # Bucket index is the value's offset from min_val, not
1260+ # the raw value -- otherwise negative or non-zero-based
1261+ # data lands outside the buckets list. Clamp the top
1262+ # edge (value == max_val) into the last bucket rather
1263+ # than one past it.
1264+ idx = int ((value - min_val ) / interval )
1265+ if idx >= num_buckets :
1266+ idx = num_buckets - 1
1267+ buckets [idx ] += 1
11301268
11311269 labels = []
11321270 decimal_places = self .chart_data [3 ].get ("decimal_places" , 0 )
11331271 format = "%0." + str (decimal_places ) + "f"
1134- for i in range (int ( max_val / interval ) ):
1135- begin = format % (i * interval )
1136- end = format % ((i + 1 ) * interval )
1272+ for i in range (num_buckets ):
1273+ begin = format % (min_val + i * interval )
1274+ end = format % (min_val + (i + 1 ) * interval )
11371275 if begin != end :
11381276 labels .append (begin + "-" + end )
11391277 else :
11401278 labels .append (begin )
1141- labels .append (format % ((i + 1 ) * interval ,))
11421279
11431280 # Draw a bar chart with values
11441281 bar_width = width / (len (buckets ) * 1.5 )
0 commit comments