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;
24
25public class QuickEntry : Gtk.Window {
26
27  private TextView _entry;
28
29  public QuickEntry( DrawArea da, bool replace, GLib.Settings settings ) {
30
31    /* Configure the window */
32    default_width   = 500;
33    default_height  = 500;
34    modal           = true;
35    deletable       = false;
36    title           = _( "Quick Entry" );
37    transient_for   = da.win;
38    window_position = WindowPosition.CENTER_ON_PARENT;
39
40    /* Add window elements */
41    var box = new Box( Orientation.VERTICAL, 0 );
42
43    /* Create the text entry area */
44    _entry = new TextView();
45    _entry.border_width = 5;
46    _entry.set_wrap_mode( Gtk.WrapMode.WORD );
47    _entry.get_style_context().add_class( "textfield" );
48    _entry.key_press_event.connect( on_keypress );
49    _entry.buffer.insert_text.connect( handle_text_insertion );
50
51    /* Create the scrolled window for the text entry area */
52    var sw = new ScrolledWindow( null, null );
53    sw.add( _entry );
54
55    var helprev    = new Revealer();
56    var helpgrid   = new Grid();
57    helpgrid.border_width = 5;
58    var help_title = make_help_label( _( "Help for inputting node information:" ) + "\n" );
59    var help_line  = make_help_label( "  - " + _( "Each line of text describes either the title of a node or note information for a node." ) );
60    var help_tab0  = make_help_label( "  - <b>" + _( "Tab" ) + "</b>:" );
61    var help_tab1  = make_help_label( "  " + _( "Creates a child node of the previous node." ) );
62    var help_hdr0  = make_help_label( "  - <b>#</b>:" );
63    var help_hdr1  = make_help_label( "  " + _( "If this character is the first non-whitespace character, makes a new root node from the title that follows." ) );
64    var help_node0 = make_help_label( "  - <b>*, - or +</b>:" );
65    var help_node1 = make_help_label( "  " + _( "If this character is the first non-whitespace character, make a new node from the title that follows." ) );
66    var help_note0 = make_help_label( "  - <b>&gt;</b>:" );
67    var help_note1 = make_help_label( "  " + _( "If this character is the first non-whitespace character, the following line is appended to the previous node's note." ) );
68    var help_utsk0 = make_help_label( "  - <b>[ ]</b>:" );
69    var help_utsk1 = make_help_label( "  " + _( "If this follows *, + or -, the node is made an uncompleted task." ) );
70    var help_ctsk0 = make_help_label( "  - <b>[x] or [X]</b>:" );
71    var help_ctsk1 = make_help_label( "  " + _( "If this follows *, + or -, the node is made a completed task." ) );
72    helpgrid.attach( help_title, 0, 0, 2 );
73    helpgrid.attach( help_line,  0, 1, 2 );
74    helpgrid.attach( help_tab0,  0, 2 );
75    helpgrid.attach( help_tab1,  1, 2 );
76    helpgrid.attach( help_hdr0,  0, 3 );
77    helpgrid.attach( help_hdr1,  1, 3 );
78    helpgrid.attach( help_node0, 0, 4 );
79    helpgrid.attach( help_node1, 1, 4 );
80    helpgrid.attach( help_note0, 0, 5 );
81    helpgrid.attach( help_note1, 1, 5 );
82    helpgrid.attach( help_utsk0, 0, 6 );
83    helpgrid.attach( help_utsk1, 1, 6 );
84    helpgrid.attach( help_ctsk0, 0, 7 );
85    helpgrid.attach( help_ctsk1, 1, 7 );
86    helprev.reveal_child = false;
87    helprev.add( helpgrid );
88
89    var bbox = new Box( Orientation.HORIZONTAL, 5 );
90    bbox.border_width = 5;
91
92    var info = new Button.from_icon_name( "dialog-information-symbolic", IconSize.BUTTON );
93    info.relief = ReliefStyle.NONE;
94    info.clicked.connect(() => {
95      helprev.reveal_child = !helprev.reveal_child;
96    });
97    bbox.pack_start( info, false, false );
98
99    if( replace ) {
100      var apply = new Button.with_label( _( "Replace" ) );
101      apply.get_style_context().add_class( STYLE_CLASS_SUGGESTED_ACTION );
102      apply.clicked.connect( () => {
103        handle_replace( da );
104        close();
105      });
106      if( !da.is_node_selected() ) apply.set_sensitive( false );
107      bbox.pack_end( apply, false, false );
108    } else {
109      var apply = new Button.with_label( _( "Insert" ) );
110      apply.get_style_context().add_class( STYLE_CLASS_SUGGESTED_ACTION );
111      apply.clicked.connect(() => {
112        handle_insert( da );
113        close();
114      });
115      bbox.pack_end( apply, false, false );
116    }
117
118    var cancel = new Button.with_label( _( "Cancel" ) );
119    cancel.clicked.connect(() => {
120      close();
121    });
122    bbox.pack_end( cancel, false, false );
123
124    box.pack_start( sw,      true,  true );
125    box.pack_end(   bbox,    false, true );
126    box.pack_end(   helprev, false, true );
127
128    add( box );
129
130    show_all();
131
132  }
133
134  private Label make_help_label( string str ) {
135    var lbl = new Label( str );
136    lbl.use_markup = true;
137    lbl.xalign     = (float)0;
138    lbl.get_style_context().add_class( "greyed-label" );
139    return( lbl );
140  }
141
142  private bool on_keypress( EventKey e ) {
143
144    switch( e.keyval ) {
145      case 32    :  return( handle_space() );
146      case 65293 :  return( handle_return() );
147      case 65289 :  return( handle_tab() );
148    }
149
150    return( false );
151
152  }
153
154  /* Called whenever text is inserted by the user (either by entry or by paste) */
155  private void handle_text_insertion( ref TextIter pos, string new_text, int new_text_length ) {
156    var cleaned  = (pos.get_offset() == 0) ? new_text.chug() : new_text;
157    if( cleaned != new_text ) {
158      var void_entry = (void*)_entry;
159      SignalHandler.block_by_func( void_entry, (void*)handle_text_insertion, this );
160      _entry.buffer.insert_text( ref pos, cleaned, cleaned.length );
161      SignalHandler.unblock_by_func( void_entry, (void*)handle_text_insertion, this );
162      Signal.stop_emission_by_name( _entry.buffer, "insert_text" );
163    }
164  }
165
166  /* Returns the text from the start of the current line to the current insertion cursor */
167  private string get_line_text( int adjust ) {
168
169    TextIter current;
170    TextIter startline;
171    TextIter endline;
172    var      buf = _entry.buffer;
173
174    buf.get_iter_at_mark( out current, buf.get_insert() );
175
176    /* Adjust the line */
177    if( adjust < 0 ) {
178      current.backward_lines( 0 - adjust );
179    } else if( adjust > 0 ) {
180      current.backward_lines( adjust );
181    }
182
183    buf.get_iter_at_line( out startline, current.get_line() );
184    buf.get_iter_at_line( out endline,   current.get_line() + 1 );
185
186    return( buf.get_text( startline, endline, true ) );
187
188  }
189
190  /* Returns the text from the start of the current line to the current insertion cursor */
191  private string get_start_to_current_text() {
192
193    TextIter startline;
194    TextIter endline;
195    var      buf = _entry.buffer;
196
197    /* Get the text on the current line */
198    buf.get_iter_at_mark( out endline,   buf.get_insert() );
199    buf.get_iter_at_line( out startline, endline.get_line() );
200
201    return( buf.get_text( startline, endline, true ) );
202
203  }
204
205  /* Returns the whitespace at the beginning of the current line */
206  private bool get_whitespace( string line, out string wspace ) {
207
208    wspace = "";
209
210    try {
211
212      MatchInfo match_info;
213      var       re = new Regex( "^([ \\t]*)" );
214
215      if( re.match( line, 0, out match_info ) ) {
216        wspace = match_info.fetch( 1 );
217        return( true );
218      }
219
220    } catch( RegexError err ) {
221      return( false );
222    }
223
224    return( false );
225
226  }
227
228  /* Converts the given whitespace to all spaces */
229  private string tabs_to_spaces( string wspace ) {
230
231    var tspace = string.nfill( 8, ' ' );
232
233    return( wspace.replace( "\t", tspace ) );
234
235  }
236
237  /* If the user attempts to hit the space bar when adding front-end whitespace, don't insert it */
238  private bool handle_space() {
239
240    return( get_start_to_current_text().strip() == "" );
241
242  }
243
244  /* If the return key is pressed, we will automatically indent the next line */
245  private bool handle_return() {
246
247    string wspace;
248
249    if( get_whitespace( get_line_text( 0 ), out wspace ) ) {
250      var ins = "\n" + wspace;
251      _entry.buffer.insert_at_cursor( ins, ins.length );
252      return( true );
253    }
254
255    return( false );
256
257  }
258
259  /* If the Tab key is pressed, only allow it if it is valid to do so */
260  private bool handle_tab() {
261
262    TextIter current;
263    var      prev = "";
264    var      curr = "";
265
266    _entry.buffer.get_iter_at_mark( out current, _entry.buffer.get_insert() );
267
268    if( current.get_line() == 0 ) {
269      return( true );
270    } else if( get_whitespace( get_line_text( 0 ), out curr ) && get_whitespace( get_line_text( -1 ), out prev ) ) {
271      return( tabs_to_spaces( curr ).length > tabs_to_spaces( prev ).length );
272    }
273
274    return( false );
275
276  }
277
278  /* Inserts the specified nodes into the given drawing area */
279  private void handle_insert( DrawArea da ) {
280    var nodes  = new Array<Node>();
281    var node   = da.get_current_node();
282    var export = (ExportText)da.win.exports.get_by_name( "text" );
283    export.import_text( _entry.buffer.text, da.settings.get_int( "quick-entry-spaces-per-tab" ), da, false, nodes );
284    if( nodes.length == 0 ) return;
285    da.undo_buffer.add_item( new UndoNodesInsert( da, nodes ) );
286    da.set_current_node( nodes.index( 0 ) );
287    da.queue_draw();
288    da.auto_save();
289    da.see();
290  }
291
292  /* Replaces the specified nodes into the given drawing area */
293  private void handle_replace( DrawArea da ) {
294    var nodes  = new Array<Node>();;
295    var node   = da.get_current_node();
296    var parent = node.parent;
297    var export = (ExportText)da.win.exports.get_by_name( "text" );
298    export.import_text( _entry.buffer.text, da.settings.get_int( "quick-entry-spaces-per-tab" ), da, true, nodes );
299    if( nodes.length == 0 ) return;
300    da.undo_buffer.add_item( new UndoNodesReplace( node, nodes ) );
301    da.set_current_node( nodes.index( 0 ) );
302    da.queue_draw();
303    da.auto_save();
304    da.see();
305  }
306
307  /* Preloads the text buffer with the given text */
308  public void preload( string value ) {
309    _entry.buffer.insert_at_cursor( value, value.length );
310  }
311
312}
313