1/*
2* Copyright (c) 2018 (https://github.com/phase1geo/Minder)
3*
4* This program is free software; you can redistribute it and/or
5* modify it under the terms of the GNU General Public
6* License as published by the Free Software Foundation; either
7* version 2 of the License, or (at your option) any later version.
8*
9* This program is distributed in the hope that it will be useful,
10* but WITHOUT ANY WARRANTY; without even the implied warranty of
11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12* General Public License for more details.
13*
14* You should have received a copy of the GNU General Public
15* License along with this program; if not, write to the
16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17* Boston, MA 02110-1301 USA
18*
19* Authored by: Trevor Williams <phase1geo@gmail.com>
20*/
21
22using Gtk;
23
24public class MapInspector : Box {
25
26  private MainWindow                  _win;
27  private DrawArea?                   _da             = null;
28  private GLib.Settings               _settings;
29  private Granite.Widgets.ModeButton? _layouts        = null;
30  private Grid?                       _theme_grid     = null;
31  private Button?                     _balance        = null;
32  private Button?                     _fold_completed = null;
33  private Button?                     _unfold_all     = null;
34
35  public MapInspector( MainWindow win, GLib.Settings settings ) {
36
37    Object( orientation:Orientation.VERTICAL, spacing:10 );
38
39    _win      = win;
40    _settings = settings;
41
42    /* Create the interface */
43    add_connection_ui();
44    add_link_color_ui();
45    add_layout_ui();
46    add_theme_ui();
47    add_button_ui();
48
49    /* Listen for changes to the current tab */
50    win.canvas_changed.connect( tab_changed );
51    win.themes.themes_changed.connect( update_themes );
52
53    /* Listen for preference changes */
54    _settings.changed.connect( settings_changed );
55
56#if GRANITE_6_OR_LATER
57    /* Listen for changes to the system dark mode */
58    var granite_settings = Granite.Settings.get_default();
59    granite_settings.notify["prefers-color-scheme"].connect( () => {
60      update_themes();
61    });
62#endif
63
64  }
65
66  /* Listen for any changes to the current tab in the main window */
67  private void tab_changed( DrawArea? da ) {
68    if( _da != null ) {
69      _da.loaded.disconnect( update_theme_layout );
70      _da.current_changed.disconnect( current_changed );
71    }
72    if( da != null ) {
73      da.loaded.connect( update_theme_layout );
74      da.current_changed.connect( current_changed );
75    }
76    _da = da;
77    _da.animator.enable        = _settings.get_boolean( "enable-animations" );
78    _da.get_connections().hide = _settings.get_boolean( "hide-connections" );
79    _da.set_theme( _da.get_theme(), false );
80    update_theme_layout();
81  }
82
83  /*
84   Called whenever the preferences change values.  We will update the displayed
85   themes based on the hide setting.
86  */
87  private void settings_changed( string key ) {
88    switch( key ) {
89      case "hide-themes-not-matching-visual-style" :  update_themes();  break;
90    }
91  }
92
93  /* Add the connection show/hide UI */
94  private void add_connection_ui() {
95
96    var box       = new Box( Orientation.HORIZONTAL, 0 );
97    var lbl       = new Label( Utils.make_title( _( "Hide connections" ) ) );
98    var hide_conn = _settings.get_boolean( "hide-connections" );
99
100    lbl.xalign = (float)0;
101    lbl.use_markup = true;
102
103    var hide_connections = new Switch();
104    hide_connections.set_active( hide_conn );
105    hide_connections.button_release_event.connect( hide_connections_changed );
106
107    box.pack_start( lbl,              false, true, 0 );
108    box.pack_end(   hide_connections, false, true, 0 );
109
110    pack_start( box, false, true );
111
112  }
113
114  /* Called whenever the hide connections switch is changed within the inspector */
115  private bool hide_connections_changed( Gdk.EventButton e ) {
116    _da.get_connections().hide = !_da.get_connections().hide;
117    _settings.set_boolean( "hide-connections", _da.get_connections().hide );
118    _da.queue_draw();
119    return( false );
120  }
121
122  /* Add link color rotation UI */
123  private void add_link_color_ui() {
124
125    var box    = new Box( Orientation.HORIZONTAL, 0 );
126    var lbl    = new Label( Utils.make_title( _( "Rotate main branch colors" ) ) );
127    var rotate = _settings.get_boolean( "rotate-main-link-colors" );
128
129    lbl.xalign     = (float)0;
130    lbl.use_markup = true;
131
132    var rotate_colors = new Switch();
133    rotate_colors.set_active( rotate );
134    rotate_colors.button_release_event.connect( rotate_colors_changed );
135
136    box.pack_start( lbl,           false, true, 0 );
137    box.pack_end(   rotate_colors, false, true, 0 );
138
139    pack_start( box, false, true );
140
141  }
142
143  /* Called whenever the rotate color switch is changed within the inspector */
144  private bool rotate_colors_changed( Gdk.EventButton e ) {
145    _da.get_theme().rotate = !_da.get_theme().rotate;
146    _settings.set_boolean( "rotate-main-link-colors", _da.get_theme().rotate );
147    return( false );
148  }
149
150  /* Adds the layout UI */
151  private void add_layout_ui() {
152
153    var icons   = new Array<string>();
154    var layouts = new Layouts();
155    layouts.get_icons( ref icons );
156
157    /* Create the modebutton to select the current layout */
158    var lbl = new Label( Utils.make_title( _( "Node Layouts" ) ) );
159    lbl.xalign = (float)0;
160    lbl.use_markup = true;
161
162    /* Create the layouts mode button */
163    _layouts = new Granite.Widgets.ModeButton();
164    _layouts.has_tooltip = true;
165    for( int i=0; i<icons.length; i++ ) {
166      _layouts.append_icon( icons.index( i ), IconSize.SMALL_TOOLBAR );
167    }
168    _layouts.button_release_event.connect( layout_changed );
169    _layouts.query_tooltip.connect( layout_show_tooltip );
170
171    pack_start( lbl,      false, true );
172    pack_start( _layouts, false, true );
173
174  }
175
176  /* Called whenever the user changes the current layout */
177  private bool layout_changed( Gdk.EventButton e ) {
178    var names = new Array<string>();
179    _da.layouts.get_names( ref names );
180    if( _layouts.selected < names.length ) {
181      var   name   = names.index( _layouts.selected );
182      var   layout = _da.layouts.get_layout( name );
183      Node? node   = _da.get_current_node();
184      _da.set_layout( name, ((node == null) ? null : node.get_root()) );
185      _balance.set_sensitive( layout.balanceable );
186    }
187    return( false );
188  }
189
190  /* Called whenever the tooltip needs to be displayed for the layout selector */
191  private bool layout_show_tooltip( int x, int y, bool keyboard, Tooltip tooltip ) {
192    if( keyboard ) {
193      return( false );
194    }
195    var names = new Array<string>();
196    _da.layouts.get_names( ref names );
197    int button_width = (int)(_layouts.get_allocated_width() / names.length);
198    if( (x / button_width) < names.length ) {
199      tooltip.set_text( names.index( x / button_width ) );
200      return( true );
201    }
202    return( false );
203  }
204
205  /* Adds the themes UI */
206  private void add_theme_ui() {
207
208    /* Create the UI */
209    var lbl = new Label( Utils.make_title( _( "Themes" ) ) );
210    lbl.xalign = (float)0;
211    lbl.use_markup = true;
212
213    var sw  = new ScrolledWindow( null, null );
214    var vp  = new Viewport( null, null );
215    var tb  = new Box( Orientation.VERTICAL, 0 );
216    _theme_grid = new Grid();
217    _theme_grid.column_homogeneous = true;
218    tb.pack_start( _theme_grid, true, true );
219    vp.set_size_request( 200, 600 );
220    vp.add( tb );
221    sw.add( vp );
222
223    /* Add the themes to the theme box */
224    update_themes();
225
226    var add = new Button.from_icon_name( "list-add-symbolic", IconSize.LARGE_TOOLBAR );
227    add.relief = ReliefStyle.NONE;
228    add.set_tooltip_text( _( "Add Custom Theme" ) );
229    add.clicked.connect( create_custom_theme );
230    tb.pack_start( add, false, true );
231
232    /* Pack the panel */
233    pack_start( lbl, false, true );
234    pack_start( sw,  true,  true );
235
236  }
237
238  /* Adds the bottom button frame */
239  private void add_button_ui() {
240
241    var grid = new Grid();
242    grid.column_homogeneous = true;
243    grid.column_spacing     = 5;
244    grid.row_spacing        = 5;
245
246    _balance = new Button.from_icon_name( "minder-balance-symbolic", IconSize.SMALL_TOOLBAR );
247    _balance.set_tooltip_text( _( "Balance Nodes" ) );
248    _balance.clicked.connect(() => {
249      _da.balance_nodes( true, true );
250    });
251
252    _fold_completed = new Button.from_icon_name( "minder-fold-completed-symbolic", IconSize.SMALL_TOOLBAR );
253    _fold_completed.set_tooltip_text( _( "Fold Completed Tasks" ) );
254    _fold_completed.clicked.connect(() => {
255      _da.fold_completed_tasks();
256    });
257
258    _unfold_all = new Button.from_icon_name( "minder-unfold-symbolic", IconSize.SMALL_TOOLBAR );
259    _unfold_all.set_tooltip_text( _( "Unfold All Nodes" ) );
260    _unfold_all.clicked.connect(() => {
261      _da.unfold_all_nodes();
262    });
263
264    grid.attach( _balance,        0, 0 );
265    grid.attach( _fold_completed, 1, 0 );
266    grid.attach( _unfold_all,     2, 0 );
267
268    pack_start( grid, false, true );
269
270  }
271
272  /* Updates the theme box widget with the current list of themes */
273  private void update_themes() {
274
275    /* Clear the contents of the theme box */
276    _theme_grid.get_children().foreach((entry) => {
277      _theme_grid.remove( entry );
278    });
279
280    /* Get the theme information to display */
281    var names    = new Array<string>();
282    var icons    = new Array<Gtk.Image>();
283#if GRANITE_6_OR_LATER
284    var hide     = _settings.get_boolean( "hide-themes-not-matching-visual-style" );
285    var settings = Granite.Settings.get_default();
286    var dark     = settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK;
287#else
288    var hide     = false;
289    var dark     = false;
290#endif
291
292    _win.themes.names( ref names );
293    _win.themes.icons( ref icons );
294
295    /* Add the themes */
296    var index = 0;
297    for( int i=0; i<names.length; i++ ) {
298      var name  = names.index( i );
299      var theme = _win.themes.get_theme( name );
300      if( !hide || (dark == theme.prefer_dark) ) {
301        var ebox  = new EventBox();
302        var item  = new Box( Orientation.VERTICAL, 0 );
303        var label = new Label( theme_label( name ) );
304        item.border_width = 5;
305        item.pack_start( icons.index( i ), false, false );
306        item.pack_start( label,            false, true, 5 );
307        ebox.button_press_event.connect((w, e) => {
308          select_theme( name );
309          _da.set_theme( theme, true );
310          if( theme.custom && (e.type == Gdk.EventType.DOUBLE_BUTTON_PRESS) ) {
311            edit_current_theme();
312          }
313          return( false );
314        });
315        ebox.add( item );
316        _theme_grid.attach( ebox, (index % 2), (index / 2) );
317        index++;
318      }
319    }
320    _theme_grid.show_all();
321
322    /* Make sure that the current theme is selected */
323    if( _da != null ) {
324      select_theme( _da.get_theme_name() );
325    }
326
327  }
328
329  /* Sets the map inspector UI to match the given layout name */
330  private void select_layout( string name ) {
331
332    /* Set the layout button to the matching value */
333    if( name == _( "Manual" ) ) { _layouts.selected = 0; }
334    else if( name == _( "Vertical" )   ) { _layouts.selected = 1; }
335    else if( name == _( "Horizontal" ) ) { _layouts.selected = 2; }
336    else if( name == _( "To left" )    ) { _layouts.selected = 3; }
337    else if( name == _( "To right" )   ) { _layouts.selected = 4; }
338    else if( name == _( "Upwards" )    ) { _layouts.selected = 5; }
339    else if( name == _( "Downwards" )  ) { _layouts.selected = 6; }
340
341    /* Set the sensitivity of the Balance Nodes button */
342    _balance.set_sensitive( _da.layouts.get_layout( name ).balanceable );
343
344  }
345
346  /* Returns the label to use for the given theme by name */
347  private string theme_label( string name ) {
348    var theme = _win.themes.get_theme( name );
349    if( theme.temporary ) {
350      return( theme.label + " (" + _( "Unsaved" ) + ")" );
351    }
352    return( theme.label );
353  }
354
355  /* Makes sure that only the given theme is selected in the UI */
356  private void select_theme( string name ) {
357
358    int index    = 0;
359    var names    = new Array<string>();
360    var shown    = new Array<string>();
361    var children = _theme_grid.get_children();
362#if GRANITE_6_OR_LATER
363    var hide     = _settings.get_boolean( "hide-themes-not-matching-visual-style" );
364    var settings = Granite.Settings.get_default();
365    var dark     = settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK;
366#else
367    var hide     = false;
368    var dark     = false;
369#endif
370
371    _win.themes.names( ref names );
372
373    /* Only show the names that are not hidden */
374    for( int i=0; i<names.length; i++ ) {
375      var tname = names.index( i );
376      var theme = _win.themes.get_theme( tname );
377      if( !hide || (dark == theme.prefer_dark) ) {
378        shown.append_val( tname );
379      }
380    }
381
382    children.reverse();
383
384    /* Deselect all themes */
385    children.foreach((entry) => {
386      var e = (EventBox)entry;
387      var b = (Box)e.get_children().nth_data( 0 );
388      var l = (Label)b.get_children().nth_data( 1 );
389      e.get_style_context().remove_class( "theme-selected" );
390      l.set_markup( theme_label( shown.index( index ) ) );
391      index++;
392    });
393
394    /* Select the matching theme */
395    index = 0;
396    children.foreach((entry) => {
397      if( shown.index( index ) == name ) {
398        var e = (EventBox)entry;
399        var b = (Box)e.get_children().nth_data( 0 );
400        var l = (Label)b.get_children().nth_data( 1 );
401        e.get_style_context().add_class( "theme-selected" );
402        l.set_markup( "<span color=\"white\">%s</span>".printf( theme_label( shown.index( index ) ) ) );
403      }
404      index++;
405    });
406
407  }
408
409  private void update_theme_layout() {
410
411    /* Make sure the current theme is selected */
412    select_theme( _da.get_theme_name() );
413
414    /* Initialize the button states */
415    current_changed();
416
417  }
418
419  /* Displays the current theme editor */
420  private void create_custom_theme() {
421    _win.show_theme_editor( false );
422  }
423
424  /* Displays the current theme editor */
425  private void edit_current_theme() {
426    _win.show_theme_editor( true );
427  }
428
429  /* Called whenever the current item is changed */
430  private void current_changed() {
431
432    Node? current         = _da.get_current_node();
433    var   foldable        = _da.completed_tasks_foldable();
434    var   unfoldable      = _da.unfoldable();
435    bool  layout_selected = false;
436
437    /* Select the layout that corresponds with the current tree */
438    if( current != null ) {
439      if( layout_selected = (current.layout != null) ) {
440        select_layout( current.layout.name );
441      }
442    } else if( _da.get_nodes().length > 0 ) {
443      if( layout_selected = (_da.get_nodes().index( 0 ).layout != null) ) {
444        select_layout( _da.get_nodes().index( 0 ).layout.name );
445      }
446    }
447
448    if( !layout_selected ) {
449      select_layout( _da.layouts.get_default().name );
450    }
451
452    /* Update the sensitivity of the buttons */
453    _fold_completed.set_sensitive( foldable );
454    _unfold_all.set_sensitive( unfoldable );
455
456  }
457
458  /* Grabs input focus on the first UI element */
459  public void grab_first() {
460    _layouts.grab_focus();
461  }
462
463}
464