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; 23using Gdk; 24using Granite.Widgets; 25 26public class NodeInspector : Box { 27 28 private const Gtk.TargetEntry[] DRAG_TARGETS = { 29 {"text/uri-list", 0, 0} 30 }; 31 32 private ScrolledWindow _sw; 33 private Switch _task; 34 private Switch _fold; 35 private Revealer _link_reveal; 36 private ColorButton _link_color; 37 private NoteView _note; 38 private DrawArea? _da = null; 39 private Button _detach_btn; 40 private string _orig_note = ""; 41 private Node? _node = null; 42 private Stack _image_stack; 43 private Image _image; 44 private Button _image_btn; 45 private Label _image_loc; 46 47 public NodeInspector( MainWindow win ) { 48 49 Object( orientation:Orientation.VERTICAL, spacing:0 ); 50 51 /* Create the node widgets */ 52 create_task(); 53 create_fold(); 54 create_link(); 55 create_note(); 56 create_image(); 57 create_buttons(); 58 59 show_all(); 60 61 win.canvas_changed.connect( tab_changed ); 62 63 } 64 65 /* Called whenever the user clicks on a tab in the tab bar */ 66 private void tab_changed( DrawArea? da ) { 67 if( _da != null ) { 68 _da.current_changed.disconnect( node_changed ); 69 _da.theme_changed.disconnect( theme_changed ); 70 } 71 _da = da; 72 if( da != null ) { 73 da.current_changed.connect( node_changed ); 74 da.theme_changed.connect( theme_changed ); 75 node_changed(); 76 } 77 } 78 79 /* Sets the width of this inspector to the given value */ 80 public void set_width( int width ) { 81 _sw.width_request = width; 82 } 83 84 /* Creates the task UI elements */ 85 private void create_task() { 86 87 var lbl = new Label( Utils.make_title( _( "Task" ) ) ); 88 lbl.xalign = (float)0; 89 lbl.use_markup = true; 90 91 _task = new Switch(); 92 _task.button_release_event.connect( task_changed ); 93 94 var box = new Box( Orientation.HORIZONTAL, 0 ); 95 box.pack_start( lbl, false, true, 0 ); 96 box.pack_end( _task, false, true, 0 ); 97 98 pack_start( box, false, true, 5 ); 99 100 } 101 102 /* Creates the fold UI elements */ 103 private void create_fold() { 104 105 var lbl = new Label( Utils.make_title( _( "Fold" ) ) ); 106 lbl.xalign = (float)0; 107 lbl.use_markup = true; 108 109 _fold = new Switch(); 110 _fold.button_release_event.connect( fold_changed ); 111 112 var box = new Box( Orientation.HORIZONTAL, 0 ); 113 box.pack_start( lbl, false, true, 0 ); 114 box.pack_end( _fold, false, true, 0 ); 115 116 pack_start( box, false, true, 5 ); 117 118 } 119 120 /* 121 Allows the user to select a different color for the current link 122 and tree. 123 */ 124 private void create_link() { 125 126 var lbl = new Label( Utils.make_title( _( "Color" ) ) ); 127 lbl.xalign = (float)0; 128 lbl.use_markup = true; 129 130 _link_color = new ColorButton(); 131 _link_color.color_set.connect(() => { 132 _da.change_current_link_color( _link_color.rgba ); 133 }); 134 135 var box = new Box( Orientation.HORIZONTAL, 0 ); 136 box.homogeneous = true; 137 box.margin_top = 5; 138 box.margin_bottom = 5; 139 box.pack_start( lbl, false, true, 0 ); 140 box.pack_end( _link_color, true, true, 0 ); 141 142 _link_reveal = new Revealer(); 143 _link_reveal.transition_type = RevealerTransitionType.NONE; 144 _link_reveal.add( box ); 145 146 pack_start( _link_reveal, false, true ); 147 148 } 149 150 /* Creates the note widget */ 151 private void create_note() { 152 153 Label lbl = new Label( Utils.make_title( _( "Note" ) ) ); 154 155 lbl.xalign = (float)0; 156 lbl.use_markup = true; 157 158 _note = new NoteView(); 159 _note.set_wrap_mode( Gtk.WrapMode.WORD ); 160 _note.buffer.text = ""; 161 _note.buffer.changed.connect( note_changed ); 162 _note.focus_in_event.connect( note_focus_in ); 163 _note.focus_out_event.connect( note_focus_out ); 164 165 _sw = new ScrolledWindow( null, null ); 166 _sw.min_content_width = 300; 167 _sw.min_content_height = 100; 168 _sw.add( _note ); 169 170 var box = new Box( Orientation.VERTICAL, 0 ); 171 box.pack_start( lbl, false, false ); 172 box.pack_start( _sw, true, true ); 173 174 pack_start( box, true, true, 5 ); 175 176 } 177 178 private void create_image() { 179 180 _image_stack = new Stack(); 181 _image_stack.transition_type = StackTransitionType.NONE; 182 _image_stack.homogeneous = false; 183 _image_stack.add_named( create_image_add(), "add" ); 184 _image_stack.add_named( create_image_edit(), "edit" ); 185 186 pack_start( _image_stack, false, true, 5 ); 187 188 } 189 190 /* Creates the add image widget */ 191 private Box create_image_add() { 192 193 var lbl = new Label( Utils.make_title( _( "Image" ) ) ); 194 lbl.xalign = (float)0; 195 lbl.use_markup = true; 196 197 var btn = new Button.with_label( _( "Add Image…" ) ); 198 btn.clicked.connect( image_button_clicked ); 199 200 var box = new Box( Orientation.VERTICAL, 10 ); 201 box.pack_start( lbl, false, false ); 202 box.pack_start( btn, false, true ); 203 204 return( box ); 205 206 } 207 208 /* Creates the edit image widget */ 209 private Box create_image_edit() { 210 211 var lbl = new Label( Utils.make_title( _( "Image" ) ) ); 212 lbl.xalign = (float)0; 213 lbl.use_markup = true; 214 215 var btn_edit = new Button.from_icon_name( "document-edit-symbolic" ); 216 btn_edit.set_tooltip_text( _( "Edit Image" ) ); 217 btn_edit.clicked.connect(() => { 218 _da.edit_current_image(); 219 }); 220 221 var btn_del = new Button.from_icon_name( "edit-delete-symbolic" ); 222 btn_del.set_tooltip_text( _( "Remove Image" ) ); 223 btn_del.clicked.connect(() => { 224 _da.delete_current_image(); 225 }); 226 227 var image_btn_box = new Box( Orientation.HORIZONTAL, 10 ); 228 image_btn_box.pack_start( btn_edit, false, false ); 229 image_btn_box.pack_start( btn_del, false, false ); 230 231 var tbox = new Box( Orientation.HORIZONTAL, 10 ); 232 tbox.pack_start( lbl, false, false ); 233 tbox.pack_end( image_btn_box, false, false ); 234 235 _image = new Image(); 236 237 _image_loc = new Label( "" ); 238 _image_loc.use_markup = true; 239 _image_loc.wrap = true; 240 _image_loc.max_width_chars = 40; 241 _image_loc.activate_link.connect( image_link_clicked ); 242 243 var box = new Box( Orientation.VERTICAL, 10 ); 244 box.pack_start( tbox, false, false ); 245 box.pack_start( _image, true, true ); 246 box.pack_start( _image_loc, false, true ); 247 248 /* Set ourselves up to be a drag target */ 249 Gtk.drag_dest_set( _image, DestDefaults.MOTION | DestDefaults.DROP, DRAG_TARGETS, Gdk.DragAction.COPY ); 250 251 _image.drag_data_received.connect((ctx, x, y, data, info, t) => { 252 if( data.get_uris().length == 1 ) { 253 if( _da.update_current_image( data.get_uris()[0] ) ) { 254 Gtk.drag_finish( ctx, true, false, t ); 255 } 256 } 257 }); 258 259 return( box ); 260 261 } 262 263 /* Called when the user clicks on the image button */ 264 private void image_button_clicked() { 265 266 _da.add_current_image(); 267 268 } 269 270 /* Called if the user clicks on the image URI */ 271 private bool image_link_clicked( string uri ) { 272 273 File file = File.new_for_uri( uri ); 274 275 /* If the URI is a file on the local filesystem, view it with the Files app */ 276 if( file.get_uri_scheme() == "file" ) { 277 var files = AppInfo.get_default_for_type( "inode/directory", true ); 278 var list = new List<File>(); 279 list.append( file ); 280 try { 281 files.launch( list, null ); 282 } catch( Error e ) { 283 return( false ); 284 } 285 return( true ); 286 } 287 288 return( false ); 289 290 } 291 292 /* Creates the node editing button grid and adds it to the popover */ 293 private void create_buttons() { 294 295 var grid = new Grid(); 296 grid.column_homogeneous = true; 297 grid.column_spacing = 5; 298 299 var copy_btn = new Button.from_icon_name( "edit-copy-symbolic", IconSize.SMALL_TOOLBAR ); 300 copy_btn.set_tooltip_text( _( "Copy Node To Clipboard" ) ); 301 copy_btn.clicked.connect( node_copy ); 302 303 var cut_btn = new Button.from_icon_name( "edit-cut-symbolic", IconSize.SMALL_TOOLBAR ); 304 cut_btn.set_tooltip_text( _( "Cut Node To Clipboard" ) ); 305 cut_btn.clicked.connect( node_cut ); 306 307 /* Create the detach button */ 308 _detach_btn = new Button.from_icon_name( "minder-detach-symbolic", IconSize.SMALL_TOOLBAR ); 309 _detach_btn.set_tooltip_text( _( "Detach Node" ) ); 310 _detach_btn.clicked.connect( node_detach ); 311 312 /* Create the node deletion button */ 313 var del_btn = new Button.from_icon_name( "edit-delete-symbolic", IconSize.SMALL_TOOLBAR ); 314 del_btn.set_tooltip_text( _( "Delete Node" ) ); 315 del_btn.clicked.connect( node_delete ); 316 317 /* Add the buttons to the button grid */ 318 grid.attach( copy_btn, 0, 0, 1, 1 ); 319 grid.attach( cut_btn, 1, 0, 1, 1 ); 320 grid.attach( _detach_btn, 2, 0, 1, 1 ); 321 grid.attach( del_btn, 3, 0, 1, 1 ); 322 323 /* Add the button grid to the popover */ 324 pack_start( grid, false, true ); 325 326 } 327 328 /* Called whenever the task enable switch is changed within the inspector */ 329 private bool task_changed( Gdk.EventButton e ) { 330 var current = _da.get_current_node(); 331 if( current != null ) { 332 _da.change_current_task( !current.task_enabled(), false ); 333 } 334 return( false ); 335 } 336 337 /* Called whenever the fold switch is changed within the inspector */ 338 private bool fold_changed( Gdk.EventButton e ) { 339 var current = _da.get_current_node(); 340 if( current != null ) { 341 _da.change_current_fold( !current.folded ); 342 } 343 return( false ); 344 } 345 346 /* 347 Called whenever the text widget is changed. Updates the current node 348 and redraws the canvas when needed. 349 */ 350 private void note_changed() { 351 _da.change_current_node_note( _note.buffer.text ); 352 } 353 354 /* Saves the original version of the node's note so that we can */ 355 private bool note_focus_in( EventFocus e ) { 356 _node = _da.get_current_node(); 357 _orig_note = _note.buffer.text; 358 return( false ); 359 } 360 361 /* When the note buffer loses focus, save the note change to the undo buffer */ 362 private bool note_focus_out( EventFocus e ) { 363 if( (_node != null) && (_node.note != _orig_note) ) { 364 _da.undo_buffer.add_item( new UndoNodeNote( _node, _orig_note ) ); 365 } 366 return( false ); 367 } 368 369 /* Copies the current node to the clipboard */ 370 private void node_copy() { 371 MinderClipboard.copy_nodes( _da ); 372 } 373 374 /* Cuts the current node to the clipboard */ 375 private void node_cut() { 376 _da.cut_node_to_clipboard(); 377 } 378 379 /* Detaches the current node and makes it a parent node */ 380 private void node_detach() { 381 _da.detach(); 382 _detach_btn.set_sensitive( false ); 383 } 384 385 /* Deletes the current node */ 386 private void node_delete() { 387 _da.delete_node(); 388 } 389 390 /* Grabs the focus on the note widget */ 391 public void grab_note() { 392 _note.grab_focus(); 393 node_changed(); 394 } 395 396 /* Called whenever the theme is changed */ 397 private void theme_changed( DrawArea da ) { 398 399 int num_colors = Theme.num_link_colors(); 400 RGBA[] colors = new RGBA[num_colors]; 401 402 /* Gather the theme colors into an RGBA array */ 403 for( int i=0; i<num_colors; i++ ) { 404 colors[i] = _da.get_theme().link_color( i ); 405 } 406 407 /* Clear the palette */ 408 _link_color.add_palette( Orientation.HORIZONTAL, 10, null ); 409 410 /* Set the palette with the new theme colors */ 411 _link_color.add_palette( Orientation.HORIZONTAL, 10, colors ); 412 413 } 414 415 /* Called whenever the user changes the current node in the canvas */ 416 private void node_changed() { 417 418 Node? current = _da.get_current_node(); 419 420 if( current != null ) { 421 _task.set_active( current.task_enabled() ); 422 if( current.is_leaf() ) { 423 _fold.set_active( false ); 424 _fold.set_sensitive( false ); 425 } else { 426 _fold.set_active( current.folded ); 427 _fold.set_sensitive( true ); 428 } 429 if( current.is_root() ) { 430 _link_reveal.reveal_child = false; 431 } else { 432 _link_reveal.reveal_child = true; 433 _link_color.rgba = current.link_color; 434 _link_color.alpha = 65535; 435 } 436 _detach_btn.set_sensitive( current.parent != null ); 437 _note.buffer.text = current.note; 438 if( current.image != null ) { 439 var url = _da.image_manager.get_uri( current.image.id ).replace( "&", "&" ); 440 var str = "<a href=\"" + url + "\">" + url + "</a>"; 441 current.image.set_image( _image ); 442 _image_loc.label = str; 443 _image_stack.visible_child_name = "edit"; 444 } else { 445 _image_stack.visible_child_name = "add"; 446 } 447 } 448 449 } 450 451 /* Sets the input focus on the first widget in this inspector */ 452 public void grab_first() { 453 _task.grab_focus(); 454 node_changed(); 455 } 456 457} 458