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>></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