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( "&", "&amp;" );
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