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