Skip to content

Instantly share code, notes, and snippets.

@ClaudiaFrank ClaudiaFrank/Sorter.py
Last active Jul 28, 2018

Embed
What would you like to do?
Sorter
# -*- coding: utf-8 -*-
from Npp import editor, notepad, MESSAGEBOXFLAGS
from collections import OrderedDict
import random
UNIDECODE_NEEDED = True # setting this to False will disable the feature
if UNIDECODE_NEEDED:
try:
from unidecode import unidecode
except ImportError:
notepad.messageBox(('This script needs the unidecode library\n'
'in order to handle accented chars\n\n'
'If you do not need this feature, consider setting the variable\n\n'
'UNIDECODE_NEEDED=False'),'Missing Library', MESSAGEBOXFLAGS.ICONERROR)
raise
try:
import Tkinter as tk
import ttk
except ImportError as e:
notepad.messageBox(('Unable to import Tkinter libraries,\n'
'these are needed for the UI.\n\n'
'Check your installation.\n\n'
'{}'.format(e.message)),'Missing Library', MESSAGEBOXFLAGS.ICONERROR)
raise
# ***** user configuration area start *****
CLOSE_WINDOW_AFTER_RUN = True
KEEP_WINDOW_IN_FRONT = False
sorting_flags = OrderedDict()
sorting_flags['enable sort' ] = True
sorting_flags['reverse ordering' ] = False
sorting_flags['random ordering' ] = False
sorting_flags['case insensitive' ] = False
if UNIDECODE_NEEDED:
sorting_flags['accent converted' ] = False
sorting_flags['remove spaces' ] = False
remove_flags = OrderedDict()
remove_flags['remove duplicates' ] = False
remove_flags['full line compare' ] = True # one of those should be true
remove_flags['sel. columns compare' ] = False # the other should be false
remove_flags['case insensitive' ] = False
if UNIDECODE_NEEDED:
remove_flags['accent converted' ] = False
remove_flags['remove spaces' ] = False
# ***** user configuration area end *****
'''
Implements basic line sort functionality, current feature list
- sorting ascending, descending, reverse ordering
- ignore accented characters -> means e.g. Ä gets replaced by A
- remove duplicate lines (even without sorting)
- sort whole text or selected lines only
Outstanding:
- nothing yet
Current version: 0.4
(special thanks to Scott for improving code and UI,
doing testing and correcting my spelling errors)
Version:0.4
- unidecode feature optional
- sort rectangular selection with zero-width
- removing duplicates on rectangular selection
- UI improvements
- bug fixes
Version: 0.3
- ignore leading/trailing spaces when comparing duplicates
- sort rectangular selection only on selected lines
Version: 0.2
- bug fixes (thanks to Scott)
- change to unicode (accented chars bug)
- first rectangular selection implemented (zero-width outstanding)
- sort as numeric (for rectangular selection only)
- random sort (thanks to Scott again)
Version 0.1
- sorting ascending, descending, reverse ordering
- ignore accented characters -> means e.g. Ä gets replaced by A
- remove duplicate lines (even without sorting)
- sort selected text only (vertical selection)
'''
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class UsageWindow(object):
__metaclass__ = Singleton
'''
Flags:
-- means no flags are set
i = case insensitive
a = accent converted
r = reverse ordering
s = remove spaces
Only sorting:
Example:
FLAGS: -- r i ir a ar ia iar
text Test täxt test Täxt Tast text tast text
täst Test täxt Test täxt Tast text tast Text
Text Text täst test täxt Taxt test Tast Text
Text Text täst Test Täxt Taxt test Tast text
test Täst text text täst Test taxt Taxt test
täst Täst text Text täst Test taxt taxt Test
Täxt Täxt test Text Täst Text tast taxt test
Test Täxt test text Täst Text tast Taxt Test
test test Täxt täst text tast Text test Taxt
Täst test Täxt täst Text tast Text Test taxt
Täst text Täst Täst Text taxt Test test taxt
täxt text Täst Täst text taxt Test Test Taxt
täxt täst Text Täxt test test Taxt text tast
text täst Text täxt Test test Taxt Text tast
Täxt täxt Test täxt test text Tast Text Tast
Test täxt Test Täxt Test text Tast text Tast
remove spaces flag should behave the same - what it does is that it
removes leading and trailing slashes from a lime before sorting algo starts
If text is vertically selected, then the same results should be expected but
only for the lines being (wholly or partially) selected
Rectangular(rect) selection is a little bit different.
If zero width rect selection is done, the remaining part of the line is
used to key the sorting and if "real" rect selection is done
(selection contains chars) then only these chars are used as the sort key
Example:
rect_numbers = mark the numbers .7 10 8 11,3 ... with a rectangular selection
0-width rect before num = create a zero width rectangular selection before the numbers
0-width rect after num = create a zero width rectangular selection after the numbers
Note | is used to illustrate the different results
FLAGS: | -- | rect numbers | 0-width rect before num | 0-width rect after num
text_c .7 a | text_a 8 j | text_n -5 b | text_n -5 b | text_c .7 a
text_e 10 k | text_b 11,3 m | text_c .7 a | text_c .7 a | text_n -5 b
text_a 8 j | text_c .7 a | text_i 1 d | text_i 1 d | text_h 2 c
text_b 11,3 m | text_d 6 h | text_h 2 c | text_e 10 k | text_i 1 d
text_h 2 c | text_e 10 k | text_f 3 e | text_b 11,3 m | text_f 3 e
text_i 1 d | text_f 3 e | text_g 4 f | text_m 12 n | text_g 4 f
text_m 12 n | text_g 4 f | text_k 5 g | text_h 2 c | text_k 5 g
text_n -5 b | text_h 2 c | text_d 6 h | text_f 3 e | text_d 6 h
text_d 6 h | text_i 1 d | text_j 7.0 i | text_g 4 f | text_j 7.0 i
text_f 3 e | text_j 7.0 i | text_a 8 j | text_k 5 g | text_a 8 j
text_k 5 g | text_k 5 g | text_l 9 l | text_d 6 h | text_e 10 k
text_l 9 l | text_l 9 l | text_e 10 k | text_j 7.0 i | text_l 9 l
text_j 7.0 i | text_m 12 n | text_b 11,3 m | text_a 8 j | text_b 11,3 m
text_g 4 f | text_n -5 b | text_m 12 n | text_l 9 l | text_m 12 n
In general, if a rectangular selection is made, the script tries to do a numeric sorting if possible and falls back to text otherwise.
Removing duplicate lines should follow the same principles except that
column comparison radio button needs to be used instead of just having
an active column selection when running.
'''
def __init__(self, widget):
self.widget = widget
self.window = None
def show(self):
if self.window:
self.hide()
return
x = self.widget.winfo_rootx() + 20
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 1
self.window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
label = tk.Label(self.window,
text=('UsageWindow source code has all information\n'
'Click again on the question mark to close this info window'),
justify=tk.LEFT,
background="#ffffe0",
relief=tk.SOLID,
borderwidth=1)
label.pack()
def hide(self):
tw = self.window
self.window = None
if tw:
tw.destroy()
class ToolTip():
# stolen from idellib but modified to our needs
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tipwindow = None
self.id = None
self._id1 = self.widget.bind("<Enter>", self.enter)
self._id2 = self.widget.bind("<Leave>", self.leave)
def enter(self, event=None):
self.schedule()
def leave(self, event=None):
self.unschedule()
self.hidetip()
def schedule(self):
self.unschedule()
self.id = self.widget.after(500, self.showtip)
def unschedule(self):
id = self.id
self.id = None
if id:
self.widget.after_cancel(id)
def showtip(self):
if self.tipwindow or 'disabled' in self.widget.state():
return
x = self.widget.winfo_rootx() + 20
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 1
self.tipwindow = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
self.showcontents()
def showcontents(self):
label = tk.Label(self.tipwindow, text=self.text, justify=tk.LEFT,
background="#ffffe0", relief=tk.SOLID, borderwidth=1)
label.pack()
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
class SorterWindow(object):
def time_function(func):
import time
def wrap(self,*args,**kwargs):
start = time.clock()
result = func(self, *args,**kwargs)
print(u'{} took {} seconds'.format(func.__name__, time.clock()-start))
return result
return wrap
@staticmethod
def remove_duplicates(lines, key=None):
__unique = set()
for line in lines:
if remove_flags['remove spaces']:
line = line.strip()
if remove_flags['case insensitive']:
line = line.lower()
if UNIDECODE_NEEDED and remove_flags['accent converted']:
line = unidecode(line).decode('utf8')
if remove_flags['sel. columns compare'] and (not key is None):
__line = key(line)
if __line not in __unique:
yield line
__unique.add(__line)
else:
if line not in __unique:
yield line
__unique.add(line)
#@time_function
def _sort(self, text, user_defined_key=None):
_lines = text.decode('utf8').splitlines()
if sorting_flags['random ordering']:
return random.sample(_lines, len(_lines))
_key = unicode.lower if sorting_flags['case insensitive'] else None
if not user_defined_key is None:
_key = user_defined_key
def prepare_lines(lines):
for line in lines:
if sorting_flags['remove spaces']:
line = line.strip()
if UNIDECODE_NEEDED and sorting_flags['accent converted']:
line = unidecode(line).decode('utf8')
yield line
if sorting_flags['enable sort']:
_lines = list(prepare_lines(_lines))
_lines.sort(key=_key, reverse=sorting_flags['reverse ordering'])
if remove_flags['remove duplicates']:
_lines = list(self.remove_duplicates(_lines, _key))
return _lines
def center(self):
self.window.update_idletasks()
w = self.window.winfo_screenwidth()
h = self.window.winfo_screenheight()
size = tuple(int(_) for _ in self.window.geometry().split('+')[0].split('x'))
x = w/2 - size[0]/2
y = h/2 - size[1]/2
self.window.geometry("%dx%d+%d+%d" % (size + (x, y)))
def run(self):
if (sorting_flags['enable sort']) or (remove_flags['remove duplicates']):
line_ending = ['\r\n', '\r', '\n'][notepad.getFormatType()]
if editor.getSelectionEmpty() and (editor.getSelectionMode() != 1):
editor.setText(line_ending.join(self._sort(editor.getCharacterPointer())))
elif editor.getSelectionMode() == 1:
start = editor.getSelectionNStart(0)
end = editor.getSelectionNEnd(0)
rect_anchor = editor.getColumn(editor.getRectangularSelectionAnchor()) + editor.getRectangularSelectionAnchorVirtualSpace()
rect_caret = editor.getColumn(editor.getRectangularSelectionCaret()) + editor.getRectangularSelectionCaretVirtualSpace()
sort_key_width = abs(rect_anchor - rect_caret)
sort_key_start_column = min(rect_anchor, rect_caret)
sort_key_end_column = sort_key_start_column + sort_key_width
line_start, line_end = editor.getUserLineSelection()
start_position_selected_lines = editor.positionFromLine(line_start)
end_position_selected_lines = editor.getLineEndPosition(line_end)
def sort_as_int_if_possible(text):
try:
return float(text.strip().replace(',','.',1))
except:
return text
if start != end:
lines = self._sort(editor.getTextRange(start_position_selected_lines, end_position_selected_lines),
lambda x: sort_as_int_if_possible(x[sort_key_start_column:sort_key_end_column]))
else:
lines = self._sort(editor.getTextRange(start_position_selected_lines, end_position_selected_lines),
lambda x: sort_as_int_if_possible(x[sort_key_start_column:]))
editor.setTarget(start_position_selected_lines, end_position_selected_lines)
editor.replaceTarget(line_ending.join(lines))
else:
selection_start, selection_end = editor.getUserCharSelection()
start_position_selected_lines = editor.positionFromLine(editor.lineFromPosition(selection_start))
end_position_selected_lines = editor.getLineEndPosition(editor.lineFromPosition(selection_end))
lines = self._sort(editor.getTextRange(start_position_selected_lines, end_position_selected_lines))
editor.setTarget(start_position_selected_lines, end_position_selected_lines)
editor.replaceTarget(line_ending.join(lines))
if self.close_window_after_run.get():
self.window.destroy()
def func(self, event):
self.run()
def on_sorting_click(self, value):
sorting_flags[value] = bool(not sorting_flags[value])
if (value == 'enable sort'):
if sorting_flags[value] is False:
[self.button_dict.get('sort_'+key).configure(state=('disabled',)) for key in sorting_flags.keys() if key != value]
else:
[self.button_dict.get('sort_'+key).configure(state=('normal',)) for key in sorting_flags.keys()]
elif (value == 'random ordering'):
if sorting_flags[value] is True:
[self.button_dict.get('sort_'+key).configure(state=('disabled',)) for key in sorting_flags.keys() if key not in ['enable sort', 'random ordering']]
else:
[self.button_dict.get('sort_'+key).configure(state=('normal',)) for key in sorting_flags.keys() if key not in ['enable sort', 'random ordering']]
def on_comparision_click(self, value):
remove_flags['full line compare'] = not remove_flags['full line compare']
remove_flags['sel. columns compare'] = not remove_flags['sel. columns compare']
def on_remove_click(self, value):
remove_flags[value] = bool(not remove_flags[value])
if value == 'remove duplicates':
if remove_flags[value] is False:
[self.button_dict.get('remove_'+key).configure(state=('disabled',)) for key in remove_flags.keys() if key != value]
else:
[self.button_dict.get('remove_'+key).configure(state=('normal',)) for key in remove_flags.keys()]
def show_usage(self,e,widget):
UsageWindow(widget).show()
def keep_window_in_front(self):
self.window.attributes('-topmost',True if self.keep_it_in_front.get() else False)
def __init__(self):
self.TIME_SORT = True
self.remove_spaces_explanation = ('When activated leading and trailing spaces will be removed\n'
'before sorting or comparing happens')
self.button_dict = {}
self.window = tk.Tk()
self.window.title('Sorter')
self.window.resizable(False, False)
self.window.bind('<Return>', self.func)
self.window.minsize(400,10)
self.window.grid_columnconfigure(0, weight=1)
self.window.grid_columnconfigure(1, weight=1)
control_styles = ttk.Style()
control_styles.configure('TButton', font=('courier', 12, 'bold'), relief="flat")
control_styles.configure('TLabelframe.Label', font=('courier', 10, 'italic'))
self.sorting_frame = ttk.Labelframe(self.window, text="Sorting")
self.sorting_frame.grid(row=0, column=0, padx=5, pady=5, sticky=tk.W+tk.E)
self.remove_frame = ttk.Labelframe(self.window, text="Line Removal")
self.remove_frame.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W+tk.E)
btn = ttk.Button(self.window, text='RUN IT', command=self.run)
btn.grid(row=1, column=0, columnspan=2, padx=5, sticky=tk.W+tk.E)
self.close_window_after_run = tk.BooleanVar()
self.close_window_after_run.set(CLOSE_WINDOW_AFTER_RUN)
chkbtn = ttk.Checkbutton(self.window,
text='Close window after run',
variable=self.close_window_after_run)
chkbtn.grid(row=2, padx=5, pady=5, sticky=tk.W)
self.keep_it_in_front = tk.BooleanVar()
self.keep_it_in_front.set(KEEP_WINDOW_IN_FRONT)
chkbtn = ttk.Checkbutton(self.window,
text='Keep window in front',
variable=self.keep_it_in_front,
command=self.keep_window_in_front)
chkbtn.grid(row=2, column=1, padx=5, pady=5, sticky=tk.W)
label = ttk.Label(self.window, text='?', font=('courier', 10, 'bold'))
label.grid(row=2, column=1, padx=5, pady=5, sticky=tk.E)
label.bind("<Button-1>",lambda x:self.show_usage(x,label)) # x being the event instance
self.sorting_vars = []
for i, option in enumerate(sorting_flags.keys()):
self.var = tk.BooleanVar()
cb = ttk.Checkbutton(self.sorting_frame,
text=option,
variable=self.var,
command=lambda x=option:self.on_sorting_click(x))
if option == 'remove spaces':
ToolTip(cb, text=self.remove_spaces_explanation)
if sorting_flags.get(option, False):
cb.state(['selected'])
cb.grid(row=i,sticky=tk.W, ipady=2, padx=5 if option=='enable sort' else 20)
self.button_dict['sort_'+option] = cb
self.sorting_vars.append(self.var)
self.remove_vars = []
global_state = remove_flags.get('remove duplicates', False)
self.radio_var = tk.StringVar()
for i, option in enumerate(remove_flags.keys()):
self.var = tk.StringVar()
if 'compare' in option:
cb = ttk.Radiobutton(self.remove_frame,
text=option,
variable=self.radio_var,
command=lambda x=option:self.on_comparision_click(x),
value=option)
if remove_flags.get(option,None):
self.radio_var.set(option)
cb.grid(row=i, sticky=tk.W, ipady=2, padx=20)
else:
cb = ttk.Checkbutton(self.remove_frame,
text=option,
variable=self.var,
command=lambda x=option:self.on_remove_click(x))
if option == 'remove spaces':
ToolTip(cb, text=self.remove_spaces_explanation)
cb.grid(row=i, sticky=tk.W, ipady=2, padx=5 if option=='remove duplicates' else 20)
if global_state is True:
if remove_flags.get(option, False):
cb.state(['selected'])
else:
if option != 'remove duplicates':
cb.state(['selected','disabled'] if remove_flags.get(option, False) else ['disabled'])
self.button_dict['remove_'+option] = cb
self.remove_vars.append(self.var)
self.center()
self.window.focus_force()
self.window.mainloop()
SorterWindow()
@sasumner

This comment has been minimized.

Copy link

commented Jun 4, 2018

Small issue, maybe not worth handling: I have 3 monitors. Primary (task bar) is the left monitor. N++ is fullscreen on center monitor. Running script causes the Tk window to appear in the middle of the left monitor. (Win7)

I think if the Tk window had different "parentage" it would appear centered upon the N++ window, plus it would be modal (maybe it being modeless is nice though because if you forget to do a selection before running the script you can still switch back to the editor tab and make the selection...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.