Skip to content

Instantly share code, notes, and snippets.

@Axel-Erfurt
Last active January 30, 2024 19:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Axel-Erfurt/824ac1ed361322f29469558a1b7ef6b9 to your computer and use it in GitHub Desktop.
Save Axel-Erfurt/824ac1ed361322f29469558a1b7ef6b9 to your computer and use it in GitHub Desktop.
CSV Viewer Gtk3 Python
#!/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