Auto sort tabs (real-time)
-
that, of course, could be a solution but to be honest,
it would drive me nuts to see this blinking window all the time.@Maxitrol-Mat
are you interested in such a solution? -
@Ekopalypse said in Auto sort tabs (real-time):
it would drive me nuts to see this blinking window all the time.
Me too. But would it really need to be “all the time”? The only time you would need to re-sort tabs would be when a new tab is added (if you close/remove a tab, the rest are still sorted properly), so wouldn’t you just hook onto the file-opened notification (which I believe triggers for both opening an existing file and for creating a new file tab)?
But yes, no need to do further work if @Maxitrol-Mat isn’t willing to use PythonScript for the solution. ;-)
-
@Maxitrol-Mat and all
Instead of using the built-in dialog, tab sorting can be done very easily using the Move Tab Forward / Backward commands from the
View -> Tab
menu.In the following I provide a solution in NppExec plugin script language. It uses a slightly optimized version of the Bubble Sort algorithm and sorts the tabs case-insensitive per view. The active tabs of the views are restored after sorting.
::SortTabs npp_console keep // Retrieve values of some constants set local $(PrimaryViewId) ~ MAIN_VIEW set local $(SecondViewId) ~ SUB_VIEW set local $(PrimaryView) ~ PRIMARY_VIEW set local $(SecondView) ~ SECOND_VIEW // Get id of active buffer's view npp_sendmsg NPPM_GETCURRENTVIEW set local $(CurViewId) = $(MSG_RESULT) // Init variables for view switching if $(CurViewId) == $(PrimaryViewId) then set local $(ActiveView) = $(PrimaryView) set local $(ActiveViewId) = $(PrimaryViewId) set local $(InactiveView) = $(SecondView) set local $(InactiveViewId) = $(SecondViewId) else if $(CurViewId) == $(SecondViewId) then set local $(ActiveView) = $(SecondView) set local $(ActiveViewId) = $(SecondViewId) set local $(InactiveView) = $(PrimaryView) set local $(InactiveViewId) = $(PrimaryViewId) else exit endif // Remember buffer id of active tab in active view npp_sendmsg NPPM_GETCURRENTDOCINDEX 0 $(ActiveViewId) npp_sendmsg NPPM_GETBUFFERIDFROMPOS $(MSG_RESULT) $(ActiveViewId) set local $(ActiveViewCurBufferId) = $(MSG_RESULT) // Remember buffer id of active tab in inactive view if this view is visible npp_sendmsg NPPM_GETCURRENTDOCINDEX 0 $(InactiveViewId) if $(MSG_RESULT) == -1 then set local $(ProcessInactiveView) = 0 else npp_sendmsg NPPM_GETBUFFERIDFROMPOS $(MSG_RESULT) $(InactiveViewId) set local $(InactiveViewCurBufferId) = $(MSG_RESULT) set local $(ProcessInactiveView) = 1 endif // Start tab sorting in active view set local $(CurView) = $(ActiveView) :IterateAllViews // Retrieve number of open tabs in current view npp_sendmsg NPPM_GETNBOPENFILES 0 $(CurView) set local $(MaxTabIdx) ~ $(MSG_RESULT) - 1 set local $(MinTabIdx) = 0 // Sort tabs using optimized version of Bubble Sort // and "Move Tab xxx" commands from menu "View -> Tab" :DoLoopStart set local $(Swapped) = 0 set local $(CurTabIdx) = $(MaxTabIdx) npp_sendmsg NPPM_ACTIVATEDOC $(CurViewId) $(CurTabIdx) set local $(LeftFileName) ~ strupper $(FILE_NAME) :ForLoopStart if $(CurTabIdx) <= $(MinTabIdx) goto :ForLoopEnd set local $(RightFileName) = $(LeftFileName) set local $(NextTabIdx) ~ $(CurTabIdx) - 1 npp_sendmsg NPPM_ACTIVATEDOC $(CurViewId) $(NextTabIdx) set local $(LeftFileName) ~ strupper $(FILE_NAME) if "$(LeftFileName)" > "$(RightFileName)" then npp_sendmsg WM_COMMAND IDM_VIEW_TAB_MOVEFORWARD set local $(LeftFileName) = $(RightFileName) set local $(Swapped) = 1 endif set local $(CurTabIdx) ~ $(CurTabIdx) - 1 goto :ForLoopStart :ForLoopEnd set local $(MinTabIdx) ~ $(MinTabIdx) + 1 if $(Swapped) == 1 goto :DoLoopStart // As long as the inactive view is visible ... if $(ProcessInactiveView) == 1 then // ... and we didn't process all views ... if $(CurView) != $(InactiveView) then // ... advance to next view and repeat tab sorting set local $(CurView) = $(InactiveView) set local $(CurViewId) = $(InactiveViewId) goto :IterateAllViews endif endif // If inactive view is visible restore its active tab if $(ProcessInactiveView) == 1 then npp_sendmsg NPPM_GETPOSFROMBUFFERID $(InactiveViewCurBufferId) $(InactiveViewId) set local $(InactiveViewActiveTabIdx) ~ $(MSG_RESULT) & 0x3FFFFFFF npp_sendmsg NPPM_ACTIVATEDOC $(InactiveViewId) $(InactiveViewActiveTabIdx) endif // Restore active tab of active view npp_sendmsg NPPM_GETPOSFROMBUFFERID $(ActiveViewCurBufferId) $(ActiveViewId) set local $(ActiveViewActiveTabIdx) ~ $(MSG_RESULT) & 0x3FFFFFFF npp_sendmsg NPPM_ACTIVATEDOC $(ActiveViewId) $(ActiveViewActiveTabIdx)
I have to admit that it produces lots of screen flickering (depending on the number of open tabs), but as tab sorting normally is not a frequently executed task it should be OK.
-
@dinkumoil said in Auto sort tabs (real-time):
f screen flickering (depending on the number of open tabs), but as tab sorting normally is not a frequently executed task it should be
I don’t mind flickering if it will auto-sort each time I add a new file.
If the author happened to see a comment like that (he won’t BTW), it would totally seal the coffin from such a feature ever happening.
That would make such an author an idiot. Have you done any business, ever?
-
Have you done any business, ever?
Notepad++ is not a business, it’s a completely free product. Thus, business rules don’t fit for Npp project.
I don’t mind flickering if it will auto-sort each time I add a new file.
My script will not auto-sort. The script has to be assigned to a keyboard shortcut. Another way would be to install the NppEventExec plugin as well. This plugin is intended to work together with NppExec and can call NppExec scripts when an event is fired.
That would make such an author an idiot.
Because you were insulting, go figure out the solution which is perfect for you by yourself.
-
Same poster, same topic, DIFFERENT thread.
-
So not that I find this idea useful, but I needed some mental exercise while waiting for something to happen today, so I gave it a go with scripting, specifically Pythonscripting.
The result is below; it is a bit of a visual treat when a new file is opened into a view with already a lot of tabs. It is probably too annoying to really be practical, but…
I call the script
SortFileTabsByFilename.py
and here we go:# -*- coding: utf-8 -*- from Npp import editor, notepad, NOTIFICATION import os class SFTBF(object): def __init__(self): self.installed = False def install(self): if not self.installed: notepad.callback(self.file_opened_callback, [NOTIFICATION.FILEOPENED]) self.installed = True def uninstall(self): if self.installed: notepad.clearCallbacks(self.file_opened_callback) self.installed = False def is_installed(self): return self.installed def file_opened_callback(self, args): current_view = notepad.getCurrentView() other_view = 1 if current_view == 0 else 0 if notepad.getCurrentDocIndex(other_view) == 4294967295L: other_view = None curr_view_paths_list = []; curr_view_sorted_paths_list = [] other_view_paths_list = []; other_view_sorted_paths_list = [] for (filename, _, index_in_view, view) in notepad.getFiles(): if view == current_view: curr_view_paths_list.append(filename) curr_view_sorted_paths_list.append(filename) else: other_view_paths_list.append(filename) other_view_sorted_paths_list.append(filename) curr_view_sorted_paths_list.sort(key=lambda x: x.rsplit(os.sep, 1)[-1].upper()) curr_view_already_sorted = True if curr_view_paths_list == curr_view_sorted_paths_list else False other_view_sorted_paths_list.sort(key=lambda x: x.rsplit(os.sep, 1)[-1].upper()) other_view_already_sorted = True if other_view_paths_list == other_view_sorted_paths_list else False if curr_view_already_sorted and other_view_already_sorted: return # nothing to do processed_other_view = False if other_view != None and not other_view_already_sorted: self.rearrange_tabs_in_view(other_view, other_view_sorted_paths_list) processed_other_view = True processed_current_view = False if not curr_view_already_sorted: self.rearrange_tabs_in_view(current_view, curr_view_sorted_paths_list) processed_current_view = True if processed_other_view and not processed_current_view: # leave the view we started in as the active one: notepad.activateIndex(current_view, notepad.getCurrentDocIndex(current_view)) def rearrange_tabs_in_view(self, view, sorted_name_list): notepad.activateIndex(view, notepad.getCurrentDocIndex(view)) # get switched into the correct view remembered_active_filename = notepad.getCurrentFilename() destination_index = 0 num_of_tabs = len(sorted_name_list) while destination_index < num_of_tabs: current_order_list = [] for (filename, _, index_in_view, v) in notepad.getFiles(): if v == view: current_order_list.append(filename) curr_location_index = current_order_list.index(sorted_name_list[destination_index]) move_left_count = curr_location_index - destination_index if move_left_count > 0: notepad.activateFile(current_order_list[curr_location_index]) for _ in range(move_left_count): notepad.menuCommand(MENUCOMMAND.VIEW_TAB_MOVEBACKWARD) destination_index += 1 notepad.activateFile(remembered_active_filename) if __name__ == '__main__': if 'sftbf' not in globals(): sftbf = SFTBF() # each running of the script toggles install/uninstall: sftbf.uninstall() if sftbf.is_installed() else sftbf.install() notepad.messageBox('SortFileTabsByFilename {}INSTALLED!'.format('' if sftbf.is_installed() else 'UN'), '')
-
Nice.
However, when I previously said,
which I believe triggers for both opening an existing file and for creating a new file tab
… I was apparently wrong. Because File > New doesn’t trigger the script’s sorting function for me, whereas Open does. I don’t see a message that does seem to be triggered for New, either. Hmm…
-
I assume because, technically, new, does not open a file.
-
Yes, there are limitations.
I should have mentioned.So, another one is if you “clone” or “move” a file from one view to another.
It won’t be triggered then either.The ideal way would be to tie it into the “buffer activated” callback.
But, hmm, there’s a problem there: As it switches tabs to move things into proper order (during a firing of “buffer activated”), it will cause more "buffer activated"s to happen.
I didn’t do a lot of testing that way, but when I did give it a go, I got some nice N++'s hangs.
I tried turning off the callback at the start of itself, and restoring it right before it returned; that was not liked either (hangs as well).But, like I said, this was just a diversion for today, not something for me to seriously use. If anyone really wants to use it, perhaps they also work on it and figure out how to avoid its deficiencies.
-
@PeterJones said in Auto sort tabs (real-time):
Hmm…
As a workaround, it could be done at FILESAVED, because that would be triggered when the new file is first given a name.
That would require hanging line #13 to
notepad.callback(self.file_opened_callback, [NOTIFICATION.FILEOPENED,NOTIFICATION.FILESAVED])
-
Perhaps you have some expertise regarding this…
I noticed that “uninstalling” my script doesn’t actually work.
Meaning that if, after uninstall, you open a file that is out of sort order, it will still be moved into its correct name-order.I did some debugging, and I see that, with uninstall, this line IS being executed:
notepad.clearCallbacks(self.file_opened_callback)
but, apparently, isn’t being noted by PS??
Didn’t you, at one time, face a similar issue?
-
@Alan-Kilborn said in Auto sort tabs (real-time):
Didn’t you, at one time, face a similar issue?
I think you did --> https://github.com/bruderstein/PythonScript/issues/153
But maybe doesn’t apply in my case, as issue 153 seems to indicate that “Notepad” object callbacks don’t suffer this fate, just “Editor” object callbacks…
-
Your script is installing/uninstalling for me. Maybe this isn’t
fixed in PS2?? Let me give it a try with 1.5.X -
Yep, the same script that works with PS3 does not uninstall the callback when you using PS2.
-
@Ekopalypse said in Auto sort tabs (real-time):
Yep, the same script that works with PS3 does not uninstall the callback when you using PS2.
Thanks for the confirmation.
I keep forgetting about PS3.
But, in truth, I have a few project where I need non-UTF8 encodings, so I’m a bit afraid to make the jump anyway.
I will make a note in my copy of the script that it can’t be uninstalled under PS2.
But, if it is something that someone will use, I don’t think they’ll uninstall it, and will set up the install part in startup.py. -
@Alan-Kilborn said in Auto sort tabs (real-time):
The ideal way would be to tie it into the “buffer activated” callback.
So I ended up doing this to the script.
To avoid the script “retriggering” itself, I added a new member variable in the init:self.is_installed = False
and then changed the callback function (its name, too!) a bit:
def buffer_activated_callback(self, args): if self.in_callback: return # the script moving tabs around will cause more callbacks to trigger; prevent this self.in_callback = True ... ...what it was doing before... ... self.in_callback = False
It seems to work okay.
If there’s interest, I can republish the entire script with the mods. -
@Alan-Kilborn said in Auto sort tabs (real-time):
I added a new member variable in the init:\
self.is_installed = False ... if self.in_callback:
Was that a typo in the first line, or am I confused and those are two separate things?
Assuming that first line should have been
self.in_callback = False
, I was able to see that work for me.It’s fun to try to “fight” it: try to rearrange the tabs manually while the script is in effect. ;-) But I guess the kind of person who would want this running all the time would not be the kind who would try to fight it, so wouldn’t accidentally trigger the fight.
Good work on that.
Given that it was multiple changes (adding a variable, changing method name, changing the notification it’s hooked to), it might be good to re-publish, so that future readers won’t make mistakes while piecing the script together, and then complain at you that it doesn’t work.
Thanks for the fun diversion.
-
@PeterJones said in Auto sort tabs (real-time):
Was that a typo in the first line
Yep, dammit. Sorry.
it might be good to re-publish
OK, will do.
-
It’s fun to try to “fight” it: try to rearrange the tabs manually while the script is in effect. ;-)
I certainly did NOT do any type of testing like that!
If it fails during something of that nature, well, sorry, but the user is to blame in that case. :-)and then complain at you that it doesn’t work.
Hey, how did you make that little complain-at-you popup text?
Okay, so here’s an “a” version of the script, with all the changes previously discussed:
# -*- coding: utf-8 -*- from Npp import editor, notepad, NOTIFICATION import os class SFTBFa(object): def __init__(self): self.installed = False self.in_callback = False def install(self): if not self.installed: notepad.callback(self.buffer_activated_callback, [NOTIFICATION.BUFFERACTIVATED]) self.installed = True def uninstall(self): if self.installed: notepad.clearCallbacks(self.buffer_activated_callback) # <---- may not work until PS 3.0.4+; see https://github.com/bruderstein/PythonScript/issues/153 self.installed = False def is_installed(self): return self.installed def buffer_activated_callback(self, args): if self.in_callback: return # the script moving tabs around will cause more callbacks to trigger; prevent this self.in_callback = True current_view = notepad.getCurrentView() other_view = 1 if current_view == 0 else 0 if notepad.getCurrentDocIndex(other_view) == 4294967295L: other_view = None curr_view_paths_list = []; curr_view_sorted_paths_list = [] other_view_paths_list = []; other_view_sorted_paths_list = [] for (filename, _, index_in_view, view) in notepad.getFiles(): if view == current_view: curr_view_paths_list.append(filename) curr_view_sorted_paths_list.append(filename) else: other_view_paths_list.append(filename) other_view_sorted_paths_list.append(filename) curr_view_sorted_paths_list.sort(key=lambda x: x.rsplit(os.sep, 1)[-1].upper()) curr_view_already_sorted = True if curr_view_paths_list == curr_view_sorted_paths_list else False other_view_sorted_paths_list.sort(key=lambda x: x.rsplit(os.sep, 1)[-1].upper()) other_view_already_sorted = True if other_view_paths_list == other_view_sorted_paths_list else False if not (curr_view_already_sorted and other_view_already_sorted): processed_other_view = False if other_view != None and not other_view_already_sorted: self.rearrange_tabs_in_view(other_view, other_view_sorted_paths_list) processed_other_view = True processed_current_view = False if not curr_view_already_sorted: self.rearrange_tabs_in_view(current_view, curr_view_sorted_paths_list) processed_current_view = True if processed_other_view and not processed_current_view: # leave the view we started in as the active one: notepad.activateIndex(current_view, notepad.getCurrentDocIndex(current_view)) self.in_callback = False def rearrange_tabs_in_view(self, view, sorted_name_list): notepad.activateIndex(view, notepad.getCurrentDocIndex(view)) # get switched into the correct view remembered_active_filename = notepad.getCurrentFilename() destination_index = 0 num_of_tabs = len(sorted_name_list) while destination_index < num_of_tabs: current_order_list = [] for (filename, _, index_in_view, v) in notepad.getFiles(): if v == view: current_order_list.append(filename) curr_location_index = current_order_list.index(sorted_name_list[destination_index]) move_left_count = curr_location_index - destination_index if move_left_count > 0: notepad.activateFile(current_order_list[curr_location_index]) for _ in range(move_left_count): notepad.menuCommand(MENUCOMMAND.VIEW_TAB_MOVEBACKWARD) destination_index += 1 notepad.activateFile(remembered_active_filename) if __name__ == '__main__': if 'sftbf_a' not in globals(): sftbf_a = SFTBFa() # each running of the script toggles install/uninstall: sftbf_a.uninstall() if sftbf_a.is_installed() else sftbf_a.install() notepad.messageBox('SortFileTabsByFilename {}INSTALLED!'.format('' if sftbf_a.is_installed() else 'UN'), '')