Last active
April 5, 2022 20:23
-
-
Save Axel-Erfurt/ecadfb0d64d68717697a0bf4b52408dc to your computer and use it in GitHub Desktop.
CSV Viewer Gtk3 Python with Filter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# -*- coding: utf-8 -*- | |
import gi | |
gi.require_version("Gtk", "3.0") | |
gi.require_version("Gdk", "3.0") | |
from gi.repository import Gtk, Gdk, GLib | |
CSS = """ | |
window { | |
background: #eee; | |
} | |
#csv-view { | |
color: #222; | |
font-family: "Noto Sans"; | |
font-size: 9pt; | |
} | |
#csv-view :selected { | |
background: #bdf; | |
color: #222; | |
} | |
#csv-view header button { | |
color: #222; | |
background: #ddd; | |
font-weight: bold; | |
border: 0.1pt solid #111; | |
} | |
#csv-view :hover { | |
background: #444; | |
color: #ace; | |
} | |
#btn_open:hover ,#btn_save:hover, #btn_addrow:hover{ | |
background: #abc; | |
} | |
#csv-view row:nth-child(odd) { | |
background-color: shade(@base_normal, 0.93); | |
} | |
#sublabel { | |
font-size: 8pt; | |
color: #444; | |
} | |
""" | |
class TreeViewFilterWindow(Gtk.Window): | |
def __init__(self): | |
Gtk.Window.__init__(self, title="CSV Viewer") | |
self.set_border_width(4) | |
self.current_file = "" | |
self.column_count = 0 | |
self.is_changed = False | |
self.connect("delete-event", self.on_close) | |
# box | |
self.vbox = Gtk.Box(orientation=1, vexpand=True) | |
self.add(self.vbox) | |
self.hbox = Gtk.Box(orientation=0) | |
# open button | |
self.btn_open = Gtk.Button.new_from_icon_name("document-open", 2) | |
self.btn_open.set_name("btn_open") | |
self.btn_open.set_tooltip_text("Open File") | |
self.btn_open.set_hexpand(False) | |
self.btn_open.set_relief(2) | |
self.btn_open.connect("clicked", self.on_open_file) | |
self.hbox.pack_start(self.btn_open, False, False, 1) | |
# save button | |
self.btn_save = Gtk.Button.new_from_icon_name("document-save", 2) | |
self.btn_save.set_name("btn_save") | |
self.btn_save.set_tooltip_text("Save current File") | |
self.btn_save.set_hexpand(False) | |
self.btn_save.set_relief(2) | |
self.btn_save.connect("clicked", self.on_save_file) | |
self.hbox.pack_start(self.btn_save, False, False, 1) | |
# save as button | |
self.btn_save_as = Gtk.Button.new_from_icon_name("document-save-as", 2) | |
self.btn_save_as.set_name("btn_save") | |
self.btn_save_as.set_tooltip_text("Save As ...") | |
self.btn_save_as.set_hexpand(False) | |
self.btn_save_as.set_relief(2) | |
self.btn_save_as.connect("clicked", self.on_save_file_as) | |
self.hbox.pack_start(self.btn_save_as, False, False, 1) | |
# add row button | |
self.btn_addrow = Gtk.Button.new_from_icon_name("add", 2) | |
self.btn_addrow.set_name("btn_addrow") | |
self.btn_addrow.set_tooltip_text("insert row after selelected") | |
self.btn_addrow.set_hexpand(False) | |
self.btn_addrow.set_relief(2) | |
self.btn_addrow.connect("clicked", self.on_add_row) | |
self.hbox.pack_start(self.btn_addrow, False, False, 1) | |
# remove row button | |
self.btn_remove_row = Gtk.Button.new_from_icon_name("remove", 2) | |
self.btn_remove_row.set_name("btn_remove_row") | |
self.btn_remove_row.set_tooltip_text("remove selelected row") | |
self.btn_remove_row.set_hexpand(False) | |
self.btn_remove_row.set_relief(2) | |
self.btn_remove_row.connect("clicked", self.on_remove_row) | |
self.hbox.pack_start(self.btn_remove_row, False, False, 1) | |
# label | |
self.sub_title_label = Gtk.Label(label="Info") | |
self.sub_title_label.set_name("sublabel") | |
self.hbox.pack_start(self.sub_title_label, True, False, 1) | |
# search field | |
self.search_field = Gtk.SearchEntry() | |
self.search_field.set_placeholder_text("filter") | |
self.search_field.connect("activate", self.on_selection_button_clicked) | |
self.search_field.connect("search-changed", self.on_search_changed) | |
self.search_field.set_vexpand(False) | |
self.hbox.pack_end(self.search_field, False, False, 1) | |
self.vbox.pack_start(self.hbox, False, False, 1) | |
# treeview | |
self.treeview = Gtk.TreeView() | |
self.treeview.set_name("csv-view") | |
self.treeview.set_grid_lines(3) | |
self.treeview.set_reorderable(True) | |
self.treeview.connect("drag-data-received", self.drag_data_received) | |
self.treeview.connect("cursor-changed", self.onSelectionChanged) | |
self.treeview.connect("button-press-event", self.on_pressed) | |
self.my_treelist = Gtk.ScrolledWindow() | |
self.my_treelist.add(self.treeview) | |
self.vbox.pack_end(self.my_treelist, True, True, 5) | |
# style | |
provider = Gtk.CssProvider() | |
provider.load_from_data(bytes(CSS.encode())) | |
style = self.treeview.get_style_context() | |
screen = Gdk.Screen.get_default() | |
priority = Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION | |
style.add_provider_for_screen(screen, provider, priority) | |
def maybe_saved(self, *args): | |
print("is modified", self.is_changed) | |
md = Gtk.MessageDialog(title="PyEdit4", message_type=Gtk.MessageType.QUESTION, | |
text="The document was changed.\n\nSave changes?", | |
parent=None) | |
md.add_buttons("Cancel", Gtk.ResponseType.CANCEL, | |
"Yes", Gtk.ResponseType.YES, "No", Gtk.ResponseType.NO) | |
response = md.run() | |
if response == Gtk.ResponseType.YES: | |
### save | |
self.on_save_file() | |
md.destroy() | |
return False | |
elif response == Gtk.ResponseType.NO: | |
md.destroy() | |
return False | |
elif response == Gtk.ResponseType.CANCEL: | |
md.destroy() | |
return True | |
md.destroy() | |
def on_close(self, *args): | |
print("goodbye ...") | |
print(f"{self.current_file} changed: {self.is_changed}") | |
if self.is_changed: | |
b = self.maybe_saved() | |
print (f"close: {b}") | |
if b: | |
return True | |
else: | |
Gtk.main_quit() | |
else: | |
Gtk.main_quit() | |
def on_add_row(self, *args): | |
index = self.treeview.get_selection().get_selected_rows()[1][0][0] | |
self.my_liststore.insert(index + 1) | |
self.is_changed = True | |
def on_remove_row(self, *args): | |
model, paths = self.treeview.get_selection().get_selected_rows() | |
if paths: | |
for path in paths: | |
iter = self.my_liststore.get_iter(path) | |
self.my_liststore.remove(iter) | |
self.is_changed = True | |
def on_pressed(self, trview, event): | |
path, col, x, y = trview.get_path_at_pos(event.x, event.y) | |
self.column_index = col.colnr | |
self.path = path | |
def onSelectionChanged(self, *args): | |
model, pathlist = self.treeview.get_selection().get_selected() | |
if pathlist: | |
self.value_list = [] | |
for x in range(self.column_count): | |
self.value_list.append(model[pathlist][x]) | |
def on_open_file(self, *args): | |
dlg = Gtk.FileChooserDialog(title="Please choose a file", parent=None, action = 0) | |
dlg.add_buttons("Cancel", Gtk.ResponseType.CANCEL, | |
"Open", Gtk.ResponseType.OK) | |
docs = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOCUMENTS) | |
dlg.set_current_folder(docs) | |
filter = Gtk.FileFilter() | |
filter.set_name("CSV Files") | |
filter.add_pattern("*.csv") | |
dlg.add_filter(filter) | |
response = dlg.run() | |
if response == Gtk.ResponseType.OK: | |
self.current_file = (dlg.get_filename()) | |
self.load_into_table(self.current_file) | |
dlg.destroy() | |
def load_into_table(self, csv_file, *args): | |
self.search_field.set_text("") | |
self.current_filter_text = "" | |
for column in self.treeview.get_columns(): | |
self.treeview.remove_column(column) | |
my_list = [] | |
my_csv = open(csv_file, 'r').read().splitlines() | |
self.column_count = len(my_csv[0].split('\t')) | |
self.my_liststore = Gtk.ListStore(*[str]* self.column_count) | |
for i, column_title in enumerate(my_csv[0].split('\t')): | |
renderer = Gtk.CellRendererText() | |
renderer.set_property('editable', True) | |
renderer.connect("edited", self.text_edited) | |
column = Gtk.TreeViewColumn(column_title, renderer, text=i) | |
column.colnr = i | |
self.treeview.append_column(column) | |
for line in my_csv[1:]: | |
row = line.split('\t') | |
my_list.append(tuple(row)) | |
for line_value in my_list: | |
self.my_liststore.append(list(line_value)) | |
self.my_filter = self.my_liststore.filter_new() | |
self.my_filter.set_visible_func(self.visible_cb) | |
self.treeview.set_model(self.my_filter) | |
# self.treeview.set_enable_search(True) | |
self.sub_title_label.set_text(csv_file) | |
self.is_changed = False | |
def on_save_file_as(self, *args): | |
dlg = Gtk.FileChooserDialog(title="Please choose a file", parent=None, action = 1) | |
dlg.add_buttons("Cancel", Gtk.ResponseType.CANCEL, | |
"Save", Gtk.ResponseType.OK) | |
filter = Gtk.FileFilter() | |
filter.set_name("CSV Files") | |
filter.add_pattern("*.csv") | |
dlg.add_filter(filter) | |
dlg.set_current_name("*.csv") | |
response = dlg.run() | |
if response == Gtk.ResponseType.OK: | |
infile = (dlg.get_filename()) | |
print(infile) | |
# model to csv_file | |
list_string = [] | |
for node in self.my_liststore: | |
d = [] | |
for column in range(self.column_count): | |
d.append(node[column]) | |
list_string.append(d) | |
with open(infile, 'w') as f: | |
for line in list_string: | |
value = "\t".join(line) | |
f.write(f'{value}\n') | |
self.is_changed = False | |
else: | |
print("None") | |
dlg.destroy() | |
def on_save_file(self, *args): | |
if self.current_file == "": | |
self.on_save_file_as() | |
else: | |
# model to csv_file | |
list_string = [] | |
for node in self.my_liststore: | |
d = [] | |
for column in range(self.column_count): | |
d.append(node[column]) | |
list_string.append(d) | |
with open(self.current_file, 'w') as f: | |
for line in list_string: | |
value = "\t".join(line) | |
f.write(f'{value}\n') | |
self.is_changed = False | |
def text_edited(self, cellrenderertext, treepath, text): | |
column = self.column_index | |
self.my_liststore[treepath][column] = text | |
self.is_changed = True | |
def drag_data_received(self, dest, selection_data, *args): | |
print(dest.get_drag_dest_row()) | |
def my_filter_func(self, model, iter, data): | |
if ( | |
self.current_filter_text is None | |
or self.current_filter_text == "None" | |
): | |
return True | |
else: | |
return model[iter][0] == self.current_filter_text | |
def on_selection_button_clicked(self, widget): | |
self.current_filter_text = widget.get_text() | |
self.my_filter.refilter() | |
def visible_cb(self, model, iter, data=None): | |
search_query = self.search_field.get_text().lower() | |
active_category = 0 | |
search_in_all_columns = True | |
if search_query == "": | |
return True | |
if search_in_all_columns: | |
for col in range(0,self.treeview.get_n_columns()): | |
value = model.get_value(iter, col) | |
if (search_query.lower() in value | |
or search_query.upper() in value | |
or search_query.title() in value): | |
return True | |
return False | |
value = model.get_value(iter, active_category).lower() | |
return True if value.startswith(search_query) else False | |
def on_search_changed(self, *args): | |
self.on_selection_button_clicked(self.search_field) | |
win = TreeViewFilterWindow() | |
win.connect("destroy", Gtk.main_quit) | |
win.set_size_request(600, 300) | |
win.move(0, 0) | |
win.show_all() | |
win.resize(800, 500) | |
Gtk.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
added editing, saving, add / remove row