@@ -188,27 +188,21 @@ class App(ctk.CTk):
188188 def __init__ (self , title , geometry ):
189189 super ().__init__ ()
190190
191- self .appcmds = {
192- 'Save' : save_file ,
193- 'Open from file' : open_from_file ,
194- 'Save as' : save_as ,
195- 'New file' : newfile ,
196- 'Open Plugin Folder' : open_plugin_folder ,
197- 'Go To Line' : go_to_line ,
198- 'Toggle Theme' : toggle_theme ,
199- 'Open Plugin Finder' : lambda : PluginsInfo (self .plugins_list ),
200- 'Increment font size' : increment_font_size ,
201- 'Decrement font size' : decrement_font_size ,
202- 'Toggle word wrap' : toggle_word_wrap ,
203- 'Save configuration' : save_config ,
204- 'Go to start' : go_to_start ,
205- 'Go to end' : go_to_end
206- }
191+ self ._Textboxes = []
192+ self ._TabPaths = []
193+ self .tab_names = ['Untitled' ]
207194
208- self . textbox = ctk .CTkTextbox (self , undo = CONFIGURATION ['undo' ]['enabled' ],
195+ model_textbox = ctk .CTkTextbox (self , undo = CONFIGURATION ['undo' ]['enabled' ],
209196 autoseparators = CONFIGURATION ['undo' ]['separate_edits_from_undos' ],
210197 maxundo = CONFIGURATION ['undo' ]['max_undo' ])
211- self .textbox .pack (fill = 'both' , expand = True )
198+
199+ self .textbox = model_textbox
200+
201+ self ._Textboxes .append (model_textbox )
202+ self ._TabPaths .append (None )
203+
204+ self .tabs = ctk .CTkSegmentedButton (self , values = ['Untitled' ])
205+ self .tabs .pack (fill = 'x' , side = ctk .TOP )
212206
213207 self .after (1 , lambda : threading .Thread (target = AutoUpdate , args = (self ,), daemon = True ).start ())
214208
@@ -246,42 +240,105 @@ def __load_plugins():
246240 editor_api = PluginAPI (self .textbox , self )
247241 self .plugins_list = PLUGINS_LIST
248242
249- self .path = None
243+ self ._TabPaths = [ None ]
250244 self .font_size = 14
251245 self .modified = False
246+ index = self .tabs .index (self .tab_names [0 ])
252247
253248 def write_to_recent_files ():
254249 if CONFIGURATION ['recent_files' ]['enabled' ]:
255250 if len (CONFIGURATION ['recent_files' ]['recent_file_paths' ]) >= CONFIGURATION ['recent_files' ]['keep_recent_files_count' ]:
256251 del CONFIGURATION ['recent_files' ]['recent_file_paths' ][0 ]
257- CONFIGURATION ['recent_files' ]['recent_file_paths' ].append (self .path )
252+ CONFIGURATION ['recent_files' ]['recent_file_paths' ].append (self ._TabPaths [ index ] )
258253 else :
259- CONFIGURATION ['recent_files' ]['recent_file_paths' ].append (self .path )
254+ CONFIGURATION ['recent_files' ]['recent_file_paths' ].append (self ._TabPaths [index ])
255+
256+ def newtab (event = None , file_path = None ):
257+ new_textbox = ctk .CTkTextbox (
258+ self ,
259+ undo = CONFIGURATION ['undo' ]['enabled' ],
260+ autoseparators = CONFIGURATION ['undo' ]['separate_edits_from_undos' ],
261+ maxundo = CONFIGURATION ['undo' ]['max_undo' ]
262+ )
263+ for tb in self ._Textboxes :
264+ tb .pack_forget ()
265+ self ._Textboxes .append (new_textbox )
266+ self ._TabPaths .append (file_path or None )
267+ if file_path and os .path .exists (file_path ):
268+ with open (file_path , 'r' , encoding = 'utf-8' ) as f :
269+ new_textbox .insert ('1.0' , f .read ())
270+ tab_name = file_path or f'Untitled { len (self ._Textboxes )} '
271+ self .tab_names .append (tab_name )
272+ self .tabs .configure (values = self .tab_names )
273+ self .tabs .set (tab_name )
274+ new_textbox .pack (fill = 'both' , expand = True )
275+ self .textbox = new_textbox
276+
277+ def on_tab_selected (value ):
278+ nonlocal index
279+ for tb in self ._Textboxes :
280+ tb .pack_forget ()
281+ index = self .tab_names .index (value )
282+ self ._Textboxes [index ].pack (fill = 'both' , expand = True )
283+ self ._TabPaths [index ] = self ._TabPaths [index ]
284+ self .textbox = self ._Textboxes [index ]
285+
286+ def delete_tab (index ):
287+ tb = self ._Textboxes .pop (index )
288+ tb .pack_forget ()
289+ self ._TabPaths .pop (index )
290+ self .tab_names .pop (index )
291+ new_index = max (0 , index - 1 )
292+ if not self ._Textboxes :
293+ new_textbox = ctk .CTkTextbox (self ,
294+ undo = CONFIGURATION ['undo' ]['enabled' ],
295+ autoseparators = CONFIGURATION ['undo' ]['separate_edits_from_undos' ],
296+ maxundo = CONFIGURATION ['undo' ]['max_undo' ])
297+ self ._Textboxes .append (new_textbox )
298+ self ._TabPaths .append (None )
299+ self .tab_names .append ("Untitled" )
300+ new_textbox .pack (fill = 'both' , expand = True )
301+ self .tabs .configure (values = self .tab_names )
302+ self .tabs .set ("Untitled" )
303+ self .textbox = new_textbox
304+ self .textbox .focus ()
305+ return
306+ self .tabs .set (self .tab_names [new_index ])
307+ on_tab_selected (self .tab_names [new_index ])
308+ self .tabs .configure (values = self .tab_names )
309+
310+ def __get_values_and_delete (event = None ):
311+ value = self .tabs .get ()
312+ index = self .tab_names .index (value )
313+ delete_tab (index )
314+
315+
316+ self .tabs .configure (command = on_tab_selected )
260317
261318
262319 def newfile (event = None ):
263320 if not '*' in self .title ()[7 ]:
264321 self .title ('kPad - Untitled' )
265- self .path = None
322+ self ._TabPaths [ index ] = None
266323 self .textbox .delete ('1.0' , 'end' )
267324 else :
268325 ask = askyesno ('File unsaved!' , 'Do you want to save your file before making a new one?' )
269326 if ask :
270327 save_file ()
271328 else :
272329 self .title ('kPad - Untitled' )
273- self .path = None
330+ self ._TabPaths [ index ] = None
274331 self .textbox .delete ('1.0' , 'end' )
275332
276333
277334 def save_as (event = None ):
278- self .path = asksaveasfilename (filetypes = [('Text files' , '.txt' ), ('Other' , '.*.*' )], defaultextension = '.txt' )
279- if not self .path :
335+ self ._TabPaths [ index ] = asksaveasfilename (filetypes = [('Text files' , '.txt' ), ('Other' , '.*.*' )], defaultextension = '.txt' )
336+ if not self ._TabPaths [ index ] :
280337 return
281338 try :
282- with open (self .path , 'w' , encoding = 'utf-8' ) as file :
339+ with open (self ._TabPaths [ index ] , 'w' , encoding = 'utf-8' ) as file :
283340 file .write (self .textbox .get ('1.0' , 'end-1c' ))
284- self .title (f'kPad - { self .path } ' )
341+ self .title (f'kPad - { self ._TabPaths [ index ] } ' )
285342 self .modified = False
286343 if self .title ().endswith ('*' ):
287344 self .title (self .title ()[:- 1 ])
@@ -298,10 +355,10 @@ def open_plugin_folder():
298355 subprocess .call (['xdg-open' , plugin_dir ])
299356
300357 def save_file (event = None ):
301- if not self .path :
358+ if not self ._TabPaths [ index ] :
302359 save_as ()
303360 return
304- with open (self .path , 'w' , encoding = 'utf-8' ) as f :
361+ with open (self ._TabPaths [ index ] , 'w' , encoding = 'utf-8' ) as f :
305362 f .write (self .textbox .get ('1.0' , 'end-1c' ))
306363 self .modified = False
307364 if self .title ().endswith ('*' ):
@@ -310,10 +367,10 @@ def save_file(event=None):
310367
311368 def open_from_file (event = None , path = None ):
312369 if not path :
313- self .path = askopenfilename (filetypes = [('Text files' , '.txt' ), ('All files' , '*.*' )], defaultextension = '.txt' )
314- if self .path :
370+ self ._TabPaths [ index ] = askopenfilename (filetypes = [('Text files' , '.txt' ), ('All files' , '*.*' )], defaultextension = '.txt' )
371+ if self ._TabPaths [ index ] :
315372 self .textbox .delete ('1.0' , 'end' )
316- with open (self .path , 'r' ) as file :
373+ with open (self ._TabPaths [ index ] , 'r' ) as file :
317374 self .textbox .insert ('1.0' , file .read ())
318375 write_to_recent_files ()
319376 print (CONFIGURATION ['recent_files' ]['recent_file_paths' ])
@@ -328,7 +385,13 @@ def set_font(font):
328385 def autosave ():
329386 while True :
330387 if CONFIGURATION ['auto_save' ]['enabled' ]:
331- if self .path and 'Untitled' not in self .title ():
388+ current_tab_name = self .tabs .get ()
389+ try :
390+ current_index = self .tab_names .index (current_tab_name )
391+ except ValueError :
392+ time .sleep (CONFIGURATION ['auto_save' ]['time_until_next_save' ])
393+ continue
394+ if self ._TabPaths [current_index ] and 'Untitled' not in self .title ():
332395 save_file ()
333396 time .sleep (CONFIGURATION ['auto_save' ]['time_until_next_save' ])
334397
@@ -426,10 +489,6 @@ def handle_brackets(event):
426489 menu .add_cascade (label = 'View' , menu = view_menu )
427490 plugins_menu = Menu (menu , tearoff = 0 )
428491 menu .add_cascade (label = 'Plugins' , menu = plugins_menu )
429- for plugin in self .plugins_list :
430- meta = plugin ['meta' ]
431- name = meta .get ('name' ) if meta else plugin ['module' ].__name__
432- plugins_menu .add_command (label = name , command = lambda p = plugin : p ['module' ].action (editor_api ))
433492 plugins_menu .add_separator ()
434493 plugins_menu .add_command (label = 'Show Plugin Information...' , command = lambda : PluginsInfo (self .plugins_list ))
435494
@@ -466,10 +525,11 @@ def update_cursor_info(event=None):
466525 self .bind (f'<{ mod } -s>' , save_file )
467526 self .bind (f'<{ mod } -o>' , open_from_file )
468527 self .bind (f'<{ mod } -t>' , toggle_theme )
469- self .bind (f'<{ mod } -n>' , newfile )
528+ self .bind (f'<{ mod } -n>' , lambda event = None : newtab () )
470529 self .bind (f'<{ mod } -equal>' , increment_font_size )
471530 self .bind (f'<{ mod } -minus>' , decrement_font_size )
472531 self .bind (f'<{ mod } -k>' , lambda event = None : FastCommand (self ))
532+ self .bind (f'<{ mod } -e>' , __get_values_and_delete )
473533
474534 self .textbox .bind ('<Key>' , handle_brackets )
475535
@@ -494,7 +554,7 @@ def _on_quit_():
494554 if self .modified :
495555 result = askyesnocancel ('Quit' , 'Save changes before quitting?' )
496556 if result is True :
497- if self .path :
557+ if self ._TabPaths [ index ] :
498558 save_file ()
499559 else :
500560 save_as ()
@@ -503,6 +563,27 @@ def _on_quit_():
503563 save_config ()
504564 self .destroy ()
505565
566+ self .textbox .pack (fill = 'both' , expand = True )
567+ self .protocol ('WM_DELETE_WINDOW' , _on_quit_ )
568+
569+ # Define appcmds BEFORE plugin auto-start so plugins can use it
570+ self .appcmds = {
571+ 'Save' : save_file ,
572+ 'Open from file' : open_from_file ,
573+ 'Save as' : save_as ,
574+ 'New file' : newfile ,
575+ 'Open Plugin Folder' : open_plugin_folder ,
576+ 'Go To Line' : go_to_line ,
577+ 'Toggle Theme' : toggle_theme ,
578+ 'Open Plugin Finder' : lambda : PluginsInfo (self .plugins_list ),
579+ 'Increment font size' : increment_font_size ,
580+ 'Decrement font size' : decrement_font_size ,
581+ 'Toggle word wrap' : toggle_word_wrap ,
582+ 'Save configuration' : save_config ,
583+ 'Go to start' : go_to_start ,
584+ 'Go to end' : go_to_end
585+ }
586+
506587 for plugin in self .plugins_list :
507588 name = plugin .get ('meta' , {}).get ('name' , plugin ['module' ].__name__ )
508589 if name in CONFIGURATION .get ('auto_start_plugins' , []):
@@ -511,9 +592,6 @@ def _on_quit_():
511592 except Exception as e :
512593 print (f"[PLUGIN ERROR] Auto-start failed for { name } : { e } " )
513594
514- self .textbox .pack (fill = 'both' , expand = True )
515- self .protocol ('WM_DELETE_WINDOW' , _on_quit_ )
516-
517595 self .save_file = save_file
518596 self .open_from_file = open_from_file
519597 self .save_as = save_as
@@ -528,6 +606,12 @@ def _on_quit_():
528606 self .gostart = go_to_start
529607 self .goend = go_to_end
530608
609+
610+ for plugin in self .plugins_list :
611+ meta = plugin ['meta' ]
612+ name = meta .get ('name' ) if meta else plugin ['module' ].__name__
613+ plugins_menu .add_command (label = name , command = lambda p = plugin : p ['module' ].action (editor_api ))
614+
531615class PluginsInfo (ctk .CTkToplevel ):
532616 def __init__ (self , plugins_list ):
533617 super ().__init__ ()
@@ -681,7 +765,7 @@ def __init__(self, parent):
681765 )
682766 results_box .pack (fill = 'both' , side = ctk .BOTTOM )
683767
684- COMMANDS = app .appcmds
768+ COMMANDS = parent .appcmds
685769
686770 def exit_window (event = None ):
687771 parent .textbox .focus ()
@@ -697,17 +781,17 @@ def run_command(event=None):
697781
698782 def filter_ (event = None ):
699783 query = command_entry .get ().lower ()
700- if not query == '' or query .split (' ' ) == '' :
701- results_box .delete (0 , "end" )
784+ results_box .delete (0 , "end" )
785+ COMMANDS = parent .appcmds
786+ if query .strip () != "" :
702787 for name in COMMANDS :
703788 if query in name .lower ():
704789 results_box .insert ("end" , name )
705- if results_box .size () > 0 :
706- results_box .selection_set (0 )
707790 else :
708- results_box .delete (0 , 'end' )
709- for command in COMMANDS :
710- results_box .insert ('end' , command )
791+ for name in COMMANDS :
792+ results_box .insert ("end" , name )
793+ if results_box .size () > 0 :
794+ results_box .selection_set (0 )
711795
712796 def run_filtering (event = None ):
713797 threading .Thread (target = filter_ , daemon = True ).start ()
0 commit comments