@@ -0,0 +1,603 @@
# include "gui/window.hpp"
# include <chrono>
# include <filesystem>
# include <sstream>
# include "core/registry.hpp"
# include "core/rng.hpp"
namespace effector {
namespace {
const char * const kImagePatterns [ ] = { " *.png " , " *.jpg " , " *.jpeg " , " *.webp " , " *.bmp " ,
" *.tif " , " *.tiff " , " *.gif " , " *.ppm " , " *.PNG " ,
" *.JPG " , " *.JPEG " , " *.WEBP " } ;
Glib : : RefPtr < Gdk : : Texture > toTexture ( const Image & image ) {
auto bytes = Glib : : Bytes : : create ( image . data ( ) , image . byteSize ( ) ) ;
return Gdk : : MemoryTexture : : create ( image . width ( ) , image . height ( ) , Gdk : : MemoryTexture : : Format : : R8G8B8 ,
bytes , image . stride ( ) ) ;
}
Gtk : : Label * sectionLabel ( const char * text ) {
auto * label = Gtk : : make_managed < Gtk : : Label > ( text ) ;
label - > set_xalign ( 0.0F ) ;
label - > add_css_class ( " heading " ) ;
return label ;
}
} // namespace
EffectorWindow : : EffectorWindow ( const Options & initial ) {
set_title ( " Effector " ) ;
set_default_size ( 1180 , 760 ) ;
effects_ = effects ( ) ;
effect_ = effects_ . front ( ) ;
seed_ = Rng : : randomSeed ( ) ;
renderDone_ . connect ( sigc : : mem_fun ( * this , & EffectorWindow : : onRenderFinished ) ) ;
buildUi ( ) ;
applyInitial ( initial ) ;
suspendRender_ = false ;
if ( ! sources_ - > empty ( ) ) startRender ( ) ;
}
EffectorWindow : : ~ EffectorWindow ( ) {
debounce_ . disconnect ( ) ;
if ( worker_ . joinable ( ) ) worker_ . join ( ) ;
}
// ------------------------------------------------------------------- UI ---
void EffectorWindow : : buildUi ( ) {
auto * header = Gtk : : make_managed < Gtk : : HeaderBar > ( ) ;
set_titlebar ( * header ) ;
auto * addButton = Gtk : : make_managed < Gtk : : Button > ( ) ;
addButton - > set_icon_name ( " list-add-symbolic " ) ;
addButton - > set_tooltip_text ( " Add images " ) ;
addButton - > signal_clicked ( ) . connect ( sigc : : mem_fun ( * this , & EffectorWindow : : onAddImages ) ) ;
header - > pack_start ( * addButton ) ;
std : : vector < Glib : : ustring > titles ;
for ( const auto & effect : effects_ ) titles . push_back ( effect - > title ( ) ) ;
effectDropdown_ . set_model ( Gtk : : StringList : : create ( titles ) ) ;
effectDropdown_ . set_tooltip_text ( " Effect " ) ;
effectDropdown_ . property_selected ( ) . signal_changed ( ) . connect (
sigc : : mem_fun ( * this , & EffectorWindow : : onEffectChanged ) ) ;
header - > pack_start ( effectDropdown_ ) ;
saveButton_ . set_sensitive ( false ) ;
saveButton_ . signal_clicked ( ) . connect ( sigc : : mem_fun ( * this , & EffectorWindow : : onSave ) ) ;
header - > pack_end ( saveButton_ ) ;
recreateButton_ . set_icon_name ( " view-refresh-symbolic " ) ;
recreateButton_ . set_tooltip_text ( " Recreate with a new seed " ) ;
recreateButton_ . signal_clicked ( ) . connect ( sigc : : mem_fun ( * this , & EffectorWindow : : onRecreate ) ) ;
header - > pack_end ( recreateButton_ ) ;
auto * root = Gtk : : make_managed < Gtk : : Box > ( Gtk : : Orientation : : VERTICAL ) ;
set_child ( * root ) ;
banner_ . set_xalign ( 0.0F ) ;
banner_ . set_wrap ( true ) ;
banner_ . set_visible ( false ) ;
banner_ . add_css_class ( " error " ) ;
banner_ . set_margin ( 8 ) ;
root - > append ( banner_ ) ;
auto * paned = Gtk : : make_managed < Gtk : : Paned > ( Gtk : : Orientation : : HORIZONTAL ) ;
paned - > set_vexpand ( true ) ;
paned - > set_position ( 330 ) ;
paned - > set_shrink_start_child ( false ) ;
paned - > set_start_child ( buildSidebar ( ) ) ;
paned - > set_end_child ( buildPreview ( ) ) ;
root - > append ( * paned ) ;
}
Gtk : : Widget & EffectorWindow : : buildSidebar ( ) {
auto * sidebar = Gtk : : make_managed < Gtk : : Box > ( Gtk : : Orientation : : VERTICAL , 12 ) ;
sidebar - > set_margin ( 12 ) ;
sidebar - > append ( * sectionLabel ( " Source images " ) ) ;
sourceList_ . set_selection_mode ( Gtk : : SelectionMode : : NONE ) ;
sourceList_ . add_css_class ( " boxed-list " ) ;
auto * empty = Gtk : : make_managed < Gtk : : Label > ( " No images yet " ) ;
empty - > set_margin_top ( 18 ) ;
empty - > set_margin_bottom ( 18 ) ;
sourceList_ . set_placeholder ( * empty ) ;
auto * scroller = Gtk : : make_managed < Gtk : : ScrolledWindow > ( ) ;
scroller - > set_policy ( Gtk : : PolicyType : : NEVER , Gtk : : PolicyType : : AUTOMATIC ) ;
scroller - > set_child ( sourceList_ ) ;
scroller - > set_size_request ( - 1 , 170 ) ;
scroller - > set_vexpand ( true ) ;
sidebar - > append ( * scroller ) ;
sidebar - > append ( * sectionLabel ( " Canvas " ) ) ;
auto * grid = Gtk : : make_managed < Gtk : : Grid > ( ) ;
grid - > set_column_spacing ( 8 ) ;
grid - > set_row_spacing ( 6 ) ;
widthSpin_ . set_range ( 1 , 20000 ) ;
widthSpin_ . set_increments ( 1 , 100 ) ;
widthSpin_ . set_value ( 1920 ) ;
heightSpin_ . set_range ( 1 , 20000 ) ;
heightSpin_ . set_increments ( 1 , 100 ) ;
heightSpin_ . set_value ( 1080 ) ;
for ( auto * spin : { & widthSpin_ , & heightSpin_ } ) {
spin - > set_hexpand ( true ) ;
spin - > signal_value_changed ( ) . connect ( sigc : : mem_fun ( * this , & EffectorWindow : : onParamChanged ) ) ;
}
auto * widthLabel = Gtk : : make_managed < Gtk : : Label > ( " Width " ) ;
widthLabel - > set_xalign ( 0.0F ) ;
auto * heightLabel = Gtk : : make_managed < Gtk : : Label > ( " Height " ) ;
heightLabel - > set_xalign ( 0.0F ) ;
grid - > attach ( * widthLabel , 0 , 0 ) ;
grid - > attach ( widthSpin_ , 1 , 0 ) ;
grid - > attach ( * heightLabel , 0 , 1 ) ;
grid - > attach ( heightSpin_ , 1 , 1 ) ;
sidebar - > append ( * grid ) ;
sidebar - > append ( * sectionLabel ( " Effect settings " ) ) ;
paramGrid_ . set_column_spacing ( 8 ) ;
paramGrid_ . set_row_spacing ( 6 ) ;
sidebar - > append ( paramGrid_ ) ;
buildParamWidgets ( ) ;
auto * seedBox = Gtk : : make_managed < Gtk : : Box > ( Gtk : : Orientation : : HORIZONTAL , 8 ) ;
auto * seedLabel = Gtk : : make_managed < Gtk : : Label > ( " Seed " ) ;
seedLabel - > set_xalign ( 0.0F ) ;
seedBox - > append ( * seedLabel ) ;
seedEntry_ . set_hexpand ( true ) ;
seedEntry_ . set_text ( std : : to_string ( seed_ ) ) ;
seedEntry_ . signal_activate ( ) . connect ( sigc : : mem_fun ( * this , & EffectorWindow : : onSeedEntered ) ) ;
seedBox - > append ( seedEntry_ ) ;
sidebar - > append ( * seedBox ) ;
return * sidebar ;
}
Gtk : : Widget & EffectorWindow : : buildPreview ( ) {
picture_ . set_can_shrink ( true ) ;
picture_ . set_content_fit ( Gtk : : ContentFit : : CONTAIN ) ;
placeholder_ . set_text ( " Add source images to start. " ) ;
placeholder_ . set_wrap ( true ) ;
placeholder_ . add_css_class ( " dim-label " ) ;
previewStack_ . add ( placeholder_ , " placeholder " ) ;
previewStack_ . add ( picture_ , " picture " ) ;
previewStack_ . set_visible_child ( " placeholder " ) ;
spinner_ . set_halign ( Gtk : : Align : : CENTER ) ;
spinner_ . set_valign ( Gtk : : Align : : CENTER ) ;
auto * overlay = Gtk : : make_managed < Gtk : : Overlay > ( ) ;
overlay - > set_hexpand ( true ) ;
overlay - > set_vexpand ( true ) ;
overlay - > set_child ( previewStack_ ) ;
overlay - > add_overlay ( spinner_ ) ;
auto * box = Gtk : : make_managed < Gtk : : Box > ( Gtk : : Orientation : : VERTICAL ) ;
box - > append ( * overlay ) ;
status_ . set_margin_top ( 6 ) ;
status_ . set_margin_bottom ( 8 ) ;
status_ . add_css_class ( " dim-label " ) ;
box - > append ( status_ ) ;
return * box ;
}
void EffectorWindow : : buildParamWidgets ( ) {
while ( auto * child = paramGrid_ . get_first_child ( ) ) paramGrid_ . remove ( * child ) ;
paramWidgets_ . clear ( ) ;
ownedWidgets_ . clear ( ) ;
int row = 0 ;
for ( const auto & spec : effect_ - > params ( ) ) {
auto label = std : : make_unique < Gtk : : Label > ( spec . label ) ;
label - > set_xalign ( 0.0F ) ;
if ( ! spec . help . empty ( ) ) label - > set_tooltip_text ( spec . help ) ;
std : : unique_ptr < Gtk : : Widget > widget ;
switch ( spec . type ) {
case ParamSpec : : Type : : Bool : {
auto toggle = std : : make_unique < Gtk : : Switch > ( ) ;
toggle - > set_halign ( Gtk : : Align : : START ) ;
toggle - > set_active ( spec . defaultValue ! = 0.0 ) ;
toggle - > property_active ( ) . signal_changed ( ) . connect (
sigc : : mem_fun ( * this , & EffectorWindow : : onParamChanged ) ) ;
widget = std : : move ( toggle ) ;
break ;
}
case ParamSpec : : Type : : Int :
case ParamSpec : : Type : : Double : {
auto spin = std : : make_unique < Gtk : : SpinButton > ( ) ;
spin - > set_range ( spec . minimum , spec . maximum ) ;
spin - > set_increments ( spec . step , spec . step * 10 ) ;
spin - > set_digits ( spec . type = = ParamSpec : : Type : : Int ? 0 : spec . digits ) ;
spin - > set_value ( spec . defaultValue ) ;
spin - > signal_value_changed ( ) . connect (
sigc : : mem_fun ( * this , & EffectorWindow : : onParamChanged ) ) ;
widget = std : : move ( spin ) ;
break ;
}
case ParamSpec : : Type : : Choice : {
std : : vector < Glib : : ustring > choices ( spec . choices . begin ( ) , spec . choices . end ( ) ) ;
auto dropdown = std : : make_unique < Gtk : : DropDown > ( ) ;
dropdown - > set_model ( Gtk : : StringList : : create ( choices ) ) ;
for ( std : : size_t i = 0 ; i < spec . choices . size ( ) ; + + i ) {
if ( spec . choices [ i ] = = spec . defaultChoice ) dropdown - > set_selected ( i ) ;
}
dropdown - > property_selected ( ) . signal_changed ( ) . connect (
sigc : : mem_fun ( * this , & EffectorWindow : : onParamChanged ) ) ;
widget = std : : move ( dropdown ) ;
break ;
}
}
widget - > set_hexpand ( true ) ;
paramGrid_ . attach ( * label , 0 , row ) ;
paramGrid_ . attach ( * widget , 1 , row ) ;
paramWidgets_ . push_back ( { spec , widget . get ( ) } ) ;
ownedWidgets_ . push_back ( std : : move ( label ) ) ;
ownedWidgets_ . push_back ( std : : move ( widget ) ) ;
+ + row ;
}
}
ParamValues EffectorWindow : : paramValues ( ) const {
ParamValues values = effect_ - > defaults ( ) ;
for ( const auto & entry : paramWidgets_ ) {
switch ( entry . spec . type ) {
case ParamSpec : : Type : : Bool :
values [ entry . spec . key ] = static_cast < Gtk : : Switch * > ( entry . widget ) - > get_active ( ) ;
break ;
case ParamSpec : : Type : : Int :
values [ entry . spec . key ] =
static_cast < long > ( static_cast < Gtk : : SpinButton * > ( entry . widget ) - > get_value ( ) ) ;
break ;
case ParamSpec : : Type : : Double :
values [ entry . spec . key ] = static_cast < Gtk : : SpinButton * > ( entry . widget ) - > get_value ( ) ;
break ;
case ParamSpec : : Type : : Choice : {
const auto index = static_cast < Gtk : : DropDown * > ( entry . widget ) - > get_selected ( ) ;
if ( index < entry . spec . choices . size ( ) ) values [ entry . spec . key ] = entry . spec . choices [ index ] ;
break ;
}
}
}
return values ;
}
void EffectorWindow : : applyInitial ( const Options & initial ) {
for ( std : : size_t i = 0 ; i < effects_ . size ( ) ; + + i ) {
if ( effects_ [ i ] - > name ( ) = = initial . effect ) effectDropdown_ . set_selected ( i ) ;
}
widthSpin_ . set_value ( initial . width ) ;
heightSpin_ . set_value ( initial . height ) ;
if ( initial . seed ) {
seed_ = * initial . seed ;
seedEntry_ . set_text ( std : : to_string ( seed_ ) ) ;
}
for ( const auto & entry : paramWidgets_ ) {
const auto it = initial . params . find ( entry . spec . key ) ;
if ( it = = initial . params . end ( ) ) continue ;
switch ( entry . spec . type ) {
case ParamSpec : : Type : : Bool :
static_cast < Gtk : : Switch * > ( entry . widget )
- > set_active ( paramBool ( initial . params , entry . spec . key , false ) ) ;
break ;
case ParamSpec : : Type : : Int :
static_cast < Gtk : : SpinButton * > ( entry . widget )
- > set_value ( static_cast < double > ( paramInt ( initial . params , entry . spec . key , 0 ) ) ) ;
break ;
case ParamSpec : : Type : : Double :
static_cast < Gtk : : SpinButton * > ( entry . widget )
- > set_value ( paramDouble ( initial . params , entry . spec . key , 0.0 ) ) ;
break ;
case ParamSpec : : Type : : Choice : {
const auto value = paramChoice ( initial . params , entry . spec . key , entry . spec . defaultChoice ) ;
for ( std : : size_t i = 0 ; i < entry . spec . choices . size ( ) ; + + i ) {
if ( entry . spec . choices [ i ] = = value ) {
static_cast < Gtk : : DropDown * > ( entry . widget ) - > set_selected ( i ) ;
}
}
break ;
}
}
}
if ( ! initial . images . empty ( ) ) addPaths ( initial . images ) ;
}
// -------------------------------------------------------------- sources ---
// Replaces the source snapshot, moving the images across when nothing else
// holds the old list (the common case) and copying only if a render is reading it.
template < typename Mutate >
void EffectorWindow : : mutateSources ( Mutate & & mutate ) {
auto next = std : : make_shared < SourceList > ( ) ;
if ( sources_ . use_count ( ) = = 1 ) {
* next = std : : move ( const_cast < SourceList & > ( * sources_ ) ) ;
} else {
* next = * sources_ ;
}
mutate ( * next ) ;
sources_ = std : : move ( next ) ;
}
void EffectorWindow : : addPaths ( const std : : vector < std : : string > & paths ) {
std : : vector < LoadFailure > failures ;
auto images = loadImages ( paths , failures ) ;
for ( const auto & image : images ) appendSourceRow ( image . path ( ) ) ;
mutateSources ( [ & images ] ( SourceList & list ) {
for ( auto & image : images ) list . push_back ( std : : move ( image ) ) ;
} ) ;
if ( ! failures . empty ( ) ) {
std : : string names ;
for ( const auto & failure : failures ) {
if ( ! names . empty ( ) ) names + = " , " ;
names + = std : : filesystem : : path ( failure . path ) . filename ( ) . string ( ) ;
}
showError ( " Could not read: " + names ) ;
} else if ( ! images . empty ( ) ) {
clearError ( ) ;
}
requestRender ( ) ;
}
void EffectorWindow : : appendSourceRow ( const std : : string & path ) {
auto * row = Gtk : : make_managed < Gtk : : ListBoxRow > ( ) ;
auto * box = Gtk : : make_managed < Gtk : : Box > ( Gtk : : Orientation : : HORIZONTAL , 6 ) ;
box - > set_margin_top ( 4 ) ;
box - > set_margin_bottom ( 4 ) ;
box - > set_margin_start ( 8 ) ;
box - > set_margin_end ( 4 ) ;
auto * label = Gtk : : make_managed < Gtk : : Label > ( std : : filesystem : : path ( path ) . filename ( ) . string ( ) ) ;
label - > set_xalign ( 0.0F ) ;
label - > set_hexpand ( true ) ;
label - > set_ellipsize ( Pango : : EllipsizeMode : : END ) ;
label - > set_tooltip_text ( path ) ;
box - > append ( * label ) ;
auto * remove = Gtk : : make_managed < Gtk : : Button > ( ) ;
remove - > set_icon_name ( " window-close-symbolic " ) ;
remove - > add_css_class ( " flat " ) ;
remove - > signal_clicked ( ) . connect ( [ this , row ] { removeSource ( row ) ; } ) ;
box - > append ( * remove ) ;
row - > set_child ( * box ) ;
sourceList_ . append ( * row ) ;
}
void EffectorWindow : : removeSource ( Gtk : : ListBoxRow * row ) {
const int index = row - > get_index ( ) ;
mutateSources ( [ index ] ( SourceList & list ) {
if ( index > = 0 & & static_cast < std : : size_t > ( index ) < list . size ( ) ) {
list . erase ( list . begin ( ) + index ) ;
}
} ) ;
sourceList_ . remove ( * row ) ;
if ( ! sources_ - > empty ( ) ) {
requestRender ( ) ;
} else {
result_ = Image ( ) ;
saveButton_ . set_sensitive ( false ) ;
previewStack_ . set_visible_child ( " placeholder " ) ;
status_ . set_text ( " " ) ;
}
}
void EffectorWindow : : onAddImages ( ) {
auto dialog = Gtk : : FileDialog : : create ( ) ;
dialog - > set_title ( " Select source images " ) ;
auto filter = Gtk : : FileFilter : : create ( ) ;
filter - > set_name ( " Images " ) ;
for ( const char * pattern : kImagePatterns ) filter - > add_pattern ( pattern ) ;
auto filters = Gio : : ListStore < Gtk : : FileFilter > : : create ( ) ;
filters - > append ( filter ) ;
dialog - > set_filters ( filters ) ;
dialog - > set_default_filter ( filter ) ;
dialog - > open_multiple ( * this , [ this , dialog ] ( const Glib : : RefPtr < Gio : : AsyncResult > & result ) {
onImagesChosen ( result , dialog ) ;
} ) ;
}
void EffectorWindow : : onImagesChosen ( const Glib : : RefPtr < Gio : : AsyncResult > & result ,
const Glib : : RefPtr < Gtk : : FileDialog > & dialog ) {
std : : vector < std : : string > paths ;
try {
for ( const auto & file : dialog - > open_multiple_finish ( result ) ) {
if ( file ) paths . push_back ( file - > get_path ( ) ) ;
}
} catch ( const Glib : : Error & ) {
return ; // cancelled
}
if ( ! paths . empty ( ) ) addPaths ( paths ) ;
}
// ------------------------------------------------------------ rendering ---
void EffectorWindow : : onEffectChanged ( ) {
const auto index = effectDropdown_ . get_selected ( ) ;
if ( index > = effects_ . size ( ) ) return ;
effect_ = effects_ [ index ] ;
buildParamWidgets ( ) ;
requestRender ( ) ;
}
void EffectorWindow : : onParamChanged ( ) { requestRender ( ) ; }
void EffectorWindow : : onSeedEntered ( ) {
try {
seed_ = std : : stoull ( seedEntry_ . get_text ( ) ) ;
} catch ( const std : : exception & ) {
seedEntry_ . set_text ( std : : to_string ( seed_ ) ) ;
return ;
}
requestRender ( ) ;
}
void EffectorWindow : : onRecreate ( ) {
seed_ = Rng : : randomSeed ( ) ;
seedEntry_ . set_text ( std : : to_string ( seed_ ) ) ;
if ( sources_ - > empty ( ) ) {
showError ( " Add at least one source image first. " ) ;
return ;
}
requestRender ( ) ;
}
void EffectorWindow : : requestRender ( ) {
if ( suspendRender_ | | sources_ - > empty ( ) ) return ;
// Spin buttons fire on every step while they are held down; coalesce those
// into one render instead of queueing a job per tick.
debounce_ . disconnect ( ) ;
debounce_ = Glib : : signal_timeout ( ) . connect (
[ this ] {
startRender ( ) ;
return false ;
} ,
120 ) ;
}
void EffectorWindow : : startRender ( ) {
if ( sources_ - > empty ( ) ) return ;
if ( rendering_ ) {
staleResult_ = true ; // a fresh render follows this one
return ;
}
rendering_ = true ;
const auto sources = sources_ ;
const auto effect = effect_ ;
const int width = static_cast < int > ( widthSpin_ . get_value ( ) ) ;
const int height = static_cast < int > ( heightSpin_ . get_value ( ) ) ;
const std : : uint64_t seed = seed_ ;
const ParamValues values = paramValues ( ) ;
spinner_ . start ( ) ;
recreateButton_ . set_sensitive ( false ) ;
worker_ = std : : thread ( [ this , sources , effect , width , height , seed , values ] {
Image image ;
std : : string error ;
const auto start = std : : chrono : : steady_clock : : now ( ) ;
try {
Rng rng ( seed ) ;
image = effect - > render ( * sources , width , height , rng , values ) ;
} catch ( const std : : exception & exception ) {
error = exception . what ( ) ;
}
const auto millis = std : : chrono : : duration < double , std : : milli > (
std : : chrono : : steady_clock : : now ( ) - start )
. count ( ) ;
{
std : : lock_guard < std : : mutex > lock ( resultMutex_ ) ;
pendingImage_ = std : : move ( image ) ;
pendingError_ = std : : move ( error ) ;
pendingMillis_ = millis ;
}
renderDone_ . emit ( ) ;
} ) ;
}
void EffectorWindow : : onRenderFinished ( ) {
if ( worker_ . joinable ( ) ) worker_ . join ( ) ;
rendering_ = false ;
Image image ;
std : : string error ;
double millis = 0.0 ;
{
std : : lock_guard < std : : mutex > lock ( resultMutex_ ) ;
image = std : : move ( pendingImage_ ) ;
error = pendingError_ ;
millis = pendingMillis_ ;
}
if ( staleResult_ ) { // settings changed while this one was running
staleResult_ = false ;
startRender ( ) ;
return ;
}
spinner_ . stop ( ) ;
recreateButton_ . set_sensitive ( true ) ;
if ( ! error . empty ( ) ) {
result_ = Image ( ) ;
saveButton_ . set_sensitive ( false ) ;
showError ( error ) ;
return ;
}
clearError ( ) ;
result_ = std : : move ( image ) ;
picture_ . set_paintable ( toTexture ( result_ ) ) ;
previewStack_ . set_visible_child ( " picture " ) ;
saveButton_ . set_sensitive ( true ) ;
std : : ostringstream status ;
status . setf ( std : : ios : : fixed ) ;
status . precision ( 1 ) ;
status < < effect_ - > title ( ) < < " · " < < result_ . width ( ) < < " × " < < result_ . height ( ) < < " · "
< < sources_ - > size ( ) < < ( sources_ - > size ( ) = = 1 ? " source " : " sources " ) < < " · seed "
< < seed_ < < " · " < < millis < < " ms " ;
status_ . set_text ( status . str ( ) ) ;
}
// --------------------------------------------------------------- saving ---
void EffectorWindow : : onSave ( ) {
if ( result_ . empty ( ) ) return ;
auto dialog = Gtk : : FileDialog : : create ( ) ;
dialog - > set_title ( " Save image " ) ;
dialog - > set_initial_name ( " effector- " + effect_ - > name ( ) + " - " + std : : to_string ( seed_ ) + " .png " ) ;
dialog - > save ( * this , [ this , dialog ] ( const Glib : : RefPtr < Gio : : AsyncResult > & result ) {
onSaveChosen ( result , dialog ) ;
} ) ;
}
void EffectorWindow : : onSaveChosen ( const Glib : : RefPtr < Gio : : AsyncResult > & result ,
const Glib : : RefPtr < Gtk : : FileDialog > & dialog ) {
std : : filesystem : : path path ;
try {
const auto file = dialog - > save_finish ( result ) ;
if ( ! file ) return ;
path = file - > get_path ( ) ;
} catch ( const Glib : : Error & ) {
return ; // cancelled
}
if ( path . extension ( ) . empty ( ) ) path . replace_extension ( " .png " ) ;
try {
result_ . save ( path . string ( ) ) ;
} catch ( const std : : exception & error ) {
showError ( std : : string ( " Could not save: " ) + error . what ( ) ) ;
return ;
}
clearError ( ) ;
status_ . set_text ( " Saved " + path . string ( ) ) ;
}
// --------------------------------------------------------------- errors ---
void EffectorWindow : : showError ( const std : : string & message ) {
banner_ . set_text ( message ) ;
banner_ . set_visible ( true ) ;
}
void EffectorWindow : : clearError ( ) { banner_ . set_visible ( false ) ; }
} // namespace effector