Last active
January 30, 2024 19:37
-
-
Save Axel-Erfurt/824ac1ed361322f29469558a1b7ef6b9 to your computer and use it in GitHub Desktop.
CSV Viewer Gtk3 Python
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/env 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 | |
from sys import argv | |
import pandas as pd | |
CSS = """ | |
#mywindow { | |
background: #eee; | |
} | |
#myheaderbar { | |
background: #eee; | |
border: 0px; | |
} | |
#csv-view { | |
padding: 2px; | |
color: #222; | |
font-family: "Noto Sans"; | |
font-size: 9pt; | |
} | |
#csv-view :selected { | |
background: #6aa1d8; | |
color: #fff; | |
font-weight: bold; | |
} | |
#csv-view header button { | |
color: #222; | |
background: #ddd; | |
font-weight: bold; | |
} | |
#csv-view :hover { | |
background: #444; | |
color: #ace; | |
} | |
#btn_open:hover ,#btn_save:hover, #btn_addrow:hover, #btn_remove_row:hover, #btn_copy_row:hover, #btn_paste_row:hover { | |
background: #abc; | |
} | |
#csv-view row:nth-child(even) { | |
background-color: #c6c6c6; | |
} | |
#csv-view row:nth-child(odd) { | |
background-color: #e9e9e9; | |
} | |
#myscrolledwin { | |
border: 0.02em solid #585858; | |
} | |
""" | |
class TreeViewFilterWindow(Gtk.Window): | |
def __init__(self): | |
Gtk.Window.__init__(self, title="CSV Viewer") | |
self.set_name("mywindow") | |
self.set_border_width(4) | |
self.current_file = "" | |
self.column_count = 0 | |
self.is_changed = False | |
self.connect("delete-event", self.on_close) | |
self.cb = "" | |
self.myheader = "" | |
self.use_header = False | |
self.editable = False | |
## file filter | |
self.file_filter_text = Gtk.FileFilter() | |
self.file_filter_text.set_name("CSV Files") | |
pattern = ["*.csv", "*.tsv"] | |
for p in pattern: | |
self.file_filter_text.add_pattern(p) | |
# box | |
self.vbox = Gtk.Box(orientation=1, vexpand=True) | |
self.add(self.vbox) | |
self.header_bar = Gtk.HeaderBar() | |
self.header_bar.set_name("myheaderbar") | |
self.header_bar.set_show_close_button(True) | |
self.header_bar.set_title("CSV Viewer") | |
self.header_bar.set_subtitle("Info") | |
self.set_titlebar(self.header_bar) | |
# 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.header_bar.add(self.btn_open) | |
# 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.header_bar.add(self.btn_save) | |
# 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.header_bar.add(self.btn_save_as) | |
# separator | |
sep = Gtk.Separator() | |
self.header_bar.pack_start(sep) | |
# 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 below selelected row") | |
self.btn_addrow.set_hexpand(False) | |
self.btn_addrow.set_relief(2) | |
self.btn_addrow.connect("clicked", self.on_add_row) | |
self.header_bar.add(self.btn_addrow) | |
# 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.header_bar.add(self.btn_remove_row) | |
sep = Gtk.Separator() | |
self.header_bar.add(sep) | |
# copy row button | |
self.btn_copy_row = Gtk.Button.new_from_icon_name("edit-copy", 2) | |
self.btn_copy_row.set_name("btn_copy_row") | |
self.btn_copy_row.set_tooltip_text("copy selelected row") | |
self.btn_copy_row.set_hexpand(False) | |
self.btn_copy_row.set_relief(2) | |
self.btn_copy_row.connect("clicked", self.on_copy_row) | |
self.header_bar.add(self.btn_copy_row) | |
# paste row button | |
self.btn_paste_row = Gtk.Button.new_from_icon_name("edit-paste", 2) | |
self.btn_paste_row.set_name("btn_paste_row") | |
self.btn_paste_row.set_tooltip_text("paste row below selelected row") | |
self.btn_paste_row.set_hexpand(False) | |
self.btn_paste_row.set_relief(2) | |
self.btn_paste_row.connect("clicked", self.on_paste_row) | |
self.header_bar.add(self.btn_paste_row) | |
sep = Gtk.Separator() | |
self.header_bar.add(sep) | |
# 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.header_bar.pack_end(self.search_field) | |
# treeview | |
self.treeview = Gtk.TreeView() | |
self.treeview.set_property("rules-hint", True) | |
self.treeview.set_name("csv-view") | |
self.treeview.set_grid_lines(3) | |
self.treeview.set_activate_on_single_click(False) | |
self.treeview.connect("row-activated", self.onSelectionChanged) | |
self.treeview.connect("button-press-event", self.on_pressed) | |
self.scrolled_window = Gtk.ScrolledWindow(vexpand = True, hexpand = True, propagate_natural_width = True, | |
margin_right = 5, margin_left = 5, margin_bottom = 5) | |
self.scrolled_window.set_name("myscrolledwin") | |
self.scrolled_window.add(self.treeview) | |
self.vbox.add(self.scrolled_window) | |
# 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 on_reordered(self, *args): | |
print("reordered") | |
def maybe_saved(self, *args): | |
print("is modified", self.is_changed) | |
md = Gtk.MessageDialog(title="CSV Viewer", 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() | |
if b: | |
return True | |
else: | |
Gtk.main_quit() | |
else: | |
Gtk.main_quit() | |
def on_add_row(self, *args): | |
model, paths = self.treeview.get_selection().get_selected_rows() | |
if paths: | |
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 | |
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: | |
self.editable = True | |
else: | |
self.editable = False | |
def onSelectionChanged(self, trview, event, *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_copy_row(self, *args): | |
model, pathlist = self.treeview.get_selection().get_selected() | |
if pathlist: | |
self.value_list = [] | |
for x in range(1, self.column_count + 1): | |
self.value_list.append(model[pathlist][x]) | |
self.cb = self.value_list | |
self.cb.insert(0, 0) | |
def on_paste_row(self, *args): | |
model, paths = self.treeview.get_selection().get_selected_rows() | |
if paths: | |
index = self.treeview.get_selection().get_selected_rows()[1][0][0] | |
self.my_liststore.insert(index + 1, self.cb) | |
self.is_changed = True | |
self.treeview.set_cursor(index + 1) | |
def on_open_file(self, *args): | |
docs = f'{GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOCUMENTS)}/CSV' | |
self.dialog = Gtk.FileChooserNative.new("Open", self, Gtk.FileChooserAction.OPEN, "Open", "Cancel") | |
self.dialog.set_current_folder(docs) | |
self.dialog.add_filter(self.file_filter_text) | |
self.dialog.set_transient_for(self) | |
self.dialog.connect("response", self.on_open_dialog_response) | |
self.dialog.show() | |
def on_open_dialog_response(self, dialog, response_id): | |
if response_id == Gtk.ResponseType.ACCEPT: | |
self.current_file = str(dialog.get_file().get_path()) | |
print(f"loading {self.current_file}") | |
name = self.current_file.split("/")[-1].split(".")[-2] | |
self.load_into_viewer(self.current_file) | |
dialog.destroy() | |
def load_into_viewer(self, file, *args): | |
self.search_field.set_text("") | |
self.current_filter_text = "" | |
for column in self.treeview.get_columns(): | |
self.treeview.remove_column(column) | |
self.df = pd.read_csv(file, header=None, keep_default_na=False, sep='\t') | |
self.my_liststore = Gtk.ListStore(int, *[str]* len(self.df.columns)) | |
self.column_count = len(self.df.columns) | |
# ask for header | |
header = self.df.iloc[0].values | |
md = Gtk.MessageDialog(title="CSV Viewer", message_type=Gtk.MessageType.QUESTION, | |
text=f"Use first row as header?\n\n{header}", | |
parent=None) | |
md.add_buttons("Yes", Gtk.ResponseType.YES, "No", Gtk.ResponseType.NO) | |
response = md.run() | |
if response == Gtk.ResponseType.YES: | |
md.destroy() | |
for row in self.df[1:].itertuples(): | |
self.my_liststore.append(list(row)) | |
# set columns | |
for i, column_title in enumerate(self.df.columns, start=1): | |
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) | |
# first row as header | |
for i in range(len(self.df.columns)): | |
self.treeview.get_column(i).set_title(self.df.iloc[0][i]) | |
self.use_header = True | |
elif response == Gtk.ResponseType.NO: | |
md.destroy() | |
for row in self.df.itertuples(): | |
self.my_liststore.append(list(row)) | |
# set columns | |
for i, column_title in enumerate(self.df.columns, start=1): | |
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) | |
self.use_header = False | |
self.treeview.set_model(self.my_liststore) | |
self.header_bar.set_subtitle(file.rpartition("/")[-1].rpartition(".")[0]) | |
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.is_changed = False | |
####################################################### | |
def on_save_file_as(self, *args): | |
if self.current_file == "": | |
return | |
header = "" | |
for i in range(len(self.df.columns)): | |
header += f"{self.treeview.get_column(i).get_title()}\t" | |
header = header.rpartition("\t")[0] | |
dlg = Gtk.FileChooserNative.new("Save", self, Gtk.FileChooserAction.SAVE, "Save", "Cancel") | |
dlg.set_do_overwrite_confirmation(True) | |
dlg.add_filter(self.file_filter_text) | |
dlg.set_current_name("*.csv") | |
response = dlg.run() | |
if response == Gtk.ResponseType.ACCEPT: | |
infile = dlg.get_filename() | |
self.header_bar.set_subtitle(infile.rpartition("/")[-1].rpartition(".")[0]) | |
self.current_file = infile | |
self.on_save_file() | |
else: | |
print("None") | |
dlg.destroy() | |
def on_save_file(self, *args): | |
if self.current_file == "": | |
self.on_save_file_as() | |
else: | |
self.table_to_df(self.current_file) | |
def table_to_df(self, file, *args): | |
header_list = [] | |
for i in range(len(self.df.columns)): | |
header_list.append(f"{self.treeview.get_column(i).get_title()}") | |
row_list = [list(row[1:]) for row in self.my_liststore] | |
df = pd.DataFrame(row_list, columns = header_list) | |
df.to_csv(file, sep='\t', index=False) | |
print(f"'{file}' saved") | |
self.is_changed = False | |
def text_edited(self, cellrenderertext, treepath, new_text): | |
column = self.column_index | |
self.my_liststore[treepath][column] = new_text | |
self.is_changed = True | |
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(1, 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 search_query in value 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(700, 400) | |
win.move(0, 0) | |
win.show_all() | |
win.resize(900, 500) | |
if len(argv) > 1: | |
mfile = argv[1] | |
win.load_into_table(mfile) | |
Gtk.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment