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 StickerInspector : Box {
26
27  private string favorites = GLib.Path.build_filename( Environment.get_user_data_dir(), "minder", "favorites.xml" );
28
29  private MainWindow    _win;
30  private DrawArea?     _da = null;
31  private GLib.Settings _settings;
32  private SearchEntry   _search;
33  private Stack         _stack;
34  private FlowBox       _favorites;
35  private FlowBox       _matched_box;
36  private Image         _dragged_sticker;
37  private double        _motion_x;
38  private double        _motion_y;
39  private Gtk.Menu      _menu;
40  private Gtk.MenuItem  _favorite;
41  private FlowBox       _clicked_category;
42  private StickerSet    _sticker_set;
43  private string        _clicked_sticker;
44
45  public const Gtk.TargetEntry[] DRAG_TARGETS = {
46    {"STRING", TargetFlags.SAME_APP, DragTypes.STICKER}
47  };
48
49  public StickerInspector( MainWindow win, GLib.Settings settings ) {
50
51    Object( orientation:Orientation.VERTICAL, spacing:10 );
52
53    _win      = win;
54    _settings = settings;
55
56    /* Setup favoriting menu */
57    _menu     = new Gtk.Menu();
58    _menu.show.connect(() => {
59      if( _clicked_category == _favorites ) {
60        _favorite.label = _( "Remove From Favorites" );
61        _favorite.set_sensitive( true );
62      } else {
63        _favorite.label = _( "Add To Favorites" );
64        _favorite.set_sensitive( !is_favorite( _clicked_sticker ) );
65      }
66    });
67    _favorite = new Gtk.MenuItem.with_label( _( "Add To Favorites" ) );
68    _favorite.activate.connect( handle_favorite );
69    _menu.add( _favorite );
70    _menu.show_all();
71
72    /*
73     Create instruction label (this will always be visible so it will not be
74     within the scrolled box
75    */
76    var lbl = new Label( _( "Drag and drop sticker onto a node or anywhere else in the map to add a sticker." ) );
77    lbl.wrap      = true;
78    lbl.wrap_mode = Pango.WrapMode.WORD;
79
80    /* Create search field */
81    _search = new SearchEntry();
82    _search.placeholder_text = _( "Search Stickers" );
83    _search.search_changed.connect( do_search );
84
85    /* Create stack */
86    _stack = new Stack();
87
88    /* Create main scrollable pane */
89    var box    = new Box( Orientation.VERTICAL, 0 );
90    var sw     = new ScrolledWindow( null, null );
91    var vp     = new Viewport( null, null );
92    vp.set_size_request( 200, 600 );
93    vp.add( box );
94    sw.add( vp );
95
96    /* Create search result flowbox */
97    _matched_box = create_icon_box();
98
99    var mbox = new Box( Orientation.VERTICAL, 0 );
100    var msw = new ScrolledWindow( null, null );
101    msw.expand = false;
102    msw.get_style_context().add_class( Gtk.STYLE_CLASS_VIEW );
103    msw.add( mbox );
104
105    mbox.pack_start( _matched_box, false, false, 0 );
106
107    _stack.add_named( sw, "all" );
108    _stack.add_named( msw, "matched" );
109
110    /* Create Favorites */
111    _favorites = create_category( box, _( "Favorites" ) );
112    load_favorites();
113
114    /* Pack the elements into this widget */
115    create_from_sticker_set( box );
116
117    /* Add the scrollable widget to the box */
118    pack_start( lbl,      false, false, 5 );
119    pack_start( _search,  false, false, 5 );
120    pack_start( _stack,   true,  true,  5 );
121
122    /* Make sure all elements are visible */
123    show_all();
124
125  }
126
127  /* Creates the rest of the UI from the stickers XML file that is stored in a gresource */
128  private void create_from_sticker_set( Box box ) {
129
130    _sticker_set = new StickerSet();
131
132    var categories = _sticker_set.get_categories();
133
134    for( int i=0; i<categories.length; i++ ) {
135      var category = create_category( box, categories.index( i ) );
136      var icons    = _sticker_set.get_category_icons( categories.index( i ) );
137      for( int j=0; j<icons.length; j++ ) {
138        create_image( category,     icons.index( j ).resource, icons.index( j ).tooltip );
139        create_image( _matched_box, icons.index( j ).resource, icons.index( j ).tooltip );
140      }
141    }
142
143  }
144
145  /* Creates the expander flowbox for the given category name and adds it to the sidebar */
146  private FlowBox create_category( Box box, string name ) {
147
148    /* Create expander */
149    var exp  = new Expander( Utils.make_title( name ) );
150    exp.use_markup = true;
151    exp.expanded   = true;
152
153    /* Create the flowbox which will contain the stickers */
154    var fbox = create_icon_box();
155    exp.add( fbox );
156
157    box.pack_start( exp, false, false, 20 );
158
159    return( fbox );
160
161  }
162
163  /* Creates the image from the given name and adds it to the flow box */
164  private void create_image( FlowBox box, string name, string tooltip ) {
165    var img = new Image.from_resource( "/com/github/phase1geo/minder/" + name );
166    img.name = name;
167    img.set_tooltip_text( tooltip );
168    box.add( img );
169  }
170
171  /* Creates the icon box and sets it up */
172  private FlowBox create_icon_box() {
173    var fbox = new FlowBox();
174    fbox.homogeneous = true;
175    fbox.selection_mode = SelectionMode.NONE;
176    drag_source_set( fbox, Gdk.ModifierType.BUTTON1_MASK, DRAG_TARGETS, Gdk.DragAction.COPY );
177    fbox.drag_begin.connect( on_drag_begin );
178    fbox.drag_data_get.connect( on_drag_data_get );
179    fbox.motion_notify_event.connect((e) => {
180      _motion_x = e.x;
181      _motion_y = e.y;
182      return( true );
183    });
184    fbox.button_press_event.connect((e) => {
185      if( e.button == Gdk.BUTTON_SECONDARY ) {
186        var int_x = (int)e.x;
187        var int_y = (int)e.y;
188        _clicked_category = fbox;
189        _clicked_sticker  = fbox.get_child_at_pos( int_x, int_y ).get_child().name;
190        Utils.popup_menu( _menu, e );
191      }
192      return( true );
193    });
194
195    return( fbox );
196  }
197
198  /* Called whenever the user selects the favorite/unfavorite menu item */
199  private void handle_favorite() {
200    if( _clicked_category == _favorites ) {
201      make_unfavorite();
202    } else {
203      make_favorite();
204    }
205  }
206
207  /* Returns true if the given icon name is favorited */
208  private bool is_favorite( string name ) {
209    bool exists = false;
210    _favorites.get_children().foreach((w) => {
211      exists |= (w as FlowBoxChild).get_child().name == name;
212    });
213    return( exists );
214  }
215
216  /* Make the current sticker a favorite */
217  private void make_favorite() {
218
219    /* Add the sticker to the favorites section */
220    create_image( _favorites, _clicked_sticker, _sticker_set.get_icon_tooltip( _clicked_sticker ) );
221    _favorites.show_all();
222
223    /* Save the favorited status */
224    save_favorites();
225
226  }
227
228  /* Remove the current sticker as a favorite */
229  private void make_unfavorite() {
230
231    /* Remove the sticker from the favorites section */
232    _favorites.get_children().foreach((w) => {
233      if( (w as FlowBoxChild).get_child().name == _clicked_sticker ) {
234        _favorites.remove( w );
235      }
236    });
237
238    /* Save the favorites */
239    save_favorites();
240
241  }
242
243  /* Save the favorited stickers to the save file */
244  private void save_favorites() {
245    Xml.Doc*  doc  = new Xml.Doc();
246    Xml.Node* root = new Xml.Node( null, "favorites" );
247    doc->set_root_element( root );
248    _favorites.get_children().foreach((w) => {
249      var name = (w as FlowBoxChild).get_child().name;
250      Xml.Node* n = new Xml.Node( null, "sticker" );
251      n->set_prop( "name", name );
252      root->add_child( n );
253    });
254    doc->save_format_file( favorites, 1 );
255    delete doc;
256  }
257
258  /* Load the favorite stickers from the file */
259  private void load_favorites() {
260    if( !FileUtils.test( favorites, FileTest.EXISTS ) ) return;
261    Xml.Doc* doc = Xml.Parser.parse_file( favorites );
262    if( doc == null ) return;
263    for( Xml.Node* it=doc->get_root_element()->children; it!=null; it=it->next ) {
264      if( (it->type == Xml.ElementType.ELEMENT_NODE) && (it->name == "sticker") ) {
265        var name = it->get_prop( "name" );
266        create_image( _favorites, name, _sticker_set.get_icon_tooltip( name ) );
267      }
268    }
269    delete doc;
270  }
271
272  /* When the sticker drag begins, set the sticker image to the dragged content */
273  private void on_drag_begin( Widget widget, DragContext context ) {
274    var fbox  = (FlowBox)widget;
275    var int_x = (int)_motion_x;
276    var int_y = (int)_motion_y;
277    _dragged_sticker = (Image)fbox.get_child_at_pos( int_x, int_y ).get_child();
278    Gtk.drag_set_icon_pixbuf( context, _dragged_sticker.pixbuf, 0, 0 );
279  }
280
281  private void on_drag_data_get( Widget widget, DragContext context, SelectionData selection_data, uint target_type, uint time ) {
282    if( target_type == DragTypes.STICKER ) {
283      selection_data.set_text( _dragged_sticker.name, -1 );
284    }
285  }
286
287  /* Performs search */
288  private void do_search() {
289
290    var search_text = _search.text;
291
292    /* If the search field is empty, show all of the icons by category again */
293    if( search_text == "" ) {
294      _matched_box.invalidate_filter();
295      _stack.set_visible_child_name( "all" );
296
297    /* Otherwise, show only the currently matching icons */
298    } else {
299      _matched_box.set_filter_func((item) => {
300        return( item.get_child().get_tooltip_text().contains( search_text ) );
301      });
302      _stack.set_visible_child_name( "matched" );
303    }
304
305  }
306
307  /* Grabbing input focus on the first UI element */
308  public void grab_first() {
309    _search.grab_focus();
310  }
311
312}
313