Skip to content

Commit

Permalink
add a less trivial example: HDF5Viewer
Browse files Browse the repository at this point in the history
  • Loading branch information
jwahlstrand committed Dec 10, 2023
1 parent 2eff182 commit 09929d9
Show file tree
Hide file tree
Showing 4 changed files with 338 additions and 4 deletions.
11 changes: 11 additions & 0 deletions examples/HDF5Viewer/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name = "HDF5Viewer"
uuid = "540b0f16-6a51-4821-b2c9-4019d94fcdf5"
version = "0.1.0"

[deps]
Gtk4 = "9db2cae5-386f-4011-9d63-a5602296539b"
HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"

[compat]
Gtk4 = "0.6"

214 changes: 214 additions & 0 deletions examples/HDF5Viewer/src/HDF5Viewer.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
module HDF5Viewer

using Gtk4, HDF5, Gtk4.GLib

if Threads.nthreads() == 1 && Threads.nthreads(:interactive) < 1
@warn("For a more responsive UI, run with multiple threads enabled.")
end

export hdf5view

# This is really just a toy at this point, but it is a less trivial example of
# using a ListView to show a tree and it uses a second thread to keep the UI
# responsive.


# Ideas for improvements:
# - style: color rows by type (group, data)
# - context menu that allows you to copy the key of an item (so you can paste it somewhere)
# - filter by type
# - replace TreeView for attributes with a ListView

_repr(d::HDF5.Dataset) = repr(MIME("text/plain"),read(d), context=:limit=>true)
_repr(d::HDF5.Group) = ""

# setup callback for the key ColumnView (the first column)
function setup_cb(f, li)
tree_expander = GtkTreeExpander()
set_child(tree_expander,GtkLabel(""))
set_child(li,tree_expander)
end

# setup callback for the type and size ColumnViews
list_setup_cb(f, li) = set_child(li,GtkLabel(""))

# bind callback for the key ColumnView
function bind_cb(f, li)
row = li[]
tree_expander = get_child(li)
Gtk4.set_list_row(tree_expander, row)
text = Gtk4.get_item(row).string
text = split(text,'/')[end]
label = get_child(tree_expander)
label.label = text
end

# Build tree model from an HDF5 file

lmm(g,k) = GtkStringList([join([k,b],'/') for b in keys(g)])
lmm(d::HDF5.Dataset,k) = nothing

function create_tree_model(filename)
h5open(filename,"r") do h
function create_model(pp)
k=pp.string::String
return lmm(h[k],k)
end
rootmodel = GtkStringList(keys(h))
GtkTreeListModel(GLib.GListModel(rootmodel),false, true, create_model)
end
end

# Print data in a TextView

function render_data(dtv,txt,atv,ls,pr)
if ls === nothing
hide(atv)
Gtk4.position(pr,0)
else
Gtk4.model(atv,GtkTreeModel(ls))
show(atv)
Gtk4.position(pr,100)
end
dtv.buffer.text = txt
end

function match_string(row, entrytext)
item = Gtk4.get_item(row)
if item === nothing
return false
end
if contains(Gtk4.G_.get_string(item), entrytext)
return true
end
# if any of this row's children match, we should show this one too
cren = Gtk4.get_children(row)
if cren !== nothing
for i=0:(length(cren)-1)
if match_string(Gtk4.G_.get_child_row(row,i), entrytext)
return true
end
end
end
return false
end

function hdf5view(filename::AbstractString)
ispath(filename) || error("Path $filename does not exist")
isfile(filename) || error("File not found")
HDF5.ishdf5(filename) || error("File is not HDF5")
b = GtkBuilder(joinpath(@__DIR__, "HDF5Viewer.ui"))
w = b["win"]::GtkWindowLeaf
Gtk4.title(w, "HDF5 Viewer: $filename")
treemodel = create_tree_model(filename)

factory = GtkSignalListItemFactory(setup_cb, bind_cb)

# bind callback for the type ColumnView
function type_bind_cb(f, li)
label = get_child(li)
row = li[]
k=Gtk4.get_item(row).string
h5open(filename,"r") do h
d=h[k]
if isa(d,HDF5.Group)
Gtk4.label(label, "Group")
else
eltyp=string(HDF5.get_jl_type(d))
Gtk4.label(label, "Array{$eltyp}")
end
end
end

# bind callback for the size ColumnView
function size_bind_cb(f, li)
label = get_child(li)
row = li[]
k=Gtk4.get_item(row).string
h5open(filename,"r") do h
d=h[k]
if isa(d,HDF5.Group)
n=length(d)
label.label = n!=1 ? "$n objects" : "1 object"
else
label.label = repr(size(d))
end
end
end

type_factory = GtkSignalListItemFactory(list_setup_cb, type_bind_cb)
size_factory = GtkSignalListItemFactory(list_setup_cb, size_bind_cb)

search_entry = b["search_entry"]::GtkSearchEntryLeaf # controls the filter
spinner = b["spinner"]::GtkSpinnerLeaf # spins during possibly long render operations

function match(row::GtkTreeListRow)
entrytext = Gtk4.text(GtkEditable(search_entry))
if entrytext == "" || entrytext === nothing
return true
end
match_string(row, entrytext)
end

filt = GtkCustomFilter(match)
filteredModel = GtkFilterListModel(GListModel(treemodel), filt)

single_sel = GtkSingleSelection(GLib.GListModel(filteredModel))
sel = GtkSelectionModel(single_sel)
list = b["list"]::GtkColumnViewLeaf
name_column = b["name_column"]::GtkColumnViewColumnLeaf
Gtk4.factory(name_column,factory)
type_column = b["type_column"]::GtkColumnViewColumnLeaf
Gtk4.factory(type_column,type_factory)
size_column = b["size_column"]::GtkColumnViewColumnLeaf
Gtk4.factory(size_column,size_factory)

Gtk4.model(list,sel)

pr = b["paned2"]::GtkPanedLeaf

atv = b["atv"]
dtv = b["dtv"]::GtkTextViewLeaf

function on_selected_changed(selection, position, n_items)
position = Gtk4.G_.get_selected(selection)
row = Gtk4.G_.get_row(treemodel,position)
row!==nothing || return
p = Gtk4.get_item(row).string
start(spinner)
Threads.@spawn h5open(filename,"r") do h
d=h[p]

# show attributes
a=attributes(d)
ls = if length(a)>0
ls=GtkListStore(String, String)
for ak in keys(a)
push!(ls,(ak,string(read(a[ak]))))
end
ls
else
nothing
end

str = _repr(d)
@idle_add begin
render_data(dtv,str,atv,ls,pr)
stop(spinner)
end
end
end

signal_connect(on_selected_changed,sel,"selection_changed")

signal_connect(search_entry, "search-changed") do widget
@idle_add changed(filt, Gtk4.FilterChange_DIFFERENT)
end
show(w)

@idle_add on_selected_changed(single_sel, 0, 1)

nothing
end

end # module
109 changes: 109 additions & 0 deletions examples/HDF5Viewer/src/HDF5Viewer.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkWindow" id="win">
<property name="default-width">800</property>
<property name="default-height">600</property>
<child>
<object class="GtkPaned" id="paned1">
<property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
<property name="position">450</property>
<child>
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
<child>
<object class="GtkLabel">
<property name="label">Filter by name:</property>
</object>
</child>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="hexpand">true</property>
</object>
</child>
<child>
<object class="GtkSpinner" id="spinner">
</object>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow" id="sw">
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkColumnView" id="list">
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkColumnViewColumn" id="name_column">
<property name="title">Name</property>
</object>
</child>
<child>
<object class="GtkColumnViewColumn" id="type_column">
<property name="title">Type</property>
</object>
</child>
<child>
<object class="GtkColumnViewColumn" id="size_column">
<property name="title">Size</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkPaned" id="paned2">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<object class="GtkScrolledWindow">
<child>
<object class="GtkTreeView" id="atv">
<child>
<object class="GtkTreeViewColumn">
<property name="title">Attribute</property>
<child>
<object class="GtkCellRendererText"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="title">Value</property>
<child>
<object class="GtkCellRendererText"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<child>
<object class="GtkTextView" id="dtv">
<property name="editable">false</property>
<property name="wrap-mode">GTK_WRAP_WORD</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>
8 changes: 4 additions & 4 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- `dialogs.jl` demonstrates various types of dialogs.

## Drawing
- `canvas.jl` demonstrates use of `GtkCanvas`, which allows drawing with [Cairo](https://github.com/JuliaGraphics/Cairo.jl). Also shows how to change the cursor when it's over a certain widget.
- `canvas.jl` demonstrates use of `GtkCanvas`, which allows drawing with [Cairo](https://github.com/JuliaGraphics/Cairo.jl). Also shows how to change the cursor when it's over a certain widget. Adapted from an example in the Gtk.jl manual.
- `canvas_cairomakie.jl` shows how to draw a [CairoMakie](https://github.com/MakieOrg/Makie.jl) plot into a `GtkCanvas`.
- `glarea.jl` shows how to use the `GtkGLArea` widget to draw using OpenGL.

Expand All @@ -19,13 +19,13 @@
- `listbox.jl` demonstrates `GtkListBox` to show a huge list of strings. This widget is a little easier to use than `GtkListView` but may be less performant.

## Multithreading

- `thread.jl` is a basic example of doing work in a separate thread while maintaining a responsive UI.
- `thread.jl` is a basic example of doing work in a separate thread while maintaining a responsive UI. Adapted from an example in the Gtk.jl manual.
- `thread_timeout.jl` adds a label that updates during the task in the example above.

## Applications

- `application.jl` is a simple example of using `GtkApplication` and `GAction`s.
- `application2.jl` together with `application.jl` shows how to use remote actions with DBus. This probably only works on Linux.
- The `ExampleApplication` subdirectory shows how to use Gtk4.jl with [PackageCompiler.jl](https://github.com/JuliaLang/PackageCompiler.jl).

## Toys
- `HDF5Viewer` is a more extended example that shows the contents of an HDF5 file. Uses `GtkBuilder`, `GtkColumnView`, and `GtkTextView` and does some work in a separate thread to keep the UI responsive.

0 comments on commit 09929d9

Please sign in to comment.