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