├── doc ├── images │ ├── demo.gif │ ├── bookmark.png │ ├── en │ │ ├── jump-1.gif │ │ ├── jump-2.gif │ │ ├── convert.gif │ │ ├── create-1.gif │ │ ├── link-in.gif │ │ ├── link-to-1.gif │ │ ├── link-to-2.gif │ │ ├── remove-1.gif │ │ ├── remove-2.gif │ │ ├── jump-dialog.gif │ │ ├── create-dialog.gif │ │ ├── create-reuse.gif │ │ ├── create-toogle.gif │ │ └── some-bookmarks.gif │ ├── fr │ │ ├── jump-1.gif │ │ ├── jump-2.gif │ │ ├── convert.gif │ │ ├── create-1.gif │ │ ├── link-in.gif │ │ ├── link-to-1.gif │ │ ├── link-to-2.gif │ │ ├── remove-1.gif │ │ ├── remove-2.gif │ │ ├── jump-dialog.gif │ │ ├── create-dialog.gif │ │ ├── create-reuse.gif │ │ ├── create-toogle.gif │ │ └── some-bookmarks.gif │ ├── Bookmark_1.png │ ├── Bookmark_2.png │ ├── english_flag.png │ ├── fp-bookmark.png │ ├── french_flag.png │ ├── french_flag_small.png │ ├── english_flag_small.png │ └── fp-bookmark.svg ├── Titles for demo.psd ├── Map for demo.mm └── Carte pour demo.mm ├── images ├── bookmarks.png ├── bookmarks-screenshot-1.png ├── bookmarks-jump-icon.svg ├── bookmarks-add-or-remove-icon.svg ├── bookmarks.svg └── bookmarks-icon.svg ├── version.properties ├── zips ├── scripts │ └── init │ │ └── BookmarksMonitorIcons.groovy ├── doc │ └── bookmarks │ │ ├── style.css │ │ ├── help_en.html │ │ ├── help_fr.html │ │ └── knacss.css └── icons │ └── bookmarks │ ├── Bookmark 1.svg │ └── Bookmark 2.svg ├── scripts ├── _25_DeleteSubTreeBookmarks.groovy ├── _51_CreateLinkToBookmark.groovy ├── _50_CreateLinkInBookmark.groovy ├── _30_JumpToBookmark.groovy ├── _10_BookmarkNode.groovy ├── _36_JumpToNextBookmark.groovy ├── _35_JumpToPreviousBookmark.groovy ├── _20_ToggleBookmark.groovy ├── _99_Help.groovy ├── _90_ConvertFPBookmarks.groovy ├── _91_ConvertBookmarksToFP.groovy └── _95_FixOldVersionNamedBookmarks.groovy ├── .gitignore ├── src └── main │ └── groovy │ ├── Utils.groovy │ ├── ClearBranchGUI.groovy │ ├── ChangeListener.groovy │ ├── CreateOrDeleteGUI.groovy │ ├── Bookmarks.groovy │ ├── JumpGUI.groovy │ └── CreateLinkGUI.groovy ├── README-fr.md └── README.md /doc/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/demo.gif -------------------------------------------------------------------------------- /images/bookmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/images/bookmarks.png -------------------------------------------------------------------------------- /doc/Titles for demo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/Titles for demo.psd -------------------------------------------------------------------------------- /doc/images/bookmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/bookmark.png -------------------------------------------------------------------------------- /doc/images/en/jump-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/jump-1.gif -------------------------------------------------------------------------------- /doc/images/en/jump-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/jump-2.gif -------------------------------------------------------------------------------- /doc/images/fr/jump-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/jump-1.gif -------------------------------------------------------------------------------- /doc/images/fr/jump-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/jump-2.gif -------------------------------------------------------------------------------- /doc/images/Bookmark_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/Bookmark_1.png -------------------------------------------------------------------------------- /doc/images/Bookmark_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/Bookmark_2.png -------------------------------------------------------------------------------- /doc/images/en/convert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/convert.gif -------------------------------------------------------------------------------- /doc/images/en/create-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/create-1.gif -------------------------------------------------------------------------------- /doc/images/en/link-in.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/link-in.gif -------------------------------------------------------------------------------- /doc/images/en/link-to-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/link-to-1.gif -------------------------------------------------------------------------------- /doc/images/en/link-to-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/link-to-2.gif -------------------------------------------------------------------------------- /doc/images/en/remove-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/remove-1.gif -------------------------------------------------------------------------------- /doc/images/en/remove-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/remove-2.gif -------------------------------------------------------------------------------- /doc/images/english_flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/english_flag.png -------------------------------------------------------------------------------- /doc/images/fp-bookmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fp-bookmark.png -------------------------------------------------------------------------------- /doc/images/fr/convert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/convert.gif -------------------------------------------------------------------------------- /doc/images/fr/create-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/create-1.gif -------------------------------------------------------------------------------- /doc/images/fr/link-in.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/link-in.gif -------------------------------------------------------------------------------- /doc/images/fr/link-to-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/link-to-1.gif -------------------------------------------------------------------------------- /doc/images/fr/link-to-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/link-to-2.gif -------------------------------------------------------------------------------- /doc/images/fr/remove-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/remove-1.gif -------------------------------------------------------------------------------- /doc/images/fr/remove-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/remove-2.gif -------------------------------------------------------------------------------- /doc/images/french_flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/french_flag.png -------------------------------------------------------------------------------- /doc/images/en/jump-dialog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/jump-dialog.gif -------------------------------------------------------------------------------- /doc/images/fr/jump-dialog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/jump-dialog.gif -------------------------------------------------------------------------------- /doc/images/en/create-dialog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/create-dialog.gif -------------------------------------------------------------------------------- /doc/images/en/create-reuse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/create-reuse.gif -------------------------------------------------------------------------------- /doc/images/en/create-toogle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/create-toogle.gif -------------------------------------------------------------------------------- /doc/images/en/some-bookmarks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/en/some-bookmarks.gif -------------------------------------------------------------------------------- /doc/images/fr/create-dialog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/create-dialog.gif -------------------------------------------------------------------------------- /doc/images/fr/create-reuse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/create-reuse.gif -------------------------------------------------------------------------------- /doc/images/fr/create-toogle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/create-toogle.gif -------------------------------------------------------------------------------- /doc/images/fr/some-bookmarks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/fr/some-bookmarks.gif -------------------------------------------------------------------------------- /doc/images/french_flag_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/french_flag_small.png -------------------------------------------------------------------------------- /doc/images/english_flag_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/doc/images/english_flag_small.png -------------------------------------------------------------------------------- /images/bookmarks-screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilive/Freeplane-Bookmarks-add-on/HEAD/images/bookmarks-screenshot-1.png -------------------------------------------------------------------------------- /version.properties: -------------------------------------------------------------------------------- 1 | version=v0.7.3 2 | downloadUrl=https://github.com/lilive/Freeplane-Bookmarks-add-on 3 | freeplaneVersionFrom=1.10.6 4 | -------------------------------------------------------------------------------- /zips/scripts/init/BookmarksMonitorIcons.groovy: -------------------------------------------------------------------------------- 1 | // Run the listener that take care of bookmarks icons modifications 2 | 3 | lilive.bookmarks.ChangeListener.startMonitor() 4 | -------------------------------------------------------------------------------- /scripts/_25_DeleteSubTreeBookmarks.groovy: -------------------------------------------------------------------------------- 1 | // Delete all bookmarks in the currently selected node and its descendants 2 | 3 | lilive.bookmarks.ClearBranchGUI.show( node ) 4 | 5 | 6 | -------------------------------------------------------------------------------- /scripts/_51_CreateLinkToBookmark.groovy: -------------------------------------------------------------------------------- 1 | // Create a link in this node. 2 | // The target is a bookmark, or the memorized node if it exists. 3 | 4 | import lilive.bookmarks.CreateLinkGUI 5 | CreateLinkGUI.show( node, CreateLinkGUI.Mode.TO ) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | *.backup 3 | bookmarks.addon.mm 4 | build 5 | lib 6 | .gradle 7 | .#* 8 | versions 9 | Thumbs.db 10 | display-downloads.bat 11 | emacs-edition-macros.el 12 | emacs-edition-functions.el 13 | Notes developpement.org 14 | -------------------------------------------------------------------------------- /scripts/_50_CreateLinkInBookmark.groovy: -------------------------------------------------------------------------------- 1 | // Create a link to this node. 2 | // The node where the link is created is a bookmark, or the memorized node if it exists. 3 | 4 | import lilive.bookmarks.CreateLinkGUI 5 | CreateLinkGUI.show( node, CreateLinkGUI.Mode.IN ) 6 | -------------------------------------------------------------------------------- /scripts/_30_JumpToBookmark.groovy: -------------------------------------------------------------------------------- 1 | // Jump to a bookmark 2 | 3 | import lilive.bookmarks.Bookmarks 4 | import lilive.bookmarks.JumpGUI 5 | 6 | // Clean the bookmarks if needed 7 | def namedBookmarks = Bookmarks.loadNamedBookmarks( node.map ) 8 | namedBookmarks = Bookmarks.fixNamedBookmarksInconsistency( namedBookmarks, node.map ) 9 | 10 | // Display the jump window 11 | JumpGUI.show( node, config ) 12 | -------------------------------------------------------------------------------- /scripts/_10_BookmarkNode.groovy: -------------------------------------------------------------------------------- 1 | // Create a bookmark (or delete an existing one) in this node 2 | 3 | import lilive.bookmarks.Bookmarks 4 | import lilive.bookmarks.CreateOrDeleteGUI 5 | 6 | // Clean the bookmarks if needed 7 | def namedBookmarks = Bookmarks.loadNamedBookmarks( node.map ) 8 | namedBookmarks = Bookmarks.fixNamedBookmarksInconsistency( namedBookmarks, node.map ) 9 | 10 | // Display the dialog window 11 | CreateOrDeleteGUI.show( node, namedBookmarks ) 12 | -------------------------------------------------------------------------------- /scripts/_36_JumpToNextBookmark.groovy: -------------------------------------------------------------------------------- 1 | // Jump to next bookmark 2 | 3 | import lilive.bookmarks.Bookmarks as BM 4 | 5 | def namedBookmarks = BM.loadNamedBookmarks( node.map ) 6 | 7 | def start = node 8 | n = node.getNext() 9 | while( n != start && ! BM.isBookmarked( n, namedBookmarks ) ) n = n.getNext() 10 | 11 | if( n != start && BM.isBookmarked( n, namedBookmarks ) ) 12 | { 13 | c.select( n ) 14 | c.centerOnNode( n ) 15 | } 16 | else 17 | { 18 | Utils.setStatusInfo( 19 | BM.gtt( 'T_no_next_BM' ) + " !", 20 | 'messagebox_warning' 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /scripts/_35_JumpToPreviousBookmark.groovy: -------------------------------------------------------------------------------- 1 | // Jump to previous bookmark 2 | 3 | import lilive.bookmarks.Bookmarks as BM 4 | 5 | def namedBookmarks = BM.loadNamedBookmarks( node.map ) 6 | 7 | def start = node 8 | n = node.getPrevious() 9 | while( n != start && ! BM.isBookmarked( n, namedBookmarks ) ) n = n.getPrevious() 10 | 11 | if( n != start && BM.isBookmarked( n, namedBookmarks ) ) 12 | { 13 | c.select( n ) 14 | c.centerOnNode( n ) 15 | } 16 | else 17 | { 18 | Utils.setStatusInfo( 19 | BM.gtt( 'T_no_prev_BM' ) + " !", 20 | 'messagebox_warning' 21 | ) 22 | } 23 | 24 | -------------------------------------------------------------------------------- /zips/doc/bookmarks/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 2em 1em; 3 | } 4 | 5 | .wrapper { 6 | max-width: 60em; 7 | margin: 0 auto; 8 | } 9 | 10 | section { 11 | margin: 2em 0 7em; 12 | } 13 | .section2 { 14 | margin-top: 0; 15 | margin-bottom: 3em; 16 | } 17 | 18 | .icon-svg { 19 | float: left; 20 | height: 2em; 21 | width: auto; 22 | margin-right: 0.5em; 23 | margin-left: 0.5em; 24 | } 25 | h1 .icon-svg { 26 | float: none; 27 | } 28 | 29 | .lang img { 30 | height: 2em; 31 | width: auto; 32 | } 33 | 34 | .shortcut { 35 | border: 1px dashed #aaa; 36 | border-radius: 0.2em; 37 | padding: 0.1em; 38 | font-size: 80%; 39 | } 40 | 41 | .command { 42 | font-style: italic; 43 | font-weight: bold; 44 | color: #005942; 45 | } 46 | -------------------------------------------------------------------------------- /src/main/groovy/Utils.groovy: -------------------------------------------------------------------------------- 1 | package lilive.bookmarks 2 | 3 | import org.freeplane.plugin.script.proxy.ScriptUtils 4 | import java.util.TimerTask 5 | 6 | public class Utils 7 | { 8 | private static Timer timer = new Timer() 9 | private static TimerTask timerTask = null 10 | 11 | public static void setStatusInfo( String message, String icon = '' ) 12 | { 13 | if( timerTask ) timerTask.cancel() 14 | 15 | if( icon ){ 16 | ScriptUtils.c().setStatusInfo( 'lilive bookmarks', message , icon ) 17 | } else { 18 | ScriptUtils.c().setStatusInfo( 'lilive bookmarks', message ) 19 | } 20 | timerTask = timer.runAfter( 21 | 5000, 22 | { ScriptUtils.c().setStatusInfo('lilive bookmarks', '' ) } 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /images/bookmarks-jump-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /images/bookmarks-add-or-remove-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /scripts/_20_ToggleBookmark.groovy: -------------------------------------------------------------------------------- 1 | // Switch between named bookmark / anonymous bookmark / no bookmark for this node 2 | 3 | import lilive.bookmarks.Bookmarks as BM 4 | 5 | def namedBookmarks = BM.loadNamedBookmarks( node.map ) 6 | namedBookmarks = BM.fixNamedBookmarksInconsistency( namedBookmarks, node.map ) 7 | def isAnonymousBookmark = BM.isAnonymousBookmarked( node ) 8 | def isNamedBookmark = BM.isNamedBookmarked( node, namedBookmarks ) 9 | 10 | if( isNamedBookmark ) 11 | { 12 | // Remove the named bookmark and put a regular bookmark instead 13 | BM.deleteNamedBookmark( node, namedBookmarks ) 14 | BM.createAnonymousBookmark( node ) 15 | Utils.setStatusInfo( BM.gtt( 'T_node_now_SBM' ), 'button_ok' ) 16 | } 17 | else if( isAnonymousBookmark ) 18 | { 19 | // Remove the named bookmark 20 | BM.deleteAnonymousBookmark( node ) 21 | Utils.setStatusInfo( BM.gtt( 'T_node_no_BM_anymore' ), 'button_cancel' ) 22 | } 23 | else 24 | { 25 | // Create a regular bookmark 26 | BM.createAnonymousBookmark( node ) 27 | Utils.setStatusInfo( BM.gtt( 'T_node_now_SBM' ), 'button_ok' ) 28 | } 29 | 30 | -------------------------------------------------------------------------------- /scripts/_99_Help.groovy: -------------------------------------------------------------------------------- 1 | // Open the help file in the user browser 2 | 3 | import java.awt.Desktop; 4 | import javax.swing.JOptionPane 5 | import lilive.bookmarks.Bookmarks as BM 6 | 7 | winTitle = BM.gtt( 'T_help_win_title' ) 8 | 9 | def messageBox( message, icon ) 10 | { 11 | ui.informationMessage( ui.frame, message, winTitle, icon ) 12 | } 13 | 14 | // Check if desktop is supported 15 | if( ! Desktop.isDesktopSupported() ) 16 | { 17 | messageBox( BM.gtt( "T_help_desktop_error" ), JOptionPane.ERROR_MESSAGE ) 18 | return 19 | } 20 | 21 | // create desktop instance 22 | Desktop desktop = Desktop.getDesktop(); 23 | 24 | // ensure that desktop is supported 25 | if( !desktop.isSupported( java.awt.Desktop.Action.BROWSE ) ) 26 | { 27 | messageBox( BM.gtt( "T_help_desktop_error" ), JOptionPane.ERROR_MESSAGE ) 28 | return 29 | } 30 | 31 | // now, show page in external browser 32 | def path = c.getUserDirectory().toPath() 33 | path = path.resolve( "doc" ).resolve( "bookmarks" ).resolve( "help_en.html" ) 34 | try 35 | { 36 | desktop.browse( path.toUri() ); 37 | } 38 | catch ( Exception e ) 39 | { 40 | messageBox( "${BM.gtt( 'T_help_browse_error' )} '${path.toString()}': ${e.message}.", JOptionPane.ERROR_MESSAGE ) 41 | } 42 | 43 | -------------------------------------------------------------------------------- /scripts/_90_ConvertFPBookmarks.groovy: -------------------------------------------------------------------------------- 1 | // Convert all builtin freeplane icon "Excellent" to standard bookmarks 2 | 3 | import javax.swing.JOptionPane 4 | import lilive.bookmarks.Bookmarks as BM 5 | 6 | winTitle = BM.gtt( 'T_FP_to_BM_win_title' ) 7 | 8 | def messageBox( message, icon ) 9 | { 10 | ui.informationMessage( ui.frame, message, winTitle, icon ) 11 | } 12 | 13 | // Ask for confirmation 14 | def cancel = ui.showConfirmDialog( 15 | node.delegate, 16 | BM.gtt( 'T_convert_FP_BM_warning' ), 17 | winTitle, JOptionPane.WARNING_MESSAGE 18 | ) 19 | if( cancel != 0 ) 20 | { 21 | messageBox( BM.gtt( "T_op_canceled" ), JOptionPane.WARNING_MESSAGE ) 22 | return 23 | } 24 | 25 | // Convert the icons 26 | def numBookmarksCreated = 0 27 | c.findAll().each 28 | { 29 | n -> 30 | if( n.icons.contains( "bookmark" ) ) 31 | { 32 | numBookmarksCreated ++; 33 | while( n.icons.remove( "bookmark" ) ){} 34 | BM.createAnonymousBookmark( n ) 35 | } 36 | } 37 | 38 | // Display result 39 | if( numBookmarksCreated > 0 ) messageBox( "${numBookmarksCreated} ${BM.gtt( 'T_BMs_created' )} !", JOptionPane.INFORMATION_MESSAGE ) 40 | else messageBox( "${BM.gtt( 'T_no_BMs_created' )} !", JOptionPane.ERROR_MESSAGE ) 41 | 42 | 43 | -------------------------------------------------------------------------------- /doc/images/fp-bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /zips/icons/bookmarks/Bookmark 1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /zips/icons/bookmarks/Bookmark 2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /scripts/_91_ConvertBookmarksToFP.groovy: -------------------------------------------------------------------------------- 1 | // Convert all bookmarks to buitin freeplane icon "Excellent" 2 | 3 | import javax.swing.JOptionPane 4 | import lilive.bookmarks.Bookmarks as BM 5 | 6 | winTitle = BM.gtt( 'T_BMs_to_FP_win_title' ) 7 | 8 | def messageBox( message, icon ) 9 | { 10 | ui.informationMessage( ui.frame, message, winTitle, icon ) 11 | } 12 | 13 | // Ask for confirmation 14 | def cancel = ui.showConfirmDialog( 15 | node.delegate, 16 | BM.gtt( 'T_convert_BM_FP_warning' ), 17 | winTitle, JOptionPane.WARNING_MESSAGE 18 | ) 19 | if( cancel != 0 ) 20 | { 21 | messageBox( BM.gtt( "T_op_canceled" ), JOptionPane.WARNING_MESSAGE ) 22 | return 23 | } 24 | 25 | // Clear the records for the named bookmarks 26 | def storageErased = BM.eraseNamedBookmarksStorage( node.map ) 27 | 28 | // Convert these icons 29 | def numIconsConverted = 0 30 | c.findAll().each 31 | { 32 | n -> 33 | if( BM.hasAnonymousBookmarkIcon( n ) || BM.hasNamedBookmarkIcon( n ) ) 34 | { 35 | def iconsNames = n.icons.icons 36 | n.icons.clear() 37 | iconsNames.each 38 | { 39 | icon -> 40 | if( icon == BM.anonymousIcon || icon == BM.namedIcon ) 41 | { 42 | n.icons.add( "bookmark" ) 43 | numIconsConverted ++; 44 | } 45 | else 46 | { 47 | n.icons.add( icon ) 48 | } 49 | } 50 | } 51 | } 52 | 53 | // Display result 54 | if( numIconsConverted > 0 ) messageBox( "${numIconsConverted} ${BM.gtt( 'T_icons_converted' )} !", JOptionPane.INFORMATION_MESSAGE ) 55 | else if( storageErased ) messageBox( "${BM.gtt( 'T_storage_erased' )} !", JOptionPane.INFORMATION_MESSAGE ) 56 | else messageBox( "${BM.gtt( 'T_no_conversion_required' )} !", JOptionPane.ERROR_MESSAGE ) 57 | 58 | 59 | -------------------------------------------------------------------------------- /images/bookmarks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /images/bookmarks-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /scripts/_95_FixOldVersionNamedBookmarks.groovy: -------------------------------------------------------------------------------- 1 | // Update the named bookmarks defined by a previous version of the addon (v0.5.0) 2 | 3 | // These old bookmarks 4 | // - have an icon named "bookmark-named" 5 | // - are stored within the local map storage at key "MarksKeys" 6 | 7 | // If a bookmark with the same name as an actual named bookmark is found in 8 | // the old bookmarks, the actual one is converted to standard bookmark 9 | 10 | import org.freeplane.plugin.script.proxy.Convertible 11 | import groovy.json.JsonBuilder 12 | import groovy.json.JsonSlurper 13 | import javax.swing.JOptionPane 14 | import lilive.bookmarks.Bookmarks as BM 15 | 16 | oldStorageKey = "MarksKeys" 17 | oldNamedIcon = "bookmark-named" 18 | 19 | def numFixedBookmarks = 0 20 | def numAnonymizedBookmarks = 0 21 | 22 | winTitle = BM.gtt( 'T_convert_v050_NMBs_win_title' ) 23 | 24 | def messageBox( message, icon ) 25 | { 26 | ui.informationMessage( ui.frame, message, winTitle, icon ) 27 | } 28 | 29 | def Map loadOldNamedBookmarks() 30 | { 31 | // Load the named bookmarks stored in the "MarksKeys" entry in map local storage 32 | def marksString = new Convertible( '{}' ) 33 | def stored = node.map.storage.getAt( oldStorageKey ) 34 | if( stored ) marksString = stored; 35 | def namedBookmarks = new JsonSlurper().parseText( marksString.getText() ) as Map 36 | 37 | // Discard nodes that do not exist anymore, and nodes that haven't got the 38 | // old bookmark icon 39 | namedBookmarks.removeAll 40 | { 41 | key, id -> 42 | def n = map.node( id ) 43 | return ( n == null || ! n.icons.contains( oldNamedIcon ) ) 44 | } 45 | 46 | namedBookmarks = namedBookmarks.sort() 47 | 48 | return namedBookmarks 49 | } 50 | 51 | def deleteOldNamedBookmarksDatas() 52 | { 53 | // Delete the bookmarks stored in the "MarksKeys" entry in map local storage 54 | node.map.storage.putAt( oldStorageKey, null ) 55 | } 56 | 57 | def mergeNamedBookmarks( oldBookmarks, uptodateBookmarks ) 58 | { 59 | // Merge the old bookmarks in the already up to date bookmarks 60 | 61 | conflicts = [:] 62 | oldBookmarks.each 63 | { 64 | key, id -> 65 | if( uptodateBookmarks.containsKey( key ) && uptodateBookmarks[ key ] != id ) conflicts[ key ] = uptodateBookmarks[ key ] 66 | uptodateBookmarks[ key ] = id; 67 | } 68 | uptodateBookmarks = uptodateBookmarks.sort() 69 | return [ uptodateBookmarks, conflicts ] 70 | } 71 | 72 | def cancel = ui.showConfirmDialog( 73 | node.delegate, 74 | BM.gtt( 'T_convert_v050_NBMs_warning' ), 75 | winTitle, JOptionPane.WARNING_MESSAGE 76 | ) 77 | if( cancel != 0 ) 78 | { 79 | messageBox( BM.gtt( "T_op_canceled" ), JOptionPane.WARNING_MESSAGE ) 80 | return 81 | } 82 | 83 | // Get the actual version bookmarks 84 | def namedBookmarks = BM.loadNamedBookmarks( node.map ) 85 | 86 | // Look for named bookmarks in the local map storage, to find them as defined 87 | // by previous version of the addon 88 | def oldNamedBookmarks = loadOldNamedBookmarks() 89 | numFixedBookmarks = oldNamedBookmarks.size() 90 | 91 | // Move old bookmarks to new bookmarks 92 | if( numFixedBookmarks ) 93 | { 94 | // Delete them from the local storage 95 | deleteOldNamedBookmarksDatas() 96 | 97 | // Merge the two 98 | ( namedBookmarks, conflicts ) = mergeNamedBookmarks( oldNamedBookmarks, namedBookmarks ) 99 | 100 | // Save them with the new settings 101 | BM.saveNamedBookmarks( namedBookmarks, node.map ) 102 | 103 | // If an actual bookmark as the same key than an old one, 104 | // convert this bookmark to anonymous bookmark 105 | conflicts.each 106 | { 107 | key, id -> 108 | def n = map.node( id ) 109 | if( n ) 110 | { 111 | n.icons.remove( BM.namedIcon ) 112 | BM.createAnonymousBookmark( n ) 113 | numAnonymizedBookmarks++; 114 | } 115 | } 116 | } 117 | 118 | // Now replace the icons 119 | c.findAll().each 120 | { 121 | n -> 122 | def update = false 123 | while( n.icons.remove( oldNamedIcon ) ) update = true 124 | if( update ) 125 | { 126 | if( BM.hasBookmarkName( n, namedBookmarks ) ) 127 | { 128 | n.icons.add( BM.namedIcon ) 129 | numFixedBookmarks++; 130 | } 131 | } 132 | } 133 | 134 | // display result 135 | def message = '' 136 | def icon = JOptionPane.INFORMATION_MESSAGE 137 | if( numFixedBookmarks ) 138 | { 139 | message = "${numFixedBookmarks} ${BM.gtt( 'T_v050_NBMs_converted')}" 140 | if( numAnonymizedBookmarks ) message += "\n${BM.gtt( 'T_and' )} ${numAnonymizedBookmarks} ${BM.gtt( 'T_NBMs_anonymized')}." 141 | else message += "." 142 | } 143 | else 144 | { 145 | message = "${BM.gtt( 'T_no_v050_NBMs_converted' ) } !" 146 | icon = JOptionPane.ERROR_MESSAGE 147 | } 148 | 149 | messageBox( message, icon ) 150 | 151 | 152 | -------------------------------------------------------------------------------- /README-fr.md: -------------------------------------------------------------------------------- 1 | # ![logo](doc/images/bookmark.png) Marque-pages pour Freeplane 2 | 3 | [Read in english](README.md) ![langage flag](doc/images/english_flag_small.png) 4 | 5 | Ce module complémentaire (add-on) permet de mettre des marque-pages sur des nœuds d'une carte Freeplane, et de passer facilement de l'un à l'autre. 6 | 7 | Les marque-pages sont de deux sortes : 8 | 9 | - Des **marque-pages standard**, signalés sur la carte par l'icône de marque-page violette. 10 | - Des **marque-pages nommés** (associés à un raccourci clavier), signalés sur la carte par l'icône de marque-page verte. 11 | 12 | ![demo](doc/images/demo.gif) 13 | 14 | Ce module crée un nouveau menu nommé "Marque-pages" dans la barre de menu. On y trouve les actions disponibles : 15 | 16 | - **Placer/Supprimer un marque-page** : Placer un marque-page sur le nœud sélectionné, ou en effacer le marque-page. Pour la création on choisit alors si on veut affecter un raccourci clavier au marque-page. 17 | - **Atteindre un marque-page** : Sauter à un marque-page déjà défini. 18 | - **Basculer un marque-page** : Selon l'état du nœud 19 | - convertit un marque-page nommé en marque-page standard 20 | - effacer un marque-page standard 21 | - place un marque-page standard 22 | 23 | En plus de ces 3 actions principales, il est aussi possible de créer facilement des liens vers ou dans les nœuds marqués. 24 | 25 | ## Retours et contributions 26 | 27 | Tout commentaire sur ce module est grandement encouragé :smile:. Vos retours d'expérience, idées et avis permettent d'améliorer ce module. 28 | - Vous pouvez écrire vos commentaires dans [la discussion dédiée](https://sourceforge.net/p/freeplane/discussion/758437/thread/ec280c4e/) sur le forum de Freeplane. 29 | - Vous pouvez aussi [soumettre ici](../../issues) des rapports de boggue ou des suggestions d'amélioration. 30 | 31 | De même, toute proposition d'amélioration du code et toute pull request est la très bienvenue. Le code du module est documenté, mais n'hésitez pas à me demander des précisions si besoin. 32 | 33 | ## Installation 34 | 35 | - Commencer par ouvrir les préférences de Freeplane (menu `Outils > Préférences`). À l'onglet `Formules & scripts`, section `Scripts`, sélectionner `Oui` pour la ligne `Autoriser l'exécution des scripts`. 36 | - Télécharger le fichier *bookmarks-vX.X.X.addon.mm* de [la dernière release](../../releases). 37 | - Ouvrir ce fichier avec Freeplane, et suivre les instructions. 38 | 39 | ## Mise-à-jour 40 | 41 | Pour mettre à jour le module il suffit d'installer la dernière version. Inutile de désinstaller la version précédente. 42 | 43 | **Important :** Si vous avez créé des marque-pages avec une version de ce module antérieure à la version v0.5.1, vous devez mettre vos cartes contenant des marque-pages à jour. Pour cela utiliser le menu *"Marque-pages>Outils>Mettre à jour les marque-pages d'une version antérieure du module"*. 44 | 45 | ## Avertissement 46 | 47 | J'utilise quotidiennement ce module pour mon usage personnel depuis un an sans aucun problème. Au 29 juin 2020 il a été téléchargé 60 fois, toutes versions confondues, et personne n'a rapporté de problème de disfonctionnement. Mais je préfère quand même prévenir et déclarer : à utiliser à ses propres risques. 48 | 49 | Pourquoi cette précaution ? Pour ces deux choses détaillées ci-dessous : 50 | 51 | - Le module modifie légèrement le fonctionnement de FreePlane, pour s'assurer que l'ensemble des marque-pages nommé reste cohérent. Il doit être impossible pour l'utilisateur d'ajouter de la façon classique l'icône de marque-page nommé à un nœud, car sinon il n'aurait pas de raccourci clavier associé. Il doit aussi être impossible de copier un marque-page nommé en même temps que son nœud, car ces marque-pages doivent être uniques. Dans ce cas le nœud est donc copié sans son marque-page. 52 | Pour cela, un peu de code est exécuté chaque fois que l'icône d'un nœud est modifiée par l'utilisateur, et chaque fois qu'un nouveau nœud est créé. Dans l'absolu, ceci pourrait avoir une conséquence négative sur le fonctionnement de FreePlane. 53 | 54 | - Le module modifie une carte mentale quand on l'utilise, en y ajoutant des icônes, et en stockant dans le fichier de la carte (dans la *map storage area*) les raccourcis claviers associés aux marque-pages nommés. Ces données sont sauvegardées directement dans le fichier de la carte. Au pire, on peut donc envisager le risque qu'un bug dans le module en vienne à corrompre le fichier de la carte. 55 | 56 | Je le redis : je n'ai eu aucun problème en un an d'utilisation. Mais tous les retours sur ces points particuliers sont les bienvenus ! 57 | 58 | ## Encore à faire - Idées de développement 59 | 60 | Voir [le readme en anglais](README.md) 61 | 62 | ## Compilation 63 | 64 | Pour créer vous-même le fichier d'installatation du module `bookmarks-vX.X.X.addon.mm`, vous devez commencer par créer la bibliothèque java associée : 65 | 66 | - Installer Freeplane (bien sûr !) 67 | - Installer gradle 68 | - Télécharger les sources 69 | - Modifier le fichier `build.gradle` pour adapter les chemins dans `repositories.dirs[]` à votre installation de Freeplane 70 | - Ouvrir une invite de commande dans le dossier contenant les sources 71 | - `gradle build` va créer le fichier `lib/bookmarks.jar` 72 | 73 | Ceci fait vous pouvez ouvrir `bookmarks.mm` dans Freeplane, puis créer le module avec [Outils > Developer Tools > Package add-on for publication](https://freeplane.sourceforge.io/wiki/index.php/Add-ons_(Develop)). Vous obtiendrez alors le fichier `bookmarks-vx.x.x.addon.mm` qui installe le module quand il est ouvert avec Freeplane. 74 | -------------------------------------------------------------------------------- /src/main/groovy/ClearBranchGUI.groovy: -------------------------------------------------------------------------------- 1 | package lilive.bookmarks 2 | 3 | import lilive.bookmarks.Bookmarks as BM 4 | import groovy.swing.SwingBuilder 5 | import java.awt.BorderLayout 6 | import java.awt.GridBagConstraints 7 | import java.awt.event.* 8 | import java.util.Map as JMap 9 | import javax.swing.AbstractAction 10 | import javax.swing.InputMap 11 | import javax.swing.JComponent 12 | import javax.swing.JFrame 13 | import javax.swing.JList as SwingList 14 | import javax.swing.JPanel 15 | import javax.swing.KeyStroke 16 | import javax.swing.ButtonGroup 17 | import org.freeplane.api.MindMap as MindMap 18 | import org.freeplane.api.Node as Node 19 | import org.freeplane.core.ui.components.UITools as ui 20 | import org.freeplane.features.link.LinkController 21 | import org.freeplane.features.link.mindmapmode.MLinkController 22 | import org.freeplane.plugin.script.proxy.Proxy.Controller as ProxyController 23 | import org.freeplane.plugin.script.proxy.Proxy.Node as ProxyNode 24 | import org.freeplane.plugin.script.proxy.ScriptUtils 25 | import org.freeplane.core.util.MenuUtils 26 | import javax.swing.JRadioButton 27 | import java.awt.KeyboardFocusManager 28 | import javax.swing.JOptionPane 29 | 30 | /** 31 | * Display a window that allow to delete all bookmarks in a branch 32 | */ 33 | public class ClearBranchGUI 34 | { 35 | static private Object gui 36 | 37 | // Build and display the window 38 | static public void show( ProxyNode node ) 39 | { 40 | MindMap map = node.map 41 | SwingBuilder swing = new SwingBuilder() 42 | 43 | // Create the gui 44 | gui = createWindow( swing, node, map ) 45 | 46 | // Center the gui over the freeplane window 47 | gui.pack() 48 | gui.setLocationRelativeTo( ui.frame ) 49 | gui.visible = true 50 | } 51 | 52 | // Create the window 53 | static private Object createWindow( 54 | SwingBuilder swing, 55 | ProxyNode node, MindMap map 56 | ) 57 | { 58 | JRadioButton allBookmarksBtn 59 | JRadioButton anonymousBookmarksBtn 60 | JRadioButton namedBookmarksBtn 61 | Object gui = swing.dialog( 62 | title: BM.gtt( 'T_clear_branch_win_title' ), 63 | modal:true, 64 | owner: ui.frame, 65 | defaultCloseOperation: JFrame.DISPOSE_ON_CLOSE 66 | ){ 67 | borderLayout() 68 | // A panel for all the compontents (needed to add an inner padding) 69 | panel( 70 | border: emptyBorder( 10 ), 71 | constraints: BorderLayout.PAGE_START 72 | ){ 73 | gridLayout( rows:5, vgap:10 ) 74 | label( BM.gtt( 'T_clear_branch_message' ) ) 75 | button( 76 | text: BM.gtt( 'T_clear_branch_all_BMS' ), 77 | actionPerformed: { 78 | BM.deleteSubTreeBookmarks( node, true, true ) 79 | gui.dispose() 80 | ui.informationMessage( 81 | ui.frame, 82 | BM.gtt( 'T_clear_branch_all_BMs_deleted' ), 83 | BM.gtt( 'T_clear_branch_win_title' ), 84 | JOptionPane.INFORMATION_MESSAGE 85 | ) 86 | } 87 | ) 88 | button( 89 | text: BM.gtt( 'T_clear_branch_only_SBMs' ), 90 | actionPerformed: { 91 | BM.deleteSubTreeBookmarks( node, true, false ) 92 | gui.dispose() 93 | ui.informationMessage( 94 | ui.frame, 95 | BM.gtt( 'T_clear_branch_only_SBMs_deleted' ), 96 | BM.gtt( 'T_clear_branch_win_title' ), 97 | JOptionPane.INFORMATION_MESSAGE 98 | ) 99 | } 100 | ) 101 | button( 102 | text: BM.gtt( 'T_clear_branch_only_NBMs' ), 103 | actionPerformed: { 104 | BM.deleteSubTreeBookmarks( node, false, true ) 105 | gui.dispose() 106 | ui.informationMessage( 107 | ui.frame, 108 | BM.gtt( 'T_clear_branch_only_NBMs_deleted' ), 109 | BM.gtt( 'T_clear_branch_win_title' ), 110 | JOptionPane.INFORMATION_MESSAGE 111 | ) 112 | } 113 | ) 114 | button( 115 | text: BM.gtt( 'T_clear_branch_cancel' ), 116 | actionPerformed: { 117 | gui.dispose() 118 | ui.informationMessage( 119 | ui.frame, 120 | BM.gtt( 'T_clear_branch_canceled' ), 121 | BM.gtt( 'T_clear_branch_win_title' ), 122 | JOptionPane.ERROR_MESSAGE 123 | ) 124 | } 125 | ) 126 | } 127 | } 128 | 129 | // Set Esc key to close the gui 130 | String onEscPressID = "onEscPress" 131 | InputMap inputMap = gui.getRootPane().getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ) 132 | inputMap.put( KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 ), onEscPressID ) 133 | gui.getRootPane().getActionMap().put( 134 | onEscPressID, 135 | new AbstractAction(){ 136 | @Override 137 | public void actionPerformed( ActionEvent e ){ gui.dispose() } 138 | } 139 | ) 140 | 141 | return gui 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![logo](doc/images/bookmark.png) Bookmarks for Freeplane 2 | 3 | [Voir en français](README-fr.md) ![french flag](doc/images/french_flag_small.png) 4 | 5 | This add-on allows to bookmark some nodes in a Freeplane map, and to easily navigate between them. 6 | 7 | There are two kinds of bookmarks : 8 | 9 | - **Standard bookmarks**, flagged by the purple bookmark icon. 10 | - **Named bookmarks** (bonded to a keyboard shortcut), flagged by the green bookmark icon. 11 | 12 | ![demo](doc/images/demo.gif) 13 | 14 | This add-on create a new "Bookmarks" entry in the menu bar, which give access to the different actions : 15 | 16 | - **Add / Remove a bookmark** : Add a bookmark to the selected node, or remove it. Standard and named bookmarks can be defined here. 17 | - **Jump to bookmark** : Reach a bookmarked node. 18 | - **Toggle the bookmark** : Depending on the selected note state : 19 | - convert a named bookmark to a standard bookmark 20 | - delete a standard bookmark 21 | - create a standard bookmark 22 | 23 | Apart form these 3 main actions, it is also possible to create links *to* or *from* the bookmarked nodes. 24 | 25 | ## Feedback and contributions 26 | 27 | Any feedback will be very appreciated and will help to improve this add-on :smile: 28 | - You can post messages on [the dedicated discussion](https://sourceforge.net/p/freeplane/discussion/758437/thread/ec280c4e/) in the Freeplane forum. 29 | - You can also submit issues or make feature requests [here](../../issues). 30 | 31 | Pull requests are also very welcome. The code is documented. Do not hesitate to ask for any guidance, if needed. 32 | 33 | ## Installation 34 | 35 | - First, open the Freeplane preferences window (menu `Tools > Preferences`). Go to the tab `Plugins`, section `Scripting`, and set "script execution enabled" to "Yes". 36 | - Download the *bookmarks-vX.X.X.addon.mm* file from [the latest release](../../releases). 37 | - Open this file with Freeplane and follow the instructions. 38 | 39 | ## Update 40 | 41 | To update the add-on, just install its newer version. No need to uninstall it first. 42 | 43 | **Important :** If you have created bookmarks with a version of this add-on prior to v0.5.1, you have to update your maps that contain bookmarks. Use the *"Bookmarks>Tools>Update map with bookmarks defined by a previous version of the add-on"* feature. 44 | 45 | ## Disclaimer 46 | 47 | I have used this add-on for myself since almost two years on a daily basis, and I've never had any problems. The most downloaded version was downloaded 115 times and no one have reported problems. I think it's OK, but still I prefer to warn: use this add-on at your own risks. 48 | 49 | If you want more information consider these 2 points : 50 | 51 | - The add-on modify a little bit how Freeplane works. It introduce a monitoring function that prevent the user to add a named bookmark icon the regular way. This is needed to ensure the named bookmarks consistency. It also prevent a named bookmark to be copied when a node with a named bookmark is copied, because a named bookmark must be unique. In this case, the node is copied without its bookmark. 52 | Then, each time a icon is modified by the user, or each time a node is created, an extra piece of code is executed for this monitoring. This *may* have some negative side effects. 53 | 54 | - The add-on write the data about the named bookmarks within the map storage area, which is saved within the map file. In the worse scenario, a bug in the add-on may corrupt the map file. 55 | 56 | Again, I've never notice any problem, but feedback about these points will be appreciated ! 57 | 58 | ## TODO - Ideas 59 | 60 | *Legend: 61 | `[ ]` = To do 62 | `[?]` = To do, but is it a good idea ? 63 | `[n]` = (n is a number) To do, lower number means higher priority* 64 | 65 | --- 66 | 67 | `[1]` Update this readme 68 | `[1]` Update the documentation about "remove all bookmarks in subtree" 69 | `[1]` Improve help text about show/hide clones in help tooltip. 70 | `[1]` [Add to context menu](https://sourceforge.net/p/freeplane/discussion/758437/thread/ec280c4e/?page=1&limit=25#5f30) 71 | 72 | --- 73 | 74 | `[2]` Allow to sort the named bookmarks list by name 75 | `[2]` Allow to display anonymous bookmarks first when dialogs open, or remember the last state, or select the state according to the currently selected node 76 | `[2]` Add an icon to the menu and the package 77 | `[2]` May it be possible to display only the bookmarks, and restore the previous state afterward ? 78 | 79 | --- 80 | 81 | `[3]` Continue to reduce duplicated code 82 | `[3]` Remove JMap return from functions that modify namedBookmarks 83 | `[3]` Allow to add and remove named bookmarks directly with icon manipulation 84 | `[3]` Add option to warn when deleting a named bookmark 85 | `[3]` Add a warning on creation of a named bookmark with an already used name 86 | 87 | --- 88 | 89 | `[?]` Make it compatible with aliases (define bookmarks as aliases, allow multi-characters names) 90 | `[?]` Handle bookmarks appearance with conditional styles 91 | `[?]` Implement temporary bookmarks (vanish when map is closed) 92 | `[?]` A "jump to link anchor" feature 93 | `[?]` Move/Copy nodes to bookmark (or link anchor) 94 | `[?]` Global bookmarks (can be used to reach closed maps) 95 | `[?]` Move menu into Navigation menu 96 | `[?]` Search bookmark by name 97 | `[?]` Bookmark name edition 98 | 99 | 100 | ## Build 101 | 102 | If you want to build the add-on installation file `bookmarks-vX.X.X.addon.mm` yourself, you have to build the library before to package the add-on. 103 | 104 | - Install Freeplane (of course !) 105 | - Download the source 106 | - Install gradle 107 | - Open `build.gradle` with a text editor and modify the paths in `repositories.dirs[]` to point to your Freeplane installation 108 | - Get a command prompt at the root of the sources folder 109 | - `gradle build` will create the file lib/bookmarks.jar 110 | 111 | Now you can open `bookmarks.mm` with Freeplane and package the add-on with [Tools > Developer Tools > Package add-on for publication](https://freeplane.sourceforge.io/wiki/index.php/Add-ons_(Develop)). This will create the file `bookmarks-vx.x.x.addon.mm`. Open this file with Freeplane install the add-on. 112 | -------------------------------------------------------------------------------- /doc/Map for demo.mm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /doc/Carte pour demo.mm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/main/groovy/ChangeListener.groovy: -------------------------------------------------------------------------------- 1 | package lilive.bookmarks 2 | 3 | import org.freeplane.features.map.INodeChangeListener 4 | import org.freeplane.features.map.NodeChangeEvent 5 | import org.freeplane.features.map.IMapChangeListener 6 | import org.freeplane.features.map.NodeModel 7 | import org.freeplane.features.mode.Controller 8 | import org.freeplane.features.icon.factory.IconStoreFactory 9 | import org.freeplane.features.icon.IconStore 10 | import org.freeplane.features.icon.MindIcon 11 | import org.freeplane.plugin.script.proxy.Convertible 12 | import org.freeplane.plugin.script.proxy.ScriptUtils 13 | import java.lang.StringBuilder 14 | import java.util.Enumeration 15 | import org.freeplane.features.map.MapController 16 | 17 | public class ChangeListener implements INodeChangeListener, IMapChangeListener 18 | { 19 | public void nodeChanged(NodeChangeEvent event) 20 | { 21 | // When an icon is added or removed form a node, we must take 22 | // care about the bookmarks icons : 23 | // - A bookmark icon must be unique in a node 24 | // - A bookmark icon is the first icon of a node 25 | // - A named-bookmark icon can be set only for a node which is 26 | // referenced to be a named-bookmarked node 27 | // - It is forbidden to remove named-bookmark icon for a node 28 | // which is referenced to be a named-bookmarked node 29 | 30 | Object property = event.property 31 | NodeModel node = event.node 32 | 33 | // An icon has been added or deleted 34 | if (NodeModel.NODE_ICON.equals(property)){ 35 | 36 | // An icon has been deleted 37 | if( event.oldValue ){ 38 | 39 | String oldIcon = event.oldValue.name 40 | if( oldIcon == Bookmarks.namedIcon ){ 41 | // Prevent named bookmark icon to be removed from named bookmarked nodes, 42 | // and be sure it is at the first position 43 | if( isNodeHasNamedBookmark( node ) ) putUniqueIconAsFirstIcon( node, Bookmarks.namedIcon ) 44 | } 45 | } 46 | 47 | // An icon has been added 48 | if( event.newValue ){ 49 | 50 | String newIcon = event.newValue.name 51 | if( newIcon == Bookmarks.namedIcon ){ 52 | // If this is a named bookmark icon 53 | 54 | // Be sure a named bookmark icon is at the first position ... 55 | if( isNodeHasNamedBookmark( node ) ) putUniqueIconAsFirstIcon( node, Bookmarks.namedIcon ) 56 | // ... and that a named bookmark icon is added only to named bookmarked nodes 57 | else removeIcon( node, Bookmarks.namedIcon ) 58 | 59 | } else if( newIcon == Bookmarks.anonymousIcon ){ 60 | // If this is a regular bookmark icon 61 | 62 | if( isNodeHasNamedBookmark( node ) ){ 63 | // Prevent regular bookmark icon to be placed on named bookmarked nodes 64 | removeIcon( node, Bookmarks.anonymousIcon ) 65 | // Be sure the named bookmark icon is present at the first position 66 | putUniqueIconAsFirstIcon( node, Bookmarks.namedIcon ) 67 | } else { 68 | // Be sure the bookmark icon is present at the first position, and unique 69 | putUniqueIconAsFirstIcon( node, Bookmarks.anonymousIcon ) 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | public void onNodeInserted( NodeModel parent, NodeModel child, int newIndex ) 77 | { 78 | // When a node is inserted, we must take care about its named-bookmark 79 | // icon, because it can be a copy of another node named-bookmarked. 80 | // But only one node can be named-bookmarked with a single keyboard key, 81 | // so we must remove the named-bookmark icon from the copy. 82 | 83 | IconStore iconStore = IconStoreFactory.ICON_STORE 84 | MindIcon icon = iconStore.getMindIcon( Bookmarks.namedIcon ) 85 | 86 | def map = ScriptUtils.node().map 87 | StringBuilder named = new StringBuilder() 88 | def stored = map.storage.getAt( Bookmarks.storageKey ) 89 | if( stored ) named = new StringBuilder( stored.getString() ) 90 | 91 | purgeBranchFromBadNamedIcons( child, icon, named ) 92 | } 93 | 94 | // Return true if a node is referenced as a name-bookmarked node 95 | private boolean isNodeHasNamedBookmark( NodeModel node ) 96 | { 97 | def map = ScriptUtils.node().map 98 | String mapStorage = "" 99 | def stored = map.storage.getAt( Bookmarks.storageKey ) 100 | if( stored ) mapStorage = stored.getString(); 101 | return mapStorage.contains( '"' + node.getID() + '"' ) 102 | } 103 | 104 | // Add an icon at the first position for this node 105 | // Ensure this icon is unique 106 | private void putUniqueIconAsFirstIcon( NodeModel node, String iconName ) 107 | { 108 | def icons = node.icons 109 | int i = icons.size() - 1 110 | while( i > 0 ){ 111 | MindIcon icon = icons.get( i ) 112 | if( icon.name == iconName ) node.removeIcon( i ) 113 | i-- 114 | } 115 | 116 | if( icons.size() > 0 && icons.get( 0 ).name == iconName ) return 117 | 118 | IconStore iconStore = IconStoreFactory.ICON_STORE 119 | MindIcon iconToAdd = iconStore.getMindIcon( iconName ) 120 | node.addIcon( iconToAdd, 0 ) 121 | } 122 | 123 | // Add an icon at the first position for this node 124 | // Ensure this icon is unique 125 | private void putUniqueIconAsFirstIcon( NodeModel node, MindIcon icon ) 126 | { 127 | def icons = node.icons 128 | int i = icons.size() - 1 129 | while( i > 0 ){ 130 | MindIcon ic = icons.get( i ) 131 | if( ic.name == icon.name ) node.removeIcon( i ) 132 | i-- 133 | } 134 | 135 | if( icons.size() > 0 && icons.get( 0 ).name == icon.name ) return 136 | node.addIcon( icon, 0 ) 137 | } 138 | 139 | // Remove an icon from the node 140 | private void removeIcon( NodeModel node, String iconName ) 141 | { 142 | def icons = node.icons 143 | int i = icons.size() - 1 144 | while( i >= 0 ){ 145 | MindIcon icon = icons.get( i ) 146 | if( icon.name == iconName ) node.removeIcon( i ) 147 | i-- 148 | } 149 | } 150 | 151 | // Remove an icon from the node, and fire a change event 152 | private void removeIcon( NodeModel node, MindIcon icon, boolean fireEvent ) 153 | { 154 | def icons = node.icons 155 | boolean changed = false 156 | int i = icons.size() - 1 157 | while( i >= 0 ){ 158 | MindIcon ic = icons.get( i ) 159 | if( ic.name == icon.name ){ 160 | node.removeIcon( i ) 161 | changed = true 162 | } 163 | i-- 164 | } 165 | if( changed && fireEvent ){ 166 | NodeChangeEvent event = new NodeChangeEvent( 167 | node, NodeModel.NODE_ICON, icon, null, true, true 168 | ) 169 | node.fireNodeChanged( event ) 170 | } 171 | } 172 | 173 | // Remove the named bookmark icon for all the nodes in this branch 174 | // which are not referenced as bookmarked nodes 175 | private void purgeBranchFromBadNamedIcons( NodeModel node, MindIcon icon, StringBuilder named ) 176 | { 177 | boolean isNamed = named.indexOf( '"' + node.getID() + '"' ) >= 0 178 | if( ! isNamed ){ 179 | // Remove the icon and fire the event (to refresh the view) 180 | removeIcon( node, icon, true ) 181 | } 182 | Enumeration children = node.children() 183 | while( children.hasMoreElements() ){ 184 | purgeBranchFromBadNamedIcons( children.nextElement(), icon, named ) 185 | } 186 | } 187 | 188 | // Put this listener on node change events and map change events 189 | static public void startMonitor() 190 | { 191 | MapController mapController = Controller.currentModeController.mapController 192 | boolean started = mapController.nodeChangeListeners.any{ it.class.name == ChangeListener.class.name } 193 | if( started ) return 194 | ChangeListener listener = new ChangeListener() 195 | mapController.addNodeChangeListener( listener ) 196 | mapController.addMapChangeListener( listener ) 197 | } 198 | 199 | // Remove listener on node change events and map change events 200 | static public void stopMonitor() 201 | { 202 | MapController mapController = Controller.currentModeController.mapController 203 | mapController.nodeChangeListeners 204 | .findAll{ it.class.name == ChangeListener.class.name } 205 | .each { mapController.removeNodeChangeListener(it) } 206 | mapController.mapChangeListeners 207 | .findAll{ it.class.name == ChangeListener.class.name } 208 | .each { mapController.removeMapChangeListener(it) } 209 | } 210 | } 211 | 212 | -------------------------------------------------------------------------------- /src/main/groovy/CreateOrDeleteGUI.groovy: -------------------------------------------------------------------------------- 1 | package lilive.bookmarks 2 | 3 | import lilive.bookmarks.Bookmarks as BM 4 | import groovy.swing.SwingBuilder 5 | import java.awt.BorderLayout 6 | import java.awt.event.* 7 | import java.util.Map as JMap 8 | import javax.swing.BoxLayout 9 | import javax.swing.JFrame 10 | import org.freeplane.api.MindMap 11 | import org.freeplane.core.ui.components.UITools 12 | import org.freeplane.plugin.script.proxy.Proxy.Controller as ProxyController 13 | import org.freeplane.plugin.script.proxy.Proxy.Node as ProxyNode 14 | import org.freeplane.plugin.script.proxy.ScriptUtils 15 | 16 | /** 17 | * Display a window for bookmark creation or deletion 18 | * Usage: CreateOrDeleteGUI.show() 19 | */ 20 | public class CreateOrDeleteGUI 21 | { 22 | public static void show( ProxyNode node, JMap namedBookmarks ) 23 | { 24 | // Collect informations about the current node 25 | Boolean isAnonymousBookmark = BM.isAnonymousBookmarked( node ) 26 | Boolean isNamedBookmark = BM.isNamedBookmarked( node, namedBookmarks ) 27 | 28 | // Buid the GUI 29 | Object gui = createWindow( node, isAnonymousBookmark, isNamedBookmark, namedBookmarks ) 30 | addKeyListener( gui, node, isAnonymousBookmark, isNamedBookmark, namedBookmarks ) 31 | 32 | // Center the GUI over the freeplane window 33 | gui.setLocationRelativeTo( UITools.currentFrame ) 34 | gui.visible = true 35 | } 36 | 37 | // Create the graphical user interface to display 38 | private static Object createWindow( 39 | ProxyNode node, 40 | Boolean isAnonymousBookmark, Boolean isNamedBookmark, 41 | JMap namedBookmarks 42 | ) 43 | { 44 | MindMap map = node.map 45 | Object gui 46 | groovy.swing.SwingBuilder.build 47 | { 48 | gui = dialog( 49 | title: BM.gtt( 'T_BM_win_title' ), 50 | modal: true, 51 | owner: UITools.currentFrame, 52 | defaultCloseOperation: JFrame.DISPOSE_ON_CLOSE, 53 | pack: true 54 | ){ 55 | borderLayout() 56 | 57 | // Fist panel : instructions 58 | panel( 59 | border: emptyBorder( 10 ), 60 | constraints: BorderLayout.PAGE_START, 61 | ){ 62 | boxLayout(axis: BoxLayout.Y_AXIS ) 63 | if( isAnonymousBookmark ) 64 | { 65 | label( 66 | icon: BM.getWarningIcon(), 67 | text: BM.gtt( 'T_node_already_BM' ), 68 | border: emptyBorder( 0, 0, 5, 0 ) 69 | ) 70 | label( "${ BM.gtt( 'T_kbd_backspace' ) } : ${ BM.gtt( 'T_delete_BM' ) }" ) 71 | label( "${ BM.gtt( 'T_kbd_other' ) } : ${ BM.gtt( 'T_replace_by_NBM' ) }" ) 72 | } 73 | else if( isNamedBookmark ) 74 | { 75 | label( 76 | text: BM.gtt( 'T_node_already_NBM' ), 77 | icon: BM.getWarningIcon(), 78 | border: emptyBorder( 0, 0, 5, 0 ) 79 | ) 80 | label( "${ BM.gtt( 'T_kbd_backspace' ) } : ${ BM.gtt( 'T_delete_BM' ) }" ) 81 | label( "${ BM.gtt( 'T_kbd_space' ) } : ${ BM.gtt( 'T_replace_by_SBM' ) }" ) 82 | label( "${ BM.gtt( 'T_kbd_other' ) } : ${ BM.gtt( 'T_change_NBM_name' ) }" ) 83 | } 84 | else 85 | { 86 | label( "${ BM.gtt( 'T_kbd_space' ) } : ${ BM.gtt( 'T_create_SBM' ) }" ) 87 | label( "${ BM.gtt( 'T_kbd_other' ) } : ${ BM.gtt( 'T_create_NBM' ) }" ) 88 | } 89 | label( "${ BM.gtt( 'T_kbd_esc' ) } ${ BM.gtt( 'T_to_cancel' ) }" ) 90 | } 91 | 92 | // Second panel : only if named bookmarks exists 93 | if( namedBookmarks.size() > 0 ) 94 | { 95 | panel( 96 | border:titledBorder( BM.gtt( 'T_NBM_are' ) ), 97 | constraints: BorderLayout.CENTER 98 | ){ 99 | boxLayout( axis: BoxLayout.Y_AXIS ) 100 | namedBookmarks.each 101 | { 102 | key, id -> 103 | // Display a line of text with the associated key 104 | // and the 30 first caracters of the node 105 | ProxyNode target = map.node( id ) 106 | String text = 107 | "" + 108 | String.valueOf( (char) Integer.parseInt( key ) ) + 109 | " : " + 110 | BM.getNodeShortPlainText( target ) + 111 | "" 112 | if( id == node.id ) label( text: text, icon: BM.getWarningIcon() ) 113 | else label( text ) 114 | } 115 | } 116 | } 117 | } 118 | } 119 | return gui 120 | } 121 | 122 | // Create the response to the user key press 123 | private static void addKeyListener( 124 | Object gui, 125 | ProxyNode node, 126 | Boolean isAnonymousBookmark, Boolean isNamedBookmark, 127 | JMap namedBookmarks 128 | ) 129 | { 130 | ProxyController c = ScriptUtils.c() 131 | gui.setFocusable(true) 132 | gui.addKeyListener( 133 | new java.awt.event.KeyAdapter() 134 | { 135 | @Override 136 | public void keyTyped(KeyEvent e) 137 | { 138 | // Get the key pressed 139 | char chr = e.getKeyChar() 140 | int keyCharCode = (int) chr 141 | 142 | if( keyCharCode == 8 ) 143 | { 144 | if( isNamedBookmark ) 145 | { 146 | namedBookmarks = BM.deleteNamedBookmark( node, namedBookmarks ) 147 | gui.dispose() 148 | Utils.setStatusInfo( 149 | BM.gtt( 'T_node_no_BM_anymore' ), 150 | 'button_cancel' 151 | ) 152 | } 153 | else if( isAnonymousBookmark ) 154 | { 155 | BM.deleteAnonymousBookmark( node ) 156 | gui.dispose() 157 | Utils.setStatusInfo( 158 | BM.gtt( 'T_node_no_BM_anymore' ), 159 | 'button_cancel' 160 | ) 161 | } 162 | } 163 | else if( keyCharCode == 32 ) 164 | { 165 | if( isNamedBookmark ) 166 | { 167 | namedBookmarks = BM.deleteNamedBookmark( node, namedBookmarks ) 168 | namedBookmarks = BM.createAnonymousBookmark( node ) 169 | gui.dispose() 170 | Utils.setStatusInfo( 171 | BM.gtt( 'T_node_now_SBM' ), 172 | 'button_ok' 173 | ) 174 | } 175 | else if( ! isAnonymousBookmark ) 176 | { 177 | BM.createAnonymousBookmark( node ) 178 | gui.dispose() 179 | Utils.setStatusInfo( 180 | BM.gtt( 'T_node_now_SBM' ), 181 | 'button_ok' 182 | ) 183 | } 184 | } 185 | else if( keyCharCode > 32 && keyCharCode != 127 && keyCharCode < 256 ) 186 | { 187 | if( isAnonymousBookmark ) BM.deleteAnonymousBookmark( node ) 188 | namedBookmarks = BM.createNamedBookmark( node, keyCharCode, namedBookmarks ) 189 | gui.dispose() 190 | Utils.setStatusInfo( 191 | BM.gtt( 'T_node_now_NBM' ) + ' "' + chr + "'", 192 | 'button_ok' 193 | ) 194 | } 195 | else if( keyCharCode == 27 ) 196 | { 197 | Utils.setStatusInfo( 198 | BM.gtt( 'T_BM_operation_aborded' ), 199 | 'messagebox_warning' 200 | ) 201 | gui.dispose() 202 | } 203 | } 204 | } 205 | ) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /zips/doc/bookmarks/help_en.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bookmarks add-on for Freeplane - Help 6 | 7 | 8 | 9 | 10 | 11 |
12 |

How to use the bookmarks add-on for Freeplane

13 |

Voir en français

14 |
15 |

Summary

16 | 31 |
32 |
33 |

Define keyboard shortcuts

34 |

The add-on doesn't define keyboard shortcuts by default (because they may overwrite your own custom shortcuts). But I recommend to bind some handy keyboards shortcuts to the add-on menu entries. This will make the add-on really convenient to use, because it was mainly designed to provide a fast map navigation with the keyboard.

35 |

For example, here's my own shortcuts:

36 |

Main commands

37 |
    38 |
  • Add / Remove a bookmark: Ctrl-Maj-B
  • 39 |
  • Toggle the bookmark: Ctrl-Maj-T
  • 40 |
  • Jump to bookmark: Ctrl-Maj-J
  • 41 |
42 |

Other commands

43 |
    44 |
  • Jump to previous bookmark: Ctrl-Maj-Up
  • 45 |
  • Jump to next bookmark: Ctrl-Maj-Down
  • 46 |
  • Links > Create link in bookmarked node: Ctrl-Maj-I ( "In" - inside the selected node )
  • 47 |
  • Links > Create link to bookmarked node: Ctrl-Maj-O ( "Out" - outside the selected node )
  • 48 |
49 |

To define a keyboard shortcut in Freeplane, maintain the Ctrl key pressed and click a menu entry.

50 |
51 |
52 |

2 types of bookmarks

53 |

The add-on let you create 2 types of bookmarks:

54 |
    55 |
  • Standard bookmarks. A node with this kind of bookmark is a node with the purple icon. It is not different than any other node with an icon. The add-on use this icon to detect it.
  • 56 |
  • Named bookmarks. A node with this kind of bookmark is a node with the green icon, and a "name". This name is in fact a single letter, digit, or any single key from the keyboard. This name allow quick access to the bookmark. Such a bookmark can be use, for example, to mark the most important nodes in the map.
  • 57 |
58 |

59 |
60 |
61 |

Create a bookmark

62 |

To create a bookmark in a node, select the node then use the Add / Remove a bookmark command.

63 |

This open a dialog which show you the existing named bookmarks and the possible actions:

64 |

65 |
    66 |
  • Press space to create a standard bookmark
  • 67 |
  • Type any other key on the keyboard to create a named bookmark
  • 68 |
69 |

70 |

If you create a bookmark with a name that is already used elsewhere, the actual bookmark with this name is erased.
Example with the letter A:

71 |

72 |

Another way to create a standard bookmark is to use the Toggle the bookmark command:

73 |

74 |
75 |
76 |

Jump to a bookmark

77 |

To jump to a bookmark, use the Jump to bookmark command.

78 |

This open a dialog that let you pick the desired bookmark. You can select the bookmark with the keyboard or the mouse.

79 |

80 |

Use the key Tab to switch between standard bookmarks and named bookmarks.

81 |

82 |

You can jump to the previous/next bookmark with the commands Jump to previous bookmark and Jump to next bookmark:

83 |

84 |
85 |
86 |

Remove a bookmark

87 |

To remove a bookmark, select its node, choose the Add / Remove a bookmark command and press the Backspace key:

88 |

89 |

You can also use the Toggle the bookmark command:

90 |

91 |
92 |
93 |

Make links

94 |

In the selected node, you can create a regular Freeplane link that target any bookmarked node.

95 |

Select the node where you want to create the link and use the Links > Create link to bookmarked node command. Then select the bookmark target of the link.

96 |

For example, I want to create in a node a link to the node "Chapter 2":

97 |

98 |

Tip: you can use the same command to create a link to the node memorized with the Freeplane command Edit > Link > Set link anchor (Ctrl-M for me). In this case, press the Space key instead of picking a bookmark.

99 |

For exemple, I want to create in a node a link to the node "Conclusion":

100 |

101 |

You may also want to create a link in a bookmarked node that target the selected node.

102 |

In this case use the Links > Create link in bookmarked node command, then select the bookmarked node where to create the link.

103 |

For example, I want to create in the node "Chapter 2" a link to another node:

104 |

105 |

Tip: again, you can create the link into the node memorized with the Freeplane command Edit > Link > Set link anchor by pressing the Space key.

106 |
107 |
108 |

Other tools

109 |
110 |

Convert bookmarks to or from Freeplane builtin bookmark icons

111 |

For non users of this add-on, Freeplane features an icon that could be use as a bookmark icon. The add-on doesn't make use of it, but it is possible to bookmark all the nodes that contain it. Use the Tools > Convert all regular FreePlane bookmark icons (star icon) to add-on bookmarks command to replace this icon by the add-on standard bookmark icon.

112 |

113 |

You can do the inverse operation with the Tools > Convert all bookmarks to regular FreePlane bookmark icons (star icon) command. This could be useful if someday you want to stop using this add-on and want to keep your bookmarks. Of course, you have to use this command before deinstall the add-on. Take care, this operation is not reversible and delete the bookmarks names.

114 |
115 |
116 |

Update bookmarks from a previous version of the add-on

117 |

If after an update of this add-on you've got an incorrect behavior, consider to use the command Tools > Fix named bookmarks created by an old version of the add-on. But don't worry to much: at the moment, the only persons who need this command are the persons who used the version 0.5.0-beta.

118 |
119 |
120 |
121 |

Get more help

122 |

Feel free to add your question to the dedicated discussion on the Freeplane forum.

123 |

You can also share your concerns by opening an issue in github.

124 |
125 |
126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /zips/doc/bookmarks/help_fr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Marque-pages pour Freeplane - Aide 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Utilisation du module de Marque-pages pour Freeplane

13 |

Display in english

14 |
15 |

Sommaire

16 | 31 |
32 |
33 |

Définir des raccourcis clavier

34 |

Ce module ne définit pas de raccourcis claviers par défaut pour accéder à ses fonctionnalités (car ils pourraient remplacer vos raccourcis claviers existants). Je recommande de créer des raccourcis claviers bien choisis vers les différentes commandes du menu du module. Cela le rendra beaucoup plus pratique à utiliser, puisqu'il a été conçu dans l'idée de faciliter grandement la navigation dans les cartes à l'aide du clavier.

35 |

Voici pour exemple les raccourcis que j'utilise :

36 |

Commandes principales

37 |
    38 |
  • Placer / Supprimer un marque-page : Ctrl-Maj-BB comme Bookmark )
  • 39 |
  • Basculer un marque-page : Ctrl-Maj-TT comme Toggle )
  • 40 |
  • Atteindre un marque-page : Ctrl-Maj-JJ comme Jump )
  • 41 |
42 |

Je me suis basé sur les mots anglais pour mes raccourcis, mais j'aurais aussi bien pu utiliser Ctrl-Maj-M,Ctrl-Maj-B et Ctrl-Maj-A pour utiliser les initiales des mots français correspondants.

43 |

Autres commandes

44 |
    45 |
  • Atteindre le marque-page précédent : Ctrl-Maj-Haut
  • 46 |
  • Atteindre le marque-page suivant : Ctrl-Maj-Bas
  • 47 |
  • Liens > Créer un lien dans un marque-page : Ctrl-Maj-I ( "In" - dans le nœud sélectionné )
  • 48 |
  • Liens > Créer un lien vers un marque-page : Ctrl-Maj-O ( "Out" - hors du nœud sélectionné )
  • 49 |
50 |

Pour définir un raccourci clavier dans Freeplane, cliquer sur une entrée de menu en maintenant la touche Ctrl enfoncée, puis choisir la combinaison de touches à y associer.

51 |
52 |
53 |

Deux types de marque-pages

54 |

Le module permet d'utiliser deux sortes de marque-page:

55 |
    56 |
  • Le marque-page standard. Un nœud avec un tel marque-page contient l'icône de marque-page violette. Il n'est pas différent d'un autre nœud de Freeplane qui contient une icône, si ce n'est qu'il est reconnu par le module comme nœud marqué.
  • 57 |
  • Le marque-page nommé. Un nœud avec un tel marque-page contient l'icône de marque-page verte et a un "nom". Ce nom est une unique lettre, chiffre, ou autre touche du clavier. Il permet un accès rapide à ce nœud. On peut utiliser ce marque-page pour des nœud particulièrement important ou visités de la carte.
  • 58 |
59 |

60 |
61 |
62 |

Créer un marque-page

63 |

Pour créer un marque-page, utiliser la commande Placer / Supprimer un marque-page

64 |

Ceci ouvre une boîte de dialogue qui montre les marque-pages existants, et qui explique les actions possibles :

65 |

66 |
    67 |
  • Enfoncer la touche espace pour créer un marque-page standard
  • 68 |
  • Ou toute autre touche du clavier pour créer un marque-page nommé
  • 69 |
70 |

71 |

Si vous choisissez un nom qui est déjà utilisé par un autre marque-page nommé, ce marque-page sera effacé.
Voici ce que cela donne pour un marque-page nommé A :

72 |

73 |

Une autre façon de définir rapidement un marque-page standard est d'utiliser la commande Basculer un marque-page :

74 |

75 |
76 |
77 |

Atteindre un marque-page

78 |

Pour aller à un nœud contenant un marque-page, utiliser la commande Atteindre un marque-page.

79 |

Ceci ouvre boîte de dialogue qui permet de choisir un marque-page. Vous pouvez choisir le marque-page avec le clavier ou la souris.

80 |

81 |

Utiliser la touche Tab pour basculer l'affichage entre les marque-pages standards et les marque-pages nommés.

82 |

83 |

Vous pouvez aussi sauter au marque-page précédent ou suivant avec les commandes Atteindre le marque-page précédent et Atteindre le marque-page suivant :

84 |

85 |
86 |
87 |

Effacer un marque-page

88 |

Pour supprimer un marque-page, utiliser la commande Placer / Supprimer un marque-page et appuyer sur la touche Effacement arrière :

89 |

90 |

vous pouvez aussi utiliser la commande Basculer un marque-page :

91 |

92 |
93 |
94 |

Créer des liens

95 |

Vous pouvez créer facilement un lien vers un nœud contenant un marque-page dans le nœud actuellement sélectionné.

96 |

Sélectionnez le nœud où créer le lien, puis utiliser la commande Liens > Créer un lien vers un marque-page. Choisissez alors le nœud marqué qui sera la cible du lien.

97 |

Par exemple, pour créer dans un nœud un lien vers le nœud "Chapitre 2" :

98 |

99 |

Astuce: vous pouvez également utiliser cette commande pour créer un lien vers le nœud mémorisé par la commande Édition > Lien > Mémoriser le nœud de Freeplane (Ctrl-M pour moi). Pour ceci, presser la touche espace plutôt que choisir un marque-page.

100 |

Par exemple, pour créer un lien vers le nœud "Conclusion" :

101 |

102 |

À l'inverse, vous pouvez créer un lien vers le nœud actuellement sélectionné dans un nœud marqué.

103 |

Pour cela, utiliser la commande Liens > Créer un lien dans un marque-page, puis choisir le marque-page du nœud où créer le lien.

104 |

Par exemple, pour créer un lien vers le nœud sélectionné dans le nœud "Chapitre 2" :

105 |

106 |

Astuce: vous pouvez également utiliser cette commande pour créer un lien dans le nœud mémorisé par la commande Édition > Lien > Mémoriser le nœud de Freeplane. Pour ceci, presser la touche espace plutôt que choisir un marque-page.

107 |
108 |
109 |

Outils divers

110 |
111 |

Convertir les marque-pages natifs de Freeplane en marque-pages du module, et vice-versa

112 |

Les utilisateurs de Freeplane qui n'utilisent pas ce module peuvent utiliser l'icône en forme d'étoile pour marquer des nœuds particuliers. Ce module n'utilise pas ces icônes comme marqueurs de marque-page. Par contre, il est possible de transformer ces icônes en marque-pages utilisables par ce module avec la commande Outils > Convertir toutes les icônes de marque-page de FreePlane (étoile) en marque-pages pour ce module :

113 |

114 |

L'opération inverse est également possible avec la commande Outils > Convertir tous les marque-pages en icônes de marque-page de FreePlane (étoile). Ceci peut être utile si vous souhaitez ne plus utiliser ce module mais voulez conserver les marque-pages que vous avez définis. Une fois que vous avez ainsi converti les marque-pages de vos cartes, vous pouvez désinstaller le module car vos nœud sont maintenant marqués avec les icônes en forme d'étoile de Freeplane. Attention, cette opération efface les noms des marque-pages nommés, et n'est pas réversible.

115 |
116 |
117 |

Mettre à jour les marque-pages d'une version précédente du module

118 |

Si après une mise-à-jour du module vous avez des comportements inattendus avec vos marque-pages, vous pouvez utiliser la commande Outils > Récupérer les marque-pages nommés d'une version antérieure du module. Ceci devrait résoudre le problème. Mais ne vous alarmez pas trop, cette commande sera utile seulement au petit nombre de personnes qui ont utilisé la version 0.5.0-beta.

119 |
120 |
121 |
122 |

Obtenir plus d'aide

123 |

N'hésitez pas à poser des questions sur le forum de Freeplane. Il y existe déjà une discussion sur ce module, ajoutez-y votre question.

124 |

Vous pouvez également faire part de vos problèmes en ouvrant une issue sur le dépot github du module.

125 |
126 |
127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/main/groovy/Bookmarks.groovy: -------------------------------------------------------------------------------- 1 | package lilive.bookmarks 2 | 3 | import groovy.json.JsonBuilder 4 | import groovy.json.JsonSlurper 5 | import java.awt.Graphics2D 6 | import java.awt.Image 7 | import java.awt.image.BufferedImage 8 | import java.util.Map as JMap 9 | import javax.swing.Icon 10 | import javax.swing.ImageIcon 11 | import javax.swing.UIManager 12 | import org.freeplane.api.MindMap as MindMap 13 | import org.freeplane.api.Node 14 | import org.freeplane.api.Properties 15 | import org.freeplane.core.util.TextUtils 16 | import org.freeplane.plugin.script.proxy.Convertible 17 | import org.freeplane.plugin.script.proxy.ScriptUtils 18 | 19 | class Bookmarks 20 | { 21 | 22 | final static String storageKey = "BookmarksKeys" 23 | final static String anonymousIcon = "bookmarks/Bookmark 1" 24 | final static String namedIcon = "bookmarks/Bookmark 2" 25 | 26 | // gtt = Get Translated Text 27 | static String gtt( String key ) 28 | { 29 | return TextUtils.getText( 'addons.bookmarks.' + key ) 30 | } 31 | 32 | // Create a java.util.Map where : 33 | // - the keys are the keyboard keys assigned to a bookmarked node; 34 | // - the values are the id of the corresponding bookmarked node. 35 | // This java.util.Map is read from the freeplane map storage area. 36 | static JMap loadNamedBookmarks( MindMap map ) 37 | { 38 | // Read the datas from the map storage 39 | Convertible stored = map.storage.getAt( storageKey ) 40 | 41 | // Convert these datas to an java.util.Map 42 | if( stored ) return new JsonSlurper().parseText( stored.getText() ) as JMap 43 | else return [:] 44 | } 45 | 46 | // Store the java.util.Map namedBookmarks in the freeplane map storage area. 47 | static void saveNamedBookmarks( JMap namedBookmarks, MindMap map ) 48 | { 49 | JsonBuilder builder = new JsonBuilder() 50 | builder( namedBookmarks ) 51 | map.storage.putAt( storageKey, builder.toString() ) 52 | } 53 | 54 | // Erase the stored named bookmarks. 55 | // Return true is something change. 56 | static Boolean eraseNamedBookmarksStorage( MindMap map ) 57 | { 58 | Convertible storage = map.storage.getAt( storageKey ) 59 | if( storage != null ) 60 | { 61 | map.storage.putAt( storageKey, null ) 62 | return true 63 | } 64 | else 65 | { 66 | return false 67 | } 68 | } 69 | 70 | // Remove in namedBookmarks all the node that don't exist, 71 | // and add the named bookmark icon to each referenced node that miss it. 72 | // Return the currated bookmarks map. 73 | static JMap fixNamedBookmarksInconsistency( JMap namedBookmarks, MindMap map ) 74 | { 75 | Boolean changed = namedBookmarks.removeAll 76 | { 77 | key, id -> 78 | return ( map.node( id ) == null ) 79 | } 80 | if( changed ) saveNamedBookmarks( namedBookmarks, map ) 81 | 82 | namedBookmarks.each 83 | { 84 | key, id -> 85 | Node n = map.node( id ) 86 | if( ! Bookmarks.hasNamedBookmarkIcon( n ) ) n.icons.add( Bookmarks.namedIcon ) 87 | } 88 | 89 | return namedBookmarks 90 | } 91 | 92 | // Return true if the node has the anonymous bookmark icon 93 | static Boolean hasAnonymousBookmarkIcon( Node node ) 94 | { 95 | return node.icons.contains( anonymousIcon ) 96 | } 97 | 98 | // Return true if the node is bookmarked with an anonymous bookmark icon 99 | // (actually it's the same than hasAnonymousBookmarkIcon() 100 | static Boolean isAnonymousBookmarked( Node node ) 101 | { 102 | return node.icons.contains( anonymousIcon ) 103 | } 104 | 105 | // Return true if the node has the named bookmark icon 106 | static Boolean hasNamedBookmarkIcon( Node node ) 107 | { 108 | return node.icons.contains( namedIcon ) 109 | } 110 | 111 | // Return true if the node appear in the namedBookmarks map 112 | static Boolean hasBookmarkName( Node node, JMap namedBookmarks ) 113 | { 114 | return namedBookmarks.containsValue( node.id ) 115 | } 116 | 117 | // Return true if the node is bookmarked with a named bookmark 118 | static Boolean isNamedBookmarked( Node node, JMap namedBookmarks ) 119 | { 120 | return hasNamedBookmarkIcon( node ) && hasBookmarkName( node, namedBookmarks ) 121 | } 122 | 123 | // Return true if the node is bookmarked (anonymous or named bookmark) 124 | static Boolean isBookmarked( Node node, JMap namedBookmarks ) 125 | { 126 | return isAnonymousBookmarked( node ) || isNamedBookmarked( node, namedBookmarks ) 127 | } 128 | 129 | static String getNodeShortPlainText( Node node ) 130 | { 131 | String text = node.plainText 132 | if( text.length() > 30 ) text = text[0..27] + " ..." 133 | return text 134 | } 135 | 136 | // Create an anonymous bookmark for this node. 137 | static void createAnonymousBookmark( Node node ) 138 | { 139 | if( ! node.icons.contains( anonymousIcon ) ) 140 | { 141 | node.icons.add( anonymousIcon ) 142 | } 143 | } 144 | 145 | // Remove an anonymous bookmark for this node. 146 | static void deleteAnonymousBookmark( Node node ) 147 | { 148 | while( node.icons.remove( anonymousIcon ) ){} 149 | } 150 | 151 | // Create a named bookmark for this node. 152 | // The name of the bookmark is a single keyboard key. 153 | static JMap createNamedBookmark( Node node, keyCharCode, JMap namedBookmarks ) 154 | { 155 | String s = String.valueOf( keyCharCode ) 156 | 157 | // If another node has this named bookmark, remove it 158 | if( namedBookmarks.containsKey( s ) ) 159 | { 160 | Node n = node.map.node( namedBookmarks[ s ] ) 161 | if( n && n != node ) deleteNamedBookmark( n, namedBookmarks ) 162 | } 163 | 164 | // Delete the existing named bookmarks assigned to the current node 165 | namedBookmarks.removeAll{ k, v -> v == node.id } 166 | 167 | // Assign the key pressed to the current node 168 | namedBookmarks[ s ] = node.id 169 | namedBookmarks = namedBookmarks.sort() 170 | 171 | // Save the new namedBookmarks map 172 | saveNamedBookmarks( namedBookmarks, node.map ) 173 | 174 | // Add the icon 175 | node.icons.add( namedIcon ) 176 | 177 | return namedBookmarks 178 | } 179 | 180 | // Delete an existing named bookmark 181 | static JMap deleteNamedBookmark( Node node, JMap namedBookmarks ) 182 | { 183 | if( namedBookmarks.containsValue( node.id ) ) 184 | { 185 | // Clear the map 186 | namedBookmarks.removeAll{ key, value -> value == node.id } 187 | 188 | // Save the new namedBookmarks map 189 | saveNamedBookmarks( namedBookmarks, node.map ) 190 | } 191 | 192 | // Remove the icon 193 | while( node.icons.remove( namedIcon ) ){} 194 | 195 | return namedBookmarks 196 | } 197 | 198 | // Delete any bookmark in this node 199 | static JMap deleteBookmark( Node node, JMap namedBookmarks ) 200 | { 201 | if( isAnonymousBookmarked( node ) ) deleteAnonymousBookmark( node ) 202 | if( isNamedBookmarked( node, namedBookmarks ) ) namedBookmarks = deleteNamedBookmark( node, namedBookmarks ) 203 | return namedBookmarks 204 | } 205 | 206 | /** 207 | * Delete all bookmarks in a node and its descendants 208 | * 209 | * @param node The root of the target subtree 210 | * @param anonymous Delete anonymous bookmarks 211 | * @param named Delete named bookmarks 212 | */ 213 | static void deleteSubTreeBookmarks( Node node, boolean anonymous = true, boolean named = true ) 214 | { 215 | if( ! anonymous && ! named ) return 216 | JMap namedBookmarks = loadNamedBookmarks( node.map ) 217 | namedBookmarks = fixNamedBookmarksInconsistency( namedBookmarks, node.map ) 218 | int size = namedBookmarks.size() 219 | namedBookmarks = deleteBookmarks( [node], namedBookmarks, true, anonymous, named ) 220 | } 221 | 222 | /** 223 | * Delete any bookmarks in this list of nodes. 224 | * Can delete all bookmarks in descendants nodes in recursive mode. 225 | * 226 | * @param nodes The nodes where to delete the bookmarks 227 | * @param namedBookmarks 228 | * @param recursive Also delete the bookmarks in the nodes children 229 | * @param anonymous Delete anonymous bookmarks 230 | * @param named Delete named bookmarks 231 | */ 232 | private static JMap deleteBookmarks( 233 | ArrayList< Node > nodes, 234 | JMap namedBookmarks, 235 | Boolean recursive = false, 236 | boolean anonymous = true, boolean named = true 237 | ){ 238 | if( ! nodes ) return namedBookmarks 239 | nodes.each{ 240 | node -> 241 | if( anonymous ) deleteAnonymousBookmark( node ) 242 | if( named ) deleteNamedBookmark( node, namedBookmarks ) 243 | if( recursive ){ 244 | namedBookmarks = deleteBookmarks( node.children, namedBookmarks, true, anonymous, named ) 245 | } 246 | } 247 | return namedBookmarks 248 | } 249 | 250 | // Create a list of all the nodes with an anonymous bookmarks. 251 | // Each element of this list is a map [ id, text ] where: 252 | // - id is the node id, 253 | // - text is the node text, possibly truncated. 254 | // It is possible to exclude some node from this list. 255 | static List getAllAnonymousBookmarkedNodes( MindMap map, List excludeIds = null ) 256 | { 257 | List bookmarks = [] 258 | map.root.findAll().each 259 | { 260 | n -> 261 | if( isAnonymousBookmarked( n ) ) 262 | { 263 | if( excludeIds && excludeIds.contains( n.id ) ) return 264 | String text = getNodeShortPlainText( n ) 265 | bookmarks << [ "id": n.id, "text": text ] 266 | } 267 | } 268 | return bookmarks 269 | } 270 | 271 | // Create a list of all the nodes with a named bookmarks. 272 | // Each element of this list is a map [ id, name, text ] where: 273 | // - id is the node id, 274 | // - name is the bookmark key (string) 275 | // - text is the node text, possibly truncated, without html format. 276 | // It is possible to exclude some node from this list. 277 | static List getAllNameBookmarkedNodes( MindMap map, List excludeIds = null ) 278 | { 279 | JMap bmksMap = loadNamedBookmarks( map ) 280 | bmksMap = fixNamedBookmarksInconsistency( bmksMap, map ) 281 | 282 | if( excludeIds ) 283 | { 284 | bmksMap.removeAll{ key, id -> excludeIds.contains( id ) } 285 | } 286 | 287 | List bmksList = bmksMap.collect 288 | { 289 | key, id -> 290 | Node node = map.node( id ) 291 | return [ 292 | "id": id, 293 | "name": String.valueOf( (char) Integer.parseInt( key ) ), 294 | "text": getNodeShortPlainText( node ) 295 | ] 296 | } 297 | 298 | return bmksList; 299 | } 300 | 301 | static ImageIcon getQuestionMarkIcon() 302 | { 303 | // Get a small question mark icon from the theme. 304 | // We can't simply call icon.getImage().getScaledInstance() because some themes (ie Nimbus) 305 | // do not return a suitable icon.getImage(). That's why we paint the icon. 306 | Icon srcIcon = UIManager.getIcon("OptionPane.questionIcon") 307 | int w = srcIcon.getIconWidth() 308 | int h = srcIcon.getIconHeight() 309 | BufferedImage bufferedImage = new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB ) 310 | Graphics2D g = bufferedImage.createGraphics() 311 | srcIcon.paintIcon( null, g, 0, 0 ); 312 | g.dispose() 313 | h = h / w * 16 314 | w = 16 315 | ImageIcon icon = new ImageIcon( bufferedImage.getScaledInstance( w, h, Image.SCALE_SMOOTH ) ) 316 | return icon 317 | } 318 | 319 | static ImageIcon getWarningIcon() 320 | { 321 | // Get a small warning icon from the theme. 322 | // We can't simply call icon.getImage().getScaledInstance() because some themes (ie Nimbus) 323 | // do not return a suitable icon.getImage(). That's why we paint the icon. 324 | Icon srcIcon = UIManager.getIcon("OptionPane.warningIcon") 325 | int w = srcIcon.getIconWidth() 326 | int h = srcIcon.getIconHeight() 327 | BufferedImage bufferedImage = new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB ) 328 | Graphics2D g = bufferedImage.createGraphics() 329 | srcIcon.paintIcon( null, g, 0, 0 ); 330 | g.dispose() 331 | h = h / w * 16 332 | w = 16 333 | ImageIcon icon = new ImageIcon( bufferedImage.getScaledInstance( w, h, Image.SCALE_SMOOTH ) ) 334 | return icon 335 | } 336 | 337 | } 338 | -------------------------------------------------------------------------------- /src/main/groovy/JumpGUI.groovy: -------------------------------------------------------------------------------- 1 | package lilive.bookmarks 2 | 3 | import lilive.bookmarks.Bookmarks as BM 4 | import groovy.swing.SwingBuilder 5 | import java.awt.BorderLayout 6 | import java.awt.Component 7 | import java.awt.GridBagConstraints 8 | import java.awt.event.* 9 | import java.util.Map as JMap 10 | import javax.swing.AbstractAction 11 | import javax.swing.InputMap 12 | import javax.swing.JComponent 13 | import javax.swing.JFrame 14 | import javax.swing.JList as SwingList 15 | import javax.swing.JPanel 16 | import javax.swing.KeyStroke 17 | import org.freeplane.api.MindMap as MindMap 18 | import org.freeplane.api.Node as Node 19 | import org.freeplane.core.ui.components.UITools as ui 20 | import org.freeplane.plugin.script.proxy.Proxy.Controller as ProxyController 21 | import org.freeplane.plugin.script.proxy.Proxy.Node as ProxyNode 22 | import org.freeplane.plugin.script.proxy.ScriptUtils 23 | import org.freeplane.plugin.script.FreeplaneScriptBaseClass 24 | 25 | 26 | /** 27 | * Display a window for jumping to an existing bookmark 28 | */ 29 | public class JumpGUI 30 | { 31 | static private Object gui 32 | 33 | // Build and display the window 34 | static public void show( ProxyNode node, FreeplaneScriptBaseClass.ConfigProperties config ) 35 | { 36 | MindMap map = node.map 37 | gui = null 38 | SwingBuilder swing = new SwingBuilder() 39 | 40 | // Find all the bookmarks 41 | List namedBookmarks = BM.getAllNameBookmarkedNodes( map ) 42 | List anonymousBookmarks = BM.getAllAnonymousBookmarkedNodes( map ) 43 | 44 | // Quit the script if there is no bookmarks 45 | if( ! namedBookmarks && ! anonymousBookmarks ) 46 | { 47 | ui.informationMessage( ui.currentFrame, BM.gtt( 'T_no_bookmarks' ) + " !", BM.gtt( 'T_BM_win_title' ) ) 48 | return 49 | } 50 | 51 | // Create the gui 52 | gui = createWindow( 53 | swing, node, map, 54 | namedBookmarks, anonymousBookmarks, 55 | ! config.getBooleanProperty("addon_bookmarks_hide_clones") 56 | ) 57 | 58 | // Center the gui over the freeplane window 59 | gui.pack() 60 | gui.setLocationRelativeTo( ui.currentFrame ) 61 | gui.visible = true 62 | } 63 | 64 | // Jump to a node after the gui close 65 | static private void jumpToNodeAfterGuiDispose( ProxyNode target, String message ) 66 | { 67 | // If the code to jump to a node is executed before the gui close, 68 | // it leave freeplane in a bad focus state. 69 | // This is solved by putting this code in a listener executed 70 | // after the gui destruction: 71 | 72 | ProxyController c = ScriptUtils.c() 73 | 74 | gui.addWindowListener( 75 | new WindowAdapter() 76 | { 77 | @Override 78 | public void windowClosed( WindowEvent event ) 79 | { 80 | c.select( target ) 81 | c.centerOnNode( target ) 82 | Utils.setStatusInfo( message ) 83 | } 84 | } 85 | ) 86 | } 87 | 88 | // Build a GUI list component that display the named bookmarks and 89 | // that allow to jump to them. 90 | static private SwingList getNamedBookmarksSwingList( 91 | SwingBuilder swing, 92 | ProxyNode node, MindMap map, 93 | List bookmarks // List of the bookmarks 94 | ){ 95 | if( ! bookmarks ) return null 96 | 97 | // Create the component 98 | SwingList component 99 | swing.build{ 100 | component = list( 101 | items: bookmarks.collect 102 | { 103 | // Display for each bookmark the name of the bookmark, 104 | // followed by the text of the node 105 | if( it.id != node.id ) "$it.name : $it.text" 106 | else "$it.name : $it.text" 107 | }, 108 | visibleRowCount: Integer.min( 20, bookmarks.size() ), 109 | border: emptyBorder( 5 ) 110 | ) 111 | } 112 | 113 | // Initialize the component state 114 | int selectedIdx = bookmarks.findIndexOf{ it.id == node.id } 115 | if( selectedIdx < 0 ) selectedIdx = 0; 116 | component.setSelectedIndex( selectedIdx ) 117 | component.ensureIndexIsVisible( selectedIdx ) 118 | 119 | // Set the user interactions 120 | 121 | ProxyController c = ScriptUtils.c() 122 | component.addKeyListener( 123 | new KeyAdapter() 124 | { 125 | public void keyReleased( KeyEvent e ) 126 | { 127 | int code = e.getKeyCode() 128 | char chr = e.getKeyChar() 129 | int idx = (int) chr 130 | if( code == KeyEvent.VK_ENTER ) 131 | { 132 | int i = component.getSelectedIndex() 133 | if( i >= 0 ) 134 | { 135 | JMap bm = bookmarks[ i ] 136 | Node target = map.node( bm.id ) 137 | jumpToNodeAfterGuiDispose( target, "${BM.gtt( 'T_jumped_to_NBM' )} \"$bm.name\"" ) 138 | gui.dispose() 139 | } 140 | } 141 | else if( idx > 32 && idx != 127 && idx < 256 ) 142 | { 143 | // Try to jump to the corresponding node id 144 | String s = String.valueOf( chr ) 145 | JMap bm = bookmarks.find{ it.name == s } 146 | if( bm ) 147 | { 148 | Node target = map.node( bm.id ) 149 | jumpToNodeAfterGuiDispose( target, "${BM.gtt( 'T_jumped_to_NBM' )} \"${s}\"" ) 150 | gui.dispose() 151 | } 152 | else 153 | { 154 | Utils.setStatusInfo( 155 | "${BM.gtt( 'T_no_node_with_key' )} \"${chr}\"", 156 | "messagebox_warning" 157 | ) 158 | } 159 | } 160 | } 161 | } 162 | ) 163 | 164 | component.addMouseListener( 165 | new MouseAdapter() 166 | { 167 | @Override public void mouseClicked(MouseEvent e) 168 | { 169 | int idx = component.getSelectedIndex() 170 | if( idx >= 0 ) 171 | { 172 | JMap bm = bookmarks[ idx ] 173 | Node target = map.node( bm.id ) 174 | jumpToNodeAfterGuiDispose( target, "${BM.gtt( 'T_jump_to_NBM' )} \"$bm.name\"" ) 175 | gui.dispose() 176 | } 177 | } 178 | } 179 | ) 180 | 181 | return component 182 | } 183 | 184 | // Create a JPanel for the named bookmarks 185 | static private JPanel getNamedBookmarksJPanel( 186 | SwingBuilder swing, 187 | List bookmarks, // List of the bookmarks 188 | SwingList bookmarksSwingList, // Component that display this list 189 | boolean showTabTip // Do we need to add a comment about the Tab key ? 190 | ){ 191 | if( ! bookmarks ) return null 192 | 193 | // List all the named bookmarks 194 | JPanel jPane 195 | swing.build{ 196 | jPane = swing.panel( 197 | border: emptyBorder( [0,0,10,0] ) 198 | ) 199 | { 200 | gridBagLayout() 201 | // This gridbag will contains 4 items 202 | // Row 0 : A label and a question mark label with a tooltip 203 | // Row 1 : A label 204 | // Row 2 : The kist of the named bookmarks 205 | 206 | // Row 0 207 | label( 208 | "${BM.gtt( 'T_select_NBM_to_jump' )}.", 209 | constraints: gbc( gridx:0, gridy:0, anchor:GridBagConstraints.LINE_START ) 210 | ) 211 | label( 212 | icon: BM.getQuestionMarkIcon(), 213 | toolTipText: 214 | """ 215 | ${ BM.gtt( 'T_tip_jump_to_NBM' )}: 216 |
    217 |
  • ${BM.gtt( 'T_press_red_key' )}
  • 218 |
  • ${BM.gtt( 'T_click_BM' )}
  • 219 |
  • ${BM.gtt( 'T_arrow_select' )}
  • 220 |
221 | """, 222 | constraints: gbc( gridx:1, gridy:0, anchor:GridBagConstraints.LINE_START, weightx:1, insets:[0,10,0,0] ) 223 | ) 224 | 225 | // Row 1 226 | if( showTabTip ) label( 227 | "${BM.gtt( 'T_tab_to_display_SBM' )}.", 228 | constraints: gbc( gridx:0, gridy:1, gridwidth:2, anchor:GridBagConstraints.LINE_START ) 229 | ) 230 | 231 | // Row 2 232 | scrollPane( 233 | border: emptyBorder( [10,0,0,0] ), 234 | constraints: gbc( gridx:0, gridy:2, gridwidth:2, fill:GridBagConstraints.HORIZONTAL, anchor:GridBagConstraints.LINE_START ) 235 | ) 236 | { 237 | widget( bookmarksSwingList ) 238 | } 239 | } 240 | } 241 | 242 | return jPane 243 | } 244 | 245 | // Build a GUI list component that display the anonymous bookmarks and 246 | // that allow to jump to them. 247 | static private SwingList getAnonymousBookmarksSwingList( 248 | SwingBuilder swing, 249 | ProxyNode node, MindMap map, 250 | List bookmarks, // List of the bookmarks 251 | boolean showClones // Do we display the clones ? 252 | ){ 253 | if( ! bookmarks ) return null 254 | 255 | ArrayList< String > labels = [] // Label for each row 256 | ArrayList< String > ids = [] // Node id for each row 257 | // Create labels and ids 258 | bookmarks.each{ 259 | bm -> 260 | 261 | String label 262 | // Bold for the currently selected node 263 | if( node.id == bm.id ) label = "$bm.text" 264 | else label = bm.text 265 | 266 | // Do we have clones ? 267 | Node n = map.node( bm.id ) 268 | List clones = n.getNodesSharingContent() 269 | if( clones ){ 270 | // Does any other clone already inserted in the list ? 271 | List i = ids.intersect( clones.collect{ it.id } ) 272 | if( i ){ 273 | // There is at least one clone in the list 274 | // Do we need to insert this one ? 275 | if( showClones ){ 276 | // Mark this node as a clone in the list 277 | List indices = i.collect{ id1 -> ids.findIndexOf( id2 -> id1 == id2 ) } 278 | int index = indices.max() 279 | labels.addAll( index + 1, [ " [clone] ${bm.text}" ] ) 280 | ids.addAll( index + 1, [ bm.id ] ) 281 | } 282 | } 283 | else { 284 | // Insert in the list the text of this first clone 285 | labels << label 286 | ids << bm.id 287 | } 288 | } else { 289 | // Insert the text of this node in the list 290 | labels << label 291 | ids << bm.id 292 | } 293 | } 294 | 295 | // Create the component 296 | SwingList component = swing.list( 297 | items: labels, 298 | visibleRowCount: Integer.min( 20, labels.size() ) 299 | ) 300 | 301 | // Initialize the component state 302 | int selectedIdx = ids.findIndexOf{ it == node.id } 303 | if( selectedIdx < 0 ) selectedIdx = 0 304 | component.setSelectedIndex( selectedIdx ) 305 | component.ensureIndexIsVisible( selectedIdx ) 306 | 307 | // Set the user interactions 308 | component.addKeyListener( 309 | new KeyAdapter() 310 | { 311 | @Override public void keyReleased(KeyEvent e) 312 | { 313 | int code = e.getKeyCode() 314 | if( code == KeyEvent.VK_ENTER ) 315 | { 316 | int idx = component.getSelectedIndex() 317 | if( idx >= 0 ) 318 | { 319 | Node target = map.node( ids[ idx ] ) 320 | jumpToNodeAfterGuiDispose( target, BM.gtt( 'T_jumped_to_SBM' ) ) 321 | gui.dispose() 322 | } 323 | } 324 | } 325 | } 326 | ) 327 | 328 | component.addMouseListener( 329 | new MouseAdapter() 330 | { 331 | @Override public void mouseClicked(MouseEvent e) 332 | { 333 | int idx = component.getSelectedIndex() 334 | if( idx >= 0 ) 335 | { 336 | Node target = map.node( ids[ idx ] ) 337 | jumpToNodeAfterGuiDispose( target, BM.gtt( 'T_jumped_to_SBM' ) ) 338 | gui.dispose() 339 | } 340 | } 341 | } 342 | ) 343 | 344 | return component 345 | } 346 | 347 | // Create a JPanel for the anonymous bookmarks 348 | static private JPanel getAnonymousBookmarksJPanel( 349 | SwingBuilder swing, 350 | List bookmarks, // List of the bookmarks 351 | SwingList bookmarksSwingList, // Component that display this list 352 | boolean showTabTip, // Do we need to add a comment about the Tab key ? 353 | boolean showClones // Do we display the clones ? 354 | ){ 355 | if( ! bookmarks ) return null 356 | 357 | // List all the named bookmarks 358 | JPanel jPane 359 | swing.build{ 360 | jPane = swing.panel( 361 | border: emptyBorder( [0,0,10,0] ) 362 | ) 363 | { 364 | gridBagLayout() 365 | // This gridbag will contains 4 items 366 | // Row 0 : A label and a question mark label with a tooltip 367 | // Row 1 : A label 368 | // Row 2 : The list of the bookmarks 369 | // Row 3 : A tip about the clones 370 | 371 | // Row 0 372 | label( 373 | "${BM.gtt( 'T_select_BM_to_jump' )}.", 374 | constraints: gbc( gridx:0, gridy:0, anchor:GridBagConstraints.LINE_START ) 375 | ) 376 | String clonesTip 377 | if( showClones ) clonesTip = BM.gtt( 'T_tip_jump_to_SBM_hide_clones' ) 378 | else clonesTip = BM.gtt( 'T_tip_jump_to_SBM_show_clones' ) 379 | label( 380 | icon: BM.getQuestionMarkIcon(), 381 | toolTipText: 382 | """ 383 | ${BM.gtt( 'T_tip_jump_to_SBM' )}: 384 |
    385 |
  • ${BM.gtt( 'T_click_BM' )}
  • 386 |
  • ${BM.gtt( 'T_arrow_select' )}
  • 387 |
388 | ${clonesTip} 389 | """, 390 | constraints: gbc( gridx:1, gridy:0, anchor:GridBagConstraints.LINE_START, weightx:1, insets:[0,10,0,0] ) 391 | ) 392 | 393 | // Row 1 394 | if( showTabTip ) label( 395 | "${BM.gtt( 'T_tab_to_display_NBM' )}.", 396 | constraints: gbc( gridx:0, gridy:1, gridwidth:2, anchor:GridBagConstraints.LINE_START ) 397 | ) 398 | 399 | // Row 2 400 | scrollPane( 401 | border: emptyBorder( [10,0,0,0] ), 402 | constraints: gbc( gridx:0, gridy:2, gridwidth:2, fill:GridBagConstraints.HORIZONTAL, anchor:GridBagConstraints.LINE_START ) 403 | ) 404 | { 405 | widget( bookmarksSwingList ) 406 | } 407 | } 408 | } 409 | 410 | return jPane 411 | } 412 | 413 | 414 | // Create the content of the window 415 | static private Object createGui( 416 | SwingBuilder swing, 417 | JPanel namedPanel, SwingList namedSwingList, 418 | JPanel anonymPanel, SwingList anonymSwingList 419 | ) 420 | { 421 | ProxyController c = ScriptUtils.c() 422 | Object gui 423 | swing.build{ 424 | gui = dialog( 425 | title: BM.gtt( 'T_jump_to_SBM' ), 426 | modal:true, 427 | owner: ui.currentFrame, 428 | defaultCloseOperation: JFrame.DISPOSE_ON_CLOSE 429 | ){ 430 | borderLayout() 431 | // A panel for all the compontents (needed to add an inner padding) 432 | panel( 433 | border: emptyBorder( 10 ), 434 | constraints: BorderLayout.PAGE_START 435 | ){ 436 | borderLayout() 437 | 438 | // First element of the main panel : 439 | // A panel which can contains 2 panels: 440 | // - one for the named bookmarks 441 | // - one for the anonymous bookmarks 442 | panel( constraints: BorderLayout.LINE_START ) 443 | { 444 | borderLayout() 445 | if( namedPanel ) widget( namedPanel, constraints: BorderLayout.PAGE_START ) 446 | if( anonymPanel ) widget( anonymPanel, constraints: BorderLayout.PAGE_END ) 447 | } 448 | 449 | // Second element of the main panel : 450 | label( text: "${BM.gtt( 'T_press_esc_cancel' )}.", constraints: BorderLayout.PAGE_END ) 451 | } 452 | } 453 | } 454 | 455 | // Set Esc key to close the script 456 | String onEscPressID = "onEscPress" 457 | InputMap inputMap = gui.getRootPane().getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ) 458 | inputMap.put( KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 ), onEscPressID ) 459 | gui.getRootPane().getActionMap().put( 460 | onEscPressID, 461 | new AbstractAction() 462 | { 463 | @Override 464 | public void actionPerformed( ActionEvent e ) 465 | { 466 | gui.dispose() 467 | Utils.setStatusInfo( 468 | BM.gtt( 'T_jump_aborded' ), 469 | 'button_cancel' 470 | ) 471 | } 472 | } 473 | ) 474 | 475 | if( namedSwingList ) 476 | { 477 | if( anonymPanel ) anonymPanel.visible = false 478 | namedSwingList.requestFocus() 479 | } 480 | else if( anonymSwingList ) 481 | { 482 | if( namedPanel ) namedPanel.visible = false 483 | anonymSwingList.requestFocus() 484 | } 485 | 486 | // Set the Tab key to switch between the 2 bookmarks types 487 | if( namedPanel && namedSwingList && anonymPanel && anonymSwingList ) 488 | { 489 | namedSwingList.setFocusTraversalKeysEnabled( false ) 490 | anonymSwingList.setFocusTraversalKeysEnabled( false ) 491 | String onTabPressID = "onTabPress" 492 | inputMap.put( KeyStroke.getKeyStroke( KeyEvent.VK_TAB, 0 ), onTabPressID ) 493 | gui.getRootPane().getActionMap().put( 494 | onTabPressID, 495 | new AbstractAction() 496 | { 497 | public void actionPerformed( ActionEvent e ) 498 | { 499 | if( namedPanel.visible ) 500 | { 501 | namedPanel.visible = false 502 | anonymPanel.visible = true 503 | anonymSwingList.requestFocus() 504 | } 505 | else 506 | { 507 | namedPanel.visible = true 508 | anonymPanel.visible = false 509 | namedSwingList.requestFocus() 510 | } 511 | gui.pack() 512 | gui.setLocationRelativeTo( ui.currentFrame ) 513 | } 514 | } 515 | ) 516 | } 517 | return gui 518 | } 519 | 520 | 521 | // Create the window 522 | static private Object createWindow( 523 | SwingBuilder swing, 524 | ProxyNode node, MindMap map, 525 | List namedBookmarks, List anonymousBookmarks, 526 | boolean showClones 527 | ) 528 | { 529 | SwingList namedBookmarksSwingList = getNamedBookmarksSwingList( 530 | swing, node, map, namedBookmarks 531 | ) 532 | JPanel namedBookmarksJPanel = getNamedBookmarksJPanel( 533 | swing, namedBookmarks, namedBookmarksSwingList, (boolean)anonymousBookmarks 534 | ) 535 | SwingList anonymousBookmarksSwingList = getAnonymousBookmarksSwingList( 536 | swing, node, map, anonymousBookmarks, showClones 537 | ) 538 | JPanel anonymousBookmarksJPanel = getAnonymousBookmarksJPanel( 539 | swing, anonymousBookmarks, anonymousBookmarksSwingList, (boolean)namedBookmarks, showClones 540 | ) 541 | Object gui = createGui( 542 | swing, 543 | namedBookmarksJPanel, namedBookmarksSwingList, 544 | anonymousBookmarksJPanel, anonymousBookmarksSwingList 545 | ) 546 | return gui 547 | } 548 | } 549 | -------------------------------------------------------------------------------- /src/main/groovy/CreateLinkGUI.groovy: -------------------------------------------------------------------------------- 1 | package lilive.bookmarks 2 | 3 | import lilive.bookmarks.Bookmarks as BM 4 | import groovy.swing.SwingBuilder 5 | import java.awt.BorderLayout 6 | import java.awt.GridBagConstraints 7 | import java.awt.event.* 8 | import java.util.Map as JMap 9 | import javax.swing.AbstractAction 10 | import javax.swing.InputMap 11 | import javax.swing.JComponent 12 | import javax.swing.JFrame 13 | import javax.swing.JList as SwingList 14 | import javax.swing.JPanel 15 | import javax.swing.KeyStroke 16 | import org.freeplane.api.MindMap as MindMap 17 | import org.freeplane.api.Node as Node 18 | import org.freeplane.core.ui.components.UITools as ui 19 | import org.freeplane.features.link.LinkController 20 | import org.freeplane.features.link.mindmapmode.MLinkController 21 | import org.freeplane.plugin.script.proxy.Proxy.Controller as ProxyController 22 | import org.freeplane.plugin.script.proxy.Proxy.Node as ProxyNode 23 | import org.freeplane.plugin.script.proxy.ScriptUtils 24 | import org.freeplane.core.util.MenuUtils 25 | 26 | /** 27 | * Display a window that allow to create a link in or from a bookmarked node 28 | */ 29 | public class CreateLinkGUI 30 | { 31 | static private Object gui 32 | 33 | static public enum Mode { 34 | IN, // The target of the link is the selected node, the link is created in a bookmarked node 35 | TO // The target of the link is a bookmarked node, the link is created in the selected node 36 | } 37 | 38 | // Build and display the window 39 | static public void show( ProxyNode node, Mode mode ) 40 | { 41 | MindMap map = node.map 42 | SwingBuilder swing = new SwingBuilder() 43 | 44 | // Find all the bookmarks 45 | List namedBookmarks = BM.getAllNameBookmarkedNodes( map ) 46 | List anonymousBookmarks = BM.getAllAnonymousBookmarkedNodes( map ) 47 | 48 | // Quit the script if there is no bookmarks 49 | if( ! namedBookmarks && ! anonymousBookmarks ) 50 | { 51 | ui.informationMessage( ui.currentFrame, "${BM.gtt( 'T_no_bookmarks' )} !", BM.gtt( 'T_BM_win_title' ) ) 52 | return 53 | } 54 | 55 | // Create the gui 56 | gui = createWindow( 57 | swing, node, map, 58 | namedBookmarks, anonymousBookmarks, 59 | mode 60 | ) 61 | 62 | // Center the gui over the freeplane window 63 | gui.pack() 64 | gui.setLocationRelativeTo( ui.currentFrame ) 65 | gui.visible = true 66 | } 67 | 68 | // Create a link to another node in a node 69 | static private void createLink( 70 | ProxyNode n1, 71 | ProxyNode n2, 72 | String message, 73 | Mode mode 74 | ) 75 | { 76 | if( mode == Mode.IN ) n2.link.node = n1 77 | else n1.link.node = n2 78 | Utils.setStatusInfo( message, 'button_ok' ) 79 | } 80 | 81 | // Build a GUI list component that display the named bookmarks and 82 | // that allow to use them. 83 | static private SwingList getNamedBookmarksSwingList( 84 | SwingBuilder swing, 85 | ProxyNode node, MindMap map, 86 | List bookmarks, // List of the bookmarks 87 | Mode mode 88 | ){ 89 | if( ! bookmarks ) return null 90 | 91 | // Create the component 92 | SwingList component 93 | swing.build{ 94 | component = list( 95 | items: bookmarks.collect 96 | { 97 | // Display for each bookmark the name of the bookmark, 98 | // followed by the text of the node 99 | if( it.id != node.id ) "$it.name : $it.text" 100 | else "$it.name : $it.text" 101 | }, 102 | visibleRowCount: Integer.min( 20, bookmarks.size() ), 103 | border: emptyBorder( 5 ) 104 | ) 105 | } 106 | 107 | // Initialize the component state 108 | int selectedIdx = 0 109 | component.setSelectedIndex( selectedIdx ) 110 | component.ensureIndexIsVisible( selectedIdx ) 111 | 112 | // Set the user interactions 113 | 114 | ProxyController c = ScriptUtils.c() 115 | component.addKeyListener( 116 | new KeyAdapter() 117 | { 118 | public void keyReleased( KeyEvent e ) 119 | { 120 | int code = e.getKeyCode() 121 | char chr = e.getKeyChar() 122 | int idx = (int) chr 123 | if( code == KeyEvent.VK_ENTER ) 124 | { 125 | int i = component.getSelectedIndex() 126 | if( i >= 0 ) 127 | { 128 | JMap bm = bookmarks[ i ] 129 | Node target = map.node( bm.id ) 130 | if( mode == Mode.IN ) 131 | createLink( node, target, "${BM.gtt( 'T_created_link_in_NBM' )} \"$bm.name\"", mode ) 132 | else 133 | createLink( node, target, "${BM.gtt( 'T_created_link_to_NBM' )} \"$bm.name\"", mode ) 134 | gui.dispose() 135 | } 136 | } 137 | else if( idx > 32 && idx != 127 && idx < 256 ) 138 | { 139 | // Try to create the link with the corresponding node id 140 | String s = String.valueOf( chr ) 141 | JMap bm = bookmarks.find{ it.name == s } 142 | if( bm ) 143 | { 144 | Node target = map.node( bm.id ) 145 | if( mode == Mode.IN ) 146 | createLink( node, target, "${BM.gtt( 'T_created_link_in_NBM' )} \"${s}\"", mode ) 147 | else 148 | createLink( node, target, "${BM.gtt( 'T_created_link_to_NBM' )} \"${s}\"", mode ) 149 | gui.dispose() 150 | } 151 | else 152 | { 153 | Utils.setStatusInfo( 154 | "${BM.gtt( 'T_no_node_with_key' )} \"${chr}\"", 155 | "messagebox_warning" 156 | ) 157 | } 158 | } 159 | } 160 | } 161 | ) 162 | 163 | component.addMouseListener( 164 | new MouseAdapter() 165 | { 166 | @Override public void mouseClicked(MouseEvent e) 167 | { 168 | int idx = component.getSelectedIndex() 169 | if( idx >= 0 ) 170 | { 171 | JMap bm = bookmarks[ idx ] 172 | Node target = map.node( bm.id ) 173 | String t = mode == Mode.IN ? 'T_created_link_in_NBM' : 'T_created_link_to_NBM' 174 | createLink( node, target, "${BM.gtt(t)} \"$bm.name\"", mode ) 175 | gui.dispose() 176 | } 177 | } 178 | } 179 | ) 180 | 181 | return component 182 | } 183 | 184 | // Create a JPanel for the named bookmarks 185 | static private JPanel getNamedBookmarksJPanel( 186 | SwingBuilder swing, 187 | List bookmarks, // List of the bookmarks 188 | SwingList bookmarksSwingList, // Component that display this list 189 | boolean showTabTip, // Do we need to add a comment about the Tab key ? 190 | Mode mode 191 | ){ 192 | if( ! bookmarks ) return null 193 | String t1 = mode == Mode.IN ? 'T_create_link_in' : 'T_create_link_to' 194 | String t2 = mode == Mode.IN ? 'T_select_node_to_link_in' : 'T_select_BM_to_link_to' 195 | String t3 = mode == Mode.IN ? 'T_tip_create_link_in_NBM' : 'T_tip_create_link_to_NBM' 196 | 197 | // List all the named bookmarks 198 | JPanel jPane 199 | swing.build{ 200 | jPane = swing.panel( 201 | border: emptyBorder( [0,0,10,0] ) 202 | ) 203 | { 204 | gridBagLayout() 205 | // This gridbag will contains 4 items 206 | // Row 0 : A label 207 | // Row 1 : A label and a question mark label with a tooltip 208 | // Row 2 : A label 209 | // Row 3 : The list of the named bookmarks 210 | 211 | // Row 0 212 | label( 213 | "${BM.gtt(t1)}.", 214 | constraints: gbc( gridx:0, gridy:0, anchor:GridBagConstraints.LINE_START ) 215 | ) 216 | 217 | // Row 1 218 | label( 219 | "${BM.gtt(t2)}.", 220 | constraints: gbc( gridx:0, gridy:1, anchor:GridBagConstraints.LINE_START ) 221 | ) 222 | label( 223 | icon: BM.getQuestionMarkIcon(), 224 | toolTipText: 225 | """ 226 | ${BM.gtt(t3)}: 227 |
    228 |
  • ${BM.gtt( 'T_press_red_key' )}
  • 229 |
  • ${BM.gtt( 'T_click_BM' )}
  • 230 |
  • ${BM.gtt( 'T_arrow_select' )}
  • 231 |
232 | """, 233 | constraints: gbc( gridx:1, gridy:1, anchor:GridBagConstraints.LINE_START, weightx:1, insets:[0,10,0,0] ) 234 | ) 235 | 236 | // Row 2 237 | if( showTabTip ) label( 238 | "${BM.gtt( 'T_tab_to_display_SBM' )}.", 239 | constraints: gbc( gridx:0, gridy:2, gridwidth:2, anchor:GridBagConstraints.LINE_START ) 240 | ) 241 | 242 | // Row 3 243 | scrollPane( 244 | border: emptyBorder( [10,0,0,0] ), 245 | constraints: gbc( gridx:0, gridy:3, gridwidth:2, fill:GridBagConstraints.HORIZONTAL, anchor:GridBagConstraints.LINE_START ) 246 | ) 247 | { 248 | widget( bookmarksSwingList ) 249 | } 250 | } 251 | } 252 | 253 | return jPane 254 | } 255 | 256 | // Build a GUI list component that display the anonymous bookmarks and 257 | // that allow to use them. 258 | static private SwingList getAnonymousBookmarksSwingList( 259 | SwingBuilder swing, 260 | ProxyNode node, MindMap map, 261 | List bookmarks, // List of the bookmarks 262 | Mode mode 263 | ){ 264 | if( ! bookmarks ) return null 265 | 266 | // Create the component 267 | SwingList component 268 | swing.build{ 269 | component = list( 270 | items: bookmarks.collect{ it.text }, 271 | visibleRowCount: Integer.min( 20, bookmarks.size() ) 272 | ) 273 | } 274 | 275 | // Initialize the component state 276 | int selectedIdx = 0 277 | component.setSelectedIndex( selectedIdx ) 278 | component.ensureIndexIsVisible( selectedIdx ) 279 | 280 | // Set the user interactions 281 | component.addKeyListener( 282 | new KeyAdapter() 283 | { 284 | @Override public void keyReleased(KeyEvent e) 285 | { 286 | int code = e.getKeyCode() 287 | if( code == KeyEvent.VK_ENTER ) 288 | { 289 | int idx = component.getSelectedIndex() 290 | if( idx >= 0 ) 291 | { 292 | JMap bm = bookmarks[ idx ] 293 | Node target = map.node( bm.id ) 294 | String t = mode == Mode.IN ? 'T_created_link_in_SBM' : 'T_created_link_to_SBM' 295 | createLink( node, target, BM.gtt(t), mode ) 296 | gui.dispose() 297 | } 298 | } 299 | } 300 | } 301 | ) 302 | 303 | component.addMouseListener( 304 | new MouseAdapter() 305 | { 306 | @Override public void mouseClicked(MouseEvent e) 307 | { 308 | int idx = component.getSelectedIndex() 309 | if( idx >= 0 ) 310 | { 311 | JMap bm = bookmarks[ idx ] 312 | Node target = map.node( bm.id ) 313 | String t = mode == Mode.IN ? 'T_created_link_in_SBM' : 'T_created_link_to_SBM' 314 | createLink( node, target, BM.gtt(t), mode ) 315 | gui.dispose() 316 | } 317 | } 318 | } 319 | ) 320 | 321 | return component 322 | } 323 | 324 | // Create a JPanel for the anonymous bookmarks 325 | static private JPanel getAnonymousBookmarksJPanel( 326 | SwingBuilder swing, 327 | List bookmarks, // List of the bookmarks 328 | SwingList bookmarksSwingList, // Component that display this list 329 | boolean showTabTip, // Do we need to add a comment about the Tab key ? 330 | Mode mode 331 | ){ 332 | if( ! bookmarks ) return null 333 | String t1 = mode == Mode.IN ? 'T_create_link_in' : 'T_create_link_to' 334 | String t2 = mode == Mode.IN ? 'T_select_BM_to_link_in' : 'T_select_BM_to_link_to' 335 | String t3 = mode == Mode.IN ? 'T_tip_create_link_in_SBM' : 'T_tip_create_link_to_SBM' 336 | 337 | // List all the named bookmarks 338 | JPanel jPane 339 | swing.build{ 340 | jPane = swing.panel( 341 | border: emptyBorder( [0,0,10,0] ) 342 | ) 343 | { 344 | gridBagLayout() 345 | // This gridbag will contains 4 items 346 | // Row 0 : A label 347 | // Row 1 : A label and a question mark label with a tooltip 348 | // Row 2 : A label 349 | // Row 3 : The list of the named bookmarks 350 | 351 | // Row 0 352 | label( 353 | "${BM.gtt(t1)}.", 354 | constraints: gbc( gridx:0, gridy:0, anchor:GridBagConstraints.LINE_START ) 355 | ) 356 | 357 | // Row 1 358 | label( 359 | "${BM.gtt(t2)}.", 360 | constraints: gbc( gridx:0, gridy:1, anchor:GridBagConstraints.LINE_START ) 361 | ) 362 | label( 363 | icon: BM.getQuestionMarkIcon(), 364 | toolTipText: 365 | """ 366 | ${BM.gtt(t3)}: 367 |
    368 |
  • ${BM.gtt( 'T_click_BM' )}
  • 369 |
  • ${BM.gtt( 'T_arrow_select' )}
  • 370 |
371 | """, 372 | constraints: gbc( gridx:1, gridy:1, anchor:GridBagConstraints.LINE_START, weightx:1, insets:[0,10,0,0] ) 373 | ) 374 | 375 | // Row 2 376 | if( showTabTip ) label( 377 | "${BM.gtt( 'T_tab_to_display_NBM' )}.", 378 | constraints: gbc( gridx:0, gridy:2, gridwidth:2, anchor:GridBagConstraints.LINE_START ) 379 | ) 380 | 381 | // Row 3 382 | scrollPane( 383 | border: emptyBorder( [10,0,0,0] ), 384 | constraints: gbc( gridx:0, gridy:3, gridwidth:2, fill:GridBagConstraints.HORIZONTAL, anchor:GridBagConstraints.LINE_START ) 385 | ) 386 | { 387 | widget( bookmarksSwingList ) 388 | } 389 | } 390 | } 391 | 392 | return jPane 393 | } 394 | 395 | // Create the content of the window 396 | static private Object createGui( 397 | SwingBuilder swing, 398 | JPanel namedPanel, SwingList namedSwingList, 399 | JPanel anonymPanel, SwingList anonymSwingList, 400 | Mode mode 401 | ) 402 | { 403 | String memorizedNodeID = ((MLinkController)(LinkController.getController())).getAnchorID() 404 | String t1 = mode == Mode.IN ? 'T_create_link_in_win_title' : 'T_create_link_to_win_title' 405 | String t2 = mode == Mode.IN ? 'T_space_to_link_in_memorized' : 'T_space_to_link_to_memorized' 406 | 407 | Object gui 408 | swing.build{ 409 | gui = dialog( 410 | title: BM.gtt(t1), 411 | modal:true, 412 | owner: ui.currentFrame, 413 | defaultCloseOperation: JFrame.DISPOSE_ON_CLOSE 414 | ){ 415 | borderLayout() 416 | // A panel for all the compontents (needed to add an inner padding) 417 | panel( 418 | border: emptyBorder( 10 ), 419 | constraints: BorderLayout.PAGE_START 420 | ){ 421 | borderLayout() 422 | 423 | // First element of the main panel : 424 | // A panel which can contains 2 panels: 425 | // - one for the named bookmarks 426 | // - one for the anonymous bookmarks 427 | panel( constraints: BorderLayout.LINE_START ) 428 | { 429 | borderLayout() 430 | if( namedPanel ) widget( namedPanel, constraints: BorderLayout.PAGE_START ) 431 | if( anonymPanel ) widget( anonymPanel, constraints: BorderLayout.PAGE_END ) 432 | } 433 | 434 | // Second element of the main panel : 435 | vbox( constraints: BorderLayout.PAGE_END) 436 | { 437 | if( memorizedNodeID ) label( text: "${BM.gtt(t2)}." ) 438 | label( text: "${BM.gtt( 'T_press_esc_cancel' )}." ) 439 | } 440 | } 441 | } 442 | } 443 | 444 | // Set Esc key to close the script 445 | String onEscPressID = "onEscPress" 446 | InputMap inputMap = gui 447 | .getRootPane() 448 | .getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ) 449 | inputMap.put( KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 ), onEscPressID ) 450 | gui.getRootPane().getActionMap().put( 451 | onEscPressID, 452 | new AbstractAction() 453 | { 454 | @Override 455 | public void actionPerformed( ActionEvent e ) 456 | { 457 | gui.dispose() 458 | Utils.setStatusInfo( 459 | BM.gtt( 'T_link_aborded' ), 460 | 'button_cancel' 461 | ) 462 | } 463 | } 464 | ) 465 | 466 | if( memorizedNodeID ) 467 | { 468 | // Set Space key to create the link to the standard freeplane memorized node 469 | String onSpacePressID = "onSpacePress" 470 | KeyStroke spaceStroke = KeyStroke.getKeyStroke( KeyEvent.VK_SPACE, 0 ) 471 | inputMap.put( spaceStroke, onSpacePressID ) 472 | gui.getRootPane().getActionMap().put( 473 | onSpacePressID, 474 | new AbstractAction() 475 | { 476 | @Override 477 | public void actionPerformed( ActionEvent e ) 478 | { 479 | String item = mode == Mode.IN ? 'MakeLinkFromAnchorAction' : 'MakeLinkToAnchorAction' 480 | MenuUtils.executeMenuItems( [ item ] ) 481 | gui.dispose() 482 | } 483 | } 484 | ) 485 | if( namedSwingList ) namedSwingList.getInputMap().put( spaceStroke, "none" ) 486 | if( anonymSwingList ) anonymSwingList.getInputMap().put( spaceStroke, "none" ) 487 | } 488 | 489 | if( namedSwingList ) 490 | { 491 | if( anonymPanel ) anonymPanel.visible = false 492 | namedSwingList.requestFocus() 493 | } 494 | else if( anonymSwingList ) 495 | { 496 | if( namedPanel ) namedPanel.visible = false 497 | anonymSwingList.requestFocus() 498 | } 499 | 500 | // Set the Tab key to switch between the 2 bookmarks types 501 | if( namedPanel && namedSwingList && anonymPanel && anonymSwingList ) 502 | { 503 | namedSwingList.setFocusTraversalKeysEnabled( false ) 504 | anonymSwingList.setFocusTraversalKeysEnabled( false ) 505 | String onTabPressID = "onTabPress" 506 | inputMap.put( KeyStroke.getKeyStroke( KeyEvent.VK_TAB, 0 ), onTabPressID ) 507 | gui.getRootPane().getActionMap().put( 508 | onTabPressID, 509 | new AbstractAction() 510 | { 511 | public void actionPerformed( ActionEvent e ) 512 | { 513 | if( namedPanel.visible ) 514 | { 515 | namedPanel.visible = false 516 | anonymPanel.visible = true 517 | anonymSwingList.requestFocus() 518 | } 519 | else 520 | { 521 | namedPanel.visible = true 522 | anonymPanel.visible = false 523 | namedSwingList.requestFocus() 524 | } 525 | gui.pack() 526 | gui.setLocationRelativeTo( ui.currentFrame ) 527 | } 528 | } 529 | ) 530 | } 531 | return gui 532 | } 533 | 534 | // Create the window 535 | static private Object createWindow( 536 | SwingBuilder swing, 537 | ProxyNode node, MindMap map, 538 | List namedBookmarks, List anonymousBookmarks, 539 | Mode mode 540 | ) 541 | { 542 | SwingList namedBookmarksSwingList = getNamedBookmarksSwingList( 543 | swing, node, map, namedBookmarks, mode 544 | ) 545 | JPanel namedBookmarksJPanel = getNamedBookmarksJPanel( 546 | swing, namedBookmarks, namedBookmarksSwingList, (boolean)anonymousBookmarks, mode 547 | ) 548 | SwingList anonymousBookmarksSwingList = getAnonymousBookmarksSwingList( 549 | swing, node, map, anonymousBookmarks, mode 550 | ) 551 | JPanel anonymousBookmarksJPanel = getAnonymousBookmarksJPanel( 552 | swing, anonymousBookmarks, anonymousBookmarksSwingList, (boolean)namedBookmarks, mode 553 | ) 554 | Object gui = createGui( 555 | swing, 556 | namedBookmarksJPanel, namedBookmarksSwingList, 557 | anonymousBookmarksJPanel, anonymousBookmarksSwingList, 558 | mode 559 | ) 560 | return gui 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /zips/doc/bookmarks/knacss.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /*! 3 | * www.KNACSS.com v7.1.1 (december, 10 2018) @author: Alsacreations, Raphael Goetter 4 | * Licence WTFPL http://www.wtfpl.net/ 5 | */*,:after,:before{-webkit-box-sizing:border-box;box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%;overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}@-o-viewport{width:device-width}@viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;font-size:1rem;font-weight:400;text-align:left}[tabindex="-1"]:focus{outline:none!important}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{font-style:normal;line-height:inherit}address,dl,ol,ul{margin-bottom:1rem}dl,ol,ul{margin-top:0}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:700}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]),a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:inherit}label{margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button;-moz-appearance:button;appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox;-moz-appearance:listbox;appearance:listbox}textarea{overflow:auto}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none;-moz-appearance:none;appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none;appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button;appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}@media (prefers-reduced-motion:reduce){*{-webkit-animation:none!important;animation:none!important;-webkit-transition:none!important;transition:none!important}}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit;min-width:0;min-height:0}html{font-size:62.5%;font-size:.625em}body{margin:0;font-size:1.4rem;background-color:#fff;color:#212529;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;line-height:1.5}@media (min-width:576px){body{font-size:1.6rem}}a{color:#333;text-decoration:underline}a:active,a:focus,a:hover{color:#0d0d0d;text-decoration:underline}.h1-like,h1{font-size:2.8rem;font-family:sans-serif;font-weight:500}@media (min-width:576px){.h1-like,h1{font-size:3.2rem}}.h2-like,h2{font-size:2.4rem;font-family:sans-serif;font-weight:500}@media (min-width:576px){.h2-like,h2{font-size:2.8rem}}.h3-like,h3{font-size:2rem;font-weight:500}@media (min-width:576px){.h3-like,h3{font-size:2.4rem}}.h4-like,h4{font-size:1.8rem;font-weight:500}@media (min-width:576px){.h4-like,h4{font-size:2rem}}.h5-like,h5{font-size:1.6rem;font-weight:500}@media (min-width:576px){.h5-like,h5{font-size:1.8rem}}.h6-like,h6{font-size:1.4rem;font-weight:500}@media (min-width:576px){.h6-like,h6{font-size:1.6rem}}dd,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}address,blockquote,dl,ol,p,pre,ul{margin-top:0;margin-bottom:1rem}li .p-like,li ol,li p,li ul,ol ol,ul ul{margin-top:0;margin-bottom:0}blockquote,code,img,input,pre,svg,table,td,textarea,video{max-width:100%}img{height:auto}ol,ul{padding-left:2em}img{vertical-align:middle}.italic,address,cite,em,i,var{font-style:italic}code,kbd,mark{border-radius:2px}kbd{padding:0 2px;border:1px solid #999}pre{-moz-tab-size:2;-o-tab-size:2;tab-size:2}code{padding:2px 4px;background:rgba(0,0,0,.04);color:#b11}pre code{padding:0;background:none;color:inherit;border-radius:0}mark{padding:2px 4px}sub,sup{vertical-align:0}sup{bottom:1ex}sub{top:.5ex}blockquote{position:relative;padding-left:3em;min-height:2em}blockquote:before{content:"\201C";position:absolute;left:0;top:0;font-family:georgia,serif;font-size:5em;height:.4em;line-height:.9;color:#e7e9ed}blockquote>footer{margin-top:.75em;font-size:.9em;color:rgba(0,0,0,.7)}blockquote>footer:before{content:"\2014 \0020"}q{font-style:normal}.q,q{quotes:"“" "”" "‘" "’"}.q:lang(fr),q:lang(fr){quotes:"«\00a0" "\00a0»" "“" "”"}hr{display:block;clear:both;height:1px;margin:1em 0 2em;padding:0;border:0;color:#ccc;background-color:#ccc}blockquote,figure{margin-left:0;margin-right:0}code,kbd,pre,samp{white-space:pre-wrap;font-family:consolas,courier,monospace;line-height:normal}@media print{*{background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}body{width:auto;margin:auto;font-family:serif;font-size:12pt}.h1-like,.h2-like,.h3-like,.h4-like,.h5-like,.h6-like,.p-like,blockquote,h1,h2,h3,h4,h5,h6,label,ol,p,ul{color:#000;margin:auto}.print{display:block}.no-print{display:none}.p-like,blockquote,p{orphans:3;widows:3}blockquote,ol,ul{page-break-inside:avoid}.h1-like,.h2-like,.h3-like,caption,h1,h2,h3{page-break-after:avoid}a{color:#000}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}}.d-flex,.flex-column,.flex-column-reverse,.flex-container,.flex-container--column,.flex-container--column-reverse,.flex-container--row,.flex-container--row-reverse,.flex-row,.flex-row-reverse{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-container--row,.flex-row{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.flex-column,.flex-container--column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.flex-container--row-reverse,.flex-row-reverse{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.flex-column-reverse,.flex-container--column-reverse{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.flex-item-fluid,.item-fluid{-webkit-box-flex:1;-ms-flex:1 1 0%;flex:1 1}.flex-item-first,.item-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.flex-item-medium,.item-medium{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.flex-item-last,.item-last{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.flex-item-center,.item-center,.mr-auto{margin:auto}.u-bold{font-weight:700}.u-italic{font-style:italic}.u-normal{font-weight:400;font-style:normal}.u-uppercase{text-transform:uppercase}.u-lowercase{text-transform:lowercase}.u-smaller{font-size:.6em}.u-small{font-size:.8em}.u-big{font-size:1.2em}.u-bigger{font-size:1.5em}.u-biggest{font-size:2em}.u-txt-wrap{word-wrap:break-word;overflow-wrap:break-word;-webkit-hyphens:auto;-ms-hyphens:auto;hyphens:auto}.u-txt-ellipsis{white-space:nowrap;text-overflow:ellipsis}.txtleft,.u-txt-left{text-align:left}.txtright,.u-txt-right{text-align:right}.txtcenter,.u-txt-center{text-align:center}.clear,.u-clear{clear:both}.clearfix:after,.u-clearfix:after{content:"";display:table;clear:both;border-collapse:collapse}.left .u-left{margin-right:auto}.right,.u-right{margin-left:auto}.center,.u-center{margin-left:auto;margin-right:auto}.bfc,.mod,.u-bfc,.u-mod{overflow:hidden}.fl,.u-fl{float:left}img.fl,img.u-fl{margin-right:1rem}.fr,.u-fr{float:right}img.fr,img.u-fr{margin-left:1rem}img.fl,img.fr,img.u-fl,img.u-fr{margin-bottom:.5rem}.inbl,.u-inbl{display:inline-block;vertical-align:top}.is-hidden,.js-hidden,[hidden]{display:none}.tabs-content-item[aria-hidden=true],.visually-hidden{position:absolute!important;border:0!important;height:1px!important;width:1px!important;padding:0!important;overflow:hidden!important;clip:rect(0,0,0,0)!important}.is-disabled,.is-disabled~label,.js-disabled,[disabled],[disabled]~label{opacity:.5;cursor:not-allowed!important;-webkit-filter:grayscale(1);filter:grayscale(1)}ul.is-unstyled,ul.unstyled{list-style:none;padding-left:0}.color--inverse{color:#fff}.w100{width:100%}.w95{width:95%}.w90{width:90%}.w85{width:85%}.w80{width:80%}.w75{width:75%}.w70{width:70%}.w65{width:65%}.w60{width:60%}.w55{width:55%}.w50{width:50%}.w45{width:45%}.w40{width:40%}.w35{width:35%}.w30{width:30%}.w25{width:25%}.w20{width:20%}.w15{width:15%}.w10{width:10%}.w5{width:5%}.w66{width:66.66667%}.w33{width:33.33333%}.wauto{width:auto}.w960p{width:960px}.mw960p{max-width:960px}.w1140p{width:1140px}.mw1140p{max-width:1140px}.w1000p{width:1000px}.w950p{width:950px}.w900p{width:900px}.w850p{width:850px}.w800p{width:800px}.w750p{width:750px}.w700p{width:700px}.w650p{width:650px}.w600p{width:600px}.w550p{width:550px}.w500p{width:500px}.w450p{width:450px}.w400p{width:400px}.w350p{width:350px}.w300p{width:300px}.w250p{width:250px}.w200p{width:200px}.w150p{width:150px}.w100p{width:100px}.w50p{width:50px}.ma0,.man{margin:0}.pa0,.pan{padding:0}.mas{margin:1rem}.mam{margin:2rem}.mal{margin:4rem}.pas{padding:1rem}.pam{padding:2rem}.pal{padding:4rem}.mt0,.mtn{margin-top:0}.mts{margin-top:1rem}.mtm{margin-top:2rem}.mtl{margin-top:4rem}.mr0,.mrn{margin-right:0}.mrs{margin-right:1rem}.mrm{margin-right:2rem}.mrl{margin-right:4rem}.mb0,.mbn{margin-bottom:0}.mbs{margin-bottom:1rem}.mbm{margin-bottom:2rem}.mbl{margin-bottom:4rem}.ml0,.mln{margin-left:0}.mls{margin-left:1rem}.mlm{margin-left:2rem}.mll{margin-left:4rem}.mauto{margin:auto}.mtauto{margin-top:auto}.mrauto{margin-right:auto}.mbauto{margin-bottom:auto}.mlauto{margin-left:auto}.pt0,.ptn{padding-top:0}.pts{padding-top:1rem}.ptm{padding-top:2rem}.ptl{padding-top:4rem}.pr0,.prn{padding-right:0}.prs{padding-right:1rem}.prm{padding-right:2rem}.prl{padding-right:4rem}.pb0,.pbn{padding-bottom:0}.pbs{padding-bottom:1rem}.pbm{padding-bottom:2rem}.pbl{padding-bottom:4rem}.pl0,.pln{padding-left:0}.pls{padding-left:1rem}.plm{padding-left:2rem}.pll{padding-left:4rem}@media (min-width:992px){.large-hidden{display:none!important}.large-visible{display:block!important}.large-no-float{float:none}.large-inbl{display:inline-block;float:none;vertical-align:top}.large-w25{width:25%!important}.large-w33{width:33.333333%!important}.large-w50{width:50%!important}.large-w66{width:66.666666%!important}.large-w75{width:75%!important}.large-w100,.large-wauto{display:block!important;float:none!important;clear:none!important;width:auto!important;margin-left:0!important;margin-right:0!important;border:0}.large-ma0,.large-man{margin:0!important}}@media (min-width:768px) and (max-width:991px){.medium-hidden{display:none!important}.medium-visible{display:block!important}.medium-no-float{float:none}.medium-inbl{display:inline-block;float:none;vertical-align:top}.medium-w25{width:25%!important}.medium-w33{width:33.333333%!important}.medium-w50{width:50%!important}.medium-w66{width:66.666666%!important}.medium-w75{width:75%!important}.medium-w100,.medium-wauto{display:block!important;float:none!important;clear:none!important;width:auto!important;margin-left:0!important;margin-right:0!important;border:0}.medium-ma0,.medium-man{margin:0!important}}@media (min-width:576px) and (max-width:767px){.small-hidden{display:none!important}.small-visible{display:block!important}.small-no-float{float:none}.small-inbl{display:inline-block;float:none;vertical-align:top}.small-w25{width:25%!important}.small-w33{width:33.333333%!important}.small-w50{width:50%!important}.small-w66{width:66.666666%!important}.small-w75{width:75%!important}.small-w100,.small-wauto{display:block!important;float:none!important;clear:none!important;width:auto!important;margin-left:0!important;margin-right:0!important;border:0}.small-ma0,.small-man{margin:0!important}.small-pa0,.small-pan{padding:0!important}}@media (max-width:575px){.col,.mod,fieldset{display:block!important;float:none!important;clear:none!important;width:auto!important;margin-left:0!important;margin-right:0!important;border:0}.flex-column,.flex-column-reverse,.flex-container,.flex-container--column,.flex-container--column-reverse,.flex-container--row,.flex-container--row-reverse,.flex-row,.flex-row-reverse{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.tiny-hidden{display:none!important}.tiny-visible{display:block!important}.tiny-no-float{float:none}.tiny-inbl{display:inline-block;float:none;vertical-align:top}.tiny-w25{width:25%!important}.tiny-w33{width:33.333333%!important}.tiny-w50{width:50%!important}.tiny-w66{width:66.666666%!important}.tiny-w75{width:75%!important}.tiny-w100,.tiny-wauto{display:block!important;float:none!important;clear:none!important;width:auto!important;margin-left:0!important;margin-right:0!important;border:0}.tiny-ma0,.tiny-man{margin:0!important}.tiny-pa0,.tiny-pan{padding:0!important}}@media (min-width:480px){[class*=" grid-"],[class^=grid-]{display:-ms-grid;display:grid;grid-auto-flow:dense}[class*=" grid-"].has-gutter,[class^=grid-].has-gutter{grid-gap:1rem}[class*=" grid-"].has-gutter-l,[class^=grid-].has-gutter-l{grid-gap:2rem}[class*=" grid-"].has-gutter-xl,[class^=grid-].has-gutter-xl{grid-gap:4rem}}@media (min-width:480px){.autogrid,.grid{display:-ms-grid;display:grid;grid-auto-flow:column;grid-auto-columns:1fr}.autogrid.has-gutter,.grid.has-gutter{grid-column-gap:1rem}.autogrid.has-gutter-l,.grid.has-gutter-l{grid-column-gap:2rem}.autogrid.has-gutter-xl,.grid.has-gutter-xl{grid-column-gap:4rem}}[class*=grid-2]{-ms-grid-columns:(1fr)[2];grid-template-columns:repeat(2,1fr)}[class*=grid-3]{-ms-grid-columns:(1fr)[3];grid-template-columns:repeat(3,1fr)}[class*=grid-4]{-ms-grid-columns:(1fr)[4];grid-template-columns:repeat(4,1fr)}[class*=grid-5]{-ms-grid-columns:(1fr)[5];grid-template-columns:repeat(5,1fr)}[class*=grid-6]{-ms-grid-columns:(1fr)[6];grid-template-columns:repeat(6,1fr)}[class*=grid-7]{-ms-grid-columns:(1fr)[7];grid-template-columns:repeat(7,1fr)}[class*=grid-8]{-ms-grid-columns:(1fr)[8];grid-template-columns:repeat(8,1fr)}[class*=grid-9]{-ms-grid-columns:(1fr)[9];grid-template-columns:repeat(9,1fr)}[class*=grid-10]{-ms-grid-columns:(1fr)[10];grid-template-columns:repeat(10,1fr)}[class*=grid-11]{-ms-grid-columns:(1fr)[11];grid-template-columns:repeat(11,1fr)}[class*=grid-12]{-ms-grid-columns:(1fr)[12];grid-template-columns:repeat(12,1fr)}[class*=col-1]{grid-column:auto/span 1}[class*=row-1]{grid-row:auto/span 1}[class*=col-2]{grid-column:auto/span 2}[class*=row-2]{grid-row:auto/span 2}[class*=col-3]{grid-column:auto/span 3}[class*=row-3]{grid-row:auto/span 3}[class*=col-4]{grid-column:auto/span 4}[class*=row-4]{grid-row:auto/span 4}[class*=col-5]{grid-column:auto/span 5}[class*=row-5]{grid-row:auto/span 5}[class*=col-6]{grid-column:auto/span 6}[class*=row-6]{grid-row:auto/span 6}[class*=col-7]{grid-column:auto/span 7}[class*=row-7]{grid-row:auto/span 7}[class*=col-8]{grid-column:auto/span 8}[class*=row-8]{grid-row:auto/span 8}[class*=col-9]{grid-column:auto/span 9}[class*=row-9]{grid-row:auto/span 9}[class*=col-10]{grid-column:auto/span 10}[class*=row-10]{grid-row:auto/span 10}[class*=col-11]{grid-column:auto/span 11}[class*=row-11]{grid-row:auto/span 11}[class*=col-12]{grid-column:auto/span 12}[class*=row-12]{grid-row:auto/span 12}@media (min-width:480px) and (max-width:767px){[class*=grid-][class*=-small-1]{-ms-grid-columns:(1fr)[1];grid-template-columns:repeat(1,1fr)}[class*=col-][class*=-small-1]{grid-column:auto/span 1}[class*=grid-][class*=-small-2]{-ms-grid-columns:(1fr)[2];grid-template-columns:repeat(2,1fr)}[class*=col-][class*=-small-2]{grid-column:auto/span 2}[class*=grid-][class*=-small-3]{-ms-grid-columns:(1fr)[3];grid-template-columns:repeat(3,1fr)}[class*=col-][class*=-small-3]{grid-column:auto/span 3}[class*=grid-][class*=-small-4]{-ms-grid-columns:(1fr)[4];grid-template-columns:repeat(4,1fr)}[class*=col-][class*=-small-4]{grid-column:auto/span 4}[class*=-small-all]{grid-column:1/-1}}.item-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.item-last{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.grid-offset{visibility:hidden}.col-all{grid-column:1/-1}.row-all{grid-row:1/-1}@media (min-width:480px){.media{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.media-content{-webkit-box-flex:1;-ms-flex:1 1 0%;flex:1 1}.media-figure--center{-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.media--reverse{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}}.skip-links,.skip-links a{position:absolute}.skip-links a{overflow:hidden;clip:rect(1px,1px,1px,1px);padding:.5em;background:#000;color:#fff;text-decoration:none}.skip-links a:focus{position:static;overflow:visible;clip:auto}.table,table{width:100%;max-width:100%;table-layout:fixed;border-collapse:collapse;vertical-align:top;margin-bottom:2rem}.table{display:table;border:1px solid #acb3c2;background:transparent}.table--zebra tbody tr:nth-child(odd){background:#e7e9ed}.table caption{caption-side:bottom;padding:1rem;color:#333;font-style:italic;text-align:right}.table td,.table th{padding:.3rem .6rem;min-width:2rem;vertical-align:top;border:1px dotted #acb3c2;text-align:left;cursor:default}.table thead{color:#212529;background:transparent}.table--auto{table-layout:auto}fieldset,form{border:none}fieldset{padding:2rem}fieldset legend{padding:0 .5rem;border:0;white-space:normal}label{display:inline-block;cursor:pointer}[type=color],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=submit],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{font-family:inherit;font-size:inherit;border:0;-webkit-box-shadow:0 0 0 1px #333 inset;box-shadow:inset 0 0 0 1px #333;color:#212529;vertical-align:middle;padding:.5rem 1rem;margin:0;-webkit-transition:.25s;transition:.25s;-webkit-transition-property:background-color,color,border,-webkit-box-shadow;transition-property:background-color,color,border,-webkit-box-shadow;transition-property:box-shadow,background-color,color,border;transition-property:box-shadow,background-color,color,border,-webkit-box-shadow;-webkit-appearance:none;-moz-appearance:none;appearance:none}[type=submit]{background-color:#333;color:#fff;cursor:pointer}input[readonly]{background-color:#e7e9ed}select{padding-right:2rem;border-radius:0;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' style='isolation:isolate' width='20' height='20'%3E%3Cpath d='M9.96 11.966L3.523 5.589C2.464 4.627.495 6.842 1.505 7.771l6.989 6.992c.644.587 2.161.606 2.796 0l7.2-6.992c1.067-1.019-1.126-3.091-2.228-2.046L9.96 11.966z' fill='inherit'/%3E%3C/svg%3E");background-position:right .6rem center;background-repeat:no-repeat;background-size:1.2rem}select::-ms-expand{display:none}textarea{min-height:5em;vertical-align:top;resize:vertical;white-space:pre-wrap}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration,[type=search]::-webkit-search-results-button,[type=search]::-webkit-search-results-decoration{display:none}::-webkit-input-placeholder{color:#777}::-moz-placeholder{color:#777}:-ms-input-placeholder{color:#777}::-ms-input-placeholder{color:#777}::placeholder{color:#777}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#777}input::-moz-placeholder,textarea::-moz-placeholder{color:#777}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#777}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:#777}input::placeholder,textarea::placeholder{color:#777}progress{width:100%;vertical-align:middle}.btn,.btn--danger,.btn--ghost,.btn--info,.btn--inverse,.btn--primary,.btn--success,.btn--warning,.button,.button--danger,.button--ghost,.button--info,.button--inverse,.button--primary,.button--success,.button--warning,[type=button],button{display:inline-block;padding:1rem 1.5rem;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:.25s;transition:.25s;-webkit-transition-property:background-color,color,border,-webkit-box-shadow;transition-property:background-color,color,border,-webkit-box-shadow;transition-property:box-shadow,background-color,color,border;transition-property:box-shadow,background-color,color,border,-webkit-box-shadow;text-align:center;vertical-align:middle;white-space:nowrap;text-decoration:none;color:#212529;border:none;border-radius:0;background-color:#e7e9ed;font-family:inherit;font-size:inherit;line-height:1}.btn:focus,.button:focus,[type=button]:focus,button:focus{-webkit-tap-highlight-color:transparent}.btn--primary,.button--primary{background-color:#0275d8;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn--primary:active,.btn--primary:focus,.btn--primary:hover,.button--primary:active,.button--primary:focus,.button--primary:hover{background-color:#025aa5}.btn--success,.button--success{background-color:#5cb85c;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn--success:active,.btn--success:focus,.btn--success:hover,.button--success:active,.button--success:focus,.button--success:hover{background-color:#449d44}.btn--info,.button--info{background-color:#5bc0de;color:#000;-webkit-box-shadow:none;box-shadow:none}.btn--info:active,.btn--info:focus,.btn--info:hover,.button--info:active,.button--info:focus,.button--info:hover{background-color:#31b0d5}.btn--warning,.button--warning{background-color:#f0ad4e;color:#000;-webkit-box-shadow:none;box-shadow:none}.btn--warning:active,.btn--warning:focus,.btn--warning:hover,.button--warning:active,.button--warning:focus,.button--warning:hover{background-color:#ec971f}.btn--danger,.button--danger{background-color:#d9534f;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn--danger:active,.btn--danger:focus,.btn--danger:hover,.button--danger:active,.button--danger:focus,.button--danger:hover{background-color:#c9302c}.btn--inverse,.button--inverse{background-color:#333;color:#fff;-webkit-box-shadow:none;box-shadow:none}.btn--inverse:active,.btn--inverse:focus,.btn--inverse:hover,.button--inverse:active,.button--inverse:focus,.button--inverse:hover{background-color:#1a1a1a}.btn--ghost,.button--ghost{color:#fff;-webkit-box-shadow:0 0 0 1px #fff inset;box-shadow:inset 0 0 0 1px #fff}.btn--ghost,.btn--ghost:active,.btn--ghost:focus,.btn--ghost:hover,.button--ghost,.button--ghost:active,.button--ghost:focus,.button--ghost:hover{background-color:transparent}.btn--small,.button--small{padding:.7rem 1rem;font-size:.8em}.btn--big,.button--big{padding:1.5rem 2rem;font-size:1.4em}.btn--block,.button--block{width:100%!important;display:block}.btn--unstyled,.button--unstyled{padding:0;border:none;text-align:left;background:none;border-radius:0;-webkit-box-shadow:none;box-shadow:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.btn--unstyled:focus,.button--unstyled:focus{-webkit-box-shadow:none;box-shadow:none;outline:none}.nav-button{outline:0;border:0;cursor:pointer;-webkit-tap-highlight-color:transparent}.nav-button,.nav-button>*{padding:0;background-color:transparent}.nav-button>*{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:top;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;height:2.6rem;width:2.6rem;background-image:-webkit-gradient(linear,left top,left bottom,from(#333),to(#333));background-image:linear-gradient(#333,#333);background-position:50%;background-repeat:no-repeat;background-origin:content-box;background-size:100% 5px;-webkit-transition:.25s;transition:.25s;-webkit-transition-property:background,-webkit-transform;transition-property:background,-webkit-transform;transition-property:transform,background;transition-property:transform,background,-webkit-transform;will-change:transform,background}.nav-button>:after,.nav-button>:before{content:"";height:5px;background:#333;-webkit-transition:.25s;transition:.25s;-webkit-transition-property:top,-webkit-transform;transition-property:top,-webkit-transform;transition-property:transform,top;transition-property:transform,top,-webkit-transform;will-change:transform,top}.nav-button:hover>*{background-color:transparent}.nav-button:focus{outline:0}.nav-button.is-active>*{background-image:none;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.nav-button.is-active>:before{-webkit-transform:translateY(50%) rotate(45deg);transform:translateY(50%) rotate(45deg)}.nav-button.is-active>:after{-webkit-transform:translateY(-50%) rotate(-45deg);transform:translateY(-50%) rotate(-45deg)}.checkbox{border-radius:4px}.switch{border-radius:3em}.radio{border-radius:50%}.checkbox,.radio,.switch{-webkit-appearance:none;-moz-appearance:none;appearance:none;vertical-align:text-bottom;outline:0;cursor:pointer}.checkbox~label,.radio~label,.switch~label{cursor:pointer}.checkbox::-ms-check,.radio::-ms-check,.switch::-ms-check{display:none}.switch{width:4rem;height:2rem;line-height:2rem;font-size:70%;-webkit-box-shadow:inset -2rem 0 0 #333,inset 0 0 0 1px #333;box-shadow:inset -2rem 0 0 #333,inset 0 0 0 1px #333;-webkit-transition:-webkit-box-shadow .15s;transition:-webkit-box-shadow .15s;transition:box-shadow .15s;transition:box-shadow .15s,-webkit-box-shadow .15s;background-color:#fff}.switch:after,.switch:before{font-weight:700;color:#fff}.switch:before{content:"✕";float:right;margin-right:.66667rem}.switch:checked{-webkit-box-shadow:inset 2rem 0 0 #5cb85c,inset 0 0 0 1px #5cb85c;box-shadow:inset 2rem 0 0 #5cb85c,inset 0 0 0 1px #5cb85c}.switch:checked:before{content:"✓";float:left;margin-left:.66667rem}.checkbox{width:2rem;height:2rem;-webkit-box-shadow:inset 0 0 0 1px #333;box-shadow:inset 0 0 0 1px #333;background-color:#fff;-webkit-transition:background-color .15s;transition:background-color .15s}.checkbox:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23FFF' d='M6.4 1l-.7.7-2.8 2.8-.8-.8-.7-.7L0 4.4l.7.7 1.5 1.5.7.7.7-.7 3.5-3.5.7-.7L6.4 1z'/%3E%3C/svg%3E");background-size:60% 60%;background-position:50%;background-repeat:no-repeat;background-color:#333}.radio{width:2rem;height:2rem;background-size:0 0;-webkit-transition:background-size .15s;transition:background-size .15s;-webkit-box-shadow:inset 0 0 0 1px #333;box-shadow:inset 0 0 0 1px #333;background-color:#fff}.radio:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='100' height='100' viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='40' cy='40' r='24' fill='%23333'/%3E%3C/svg%3E");background-size:90% 90%;background-position:50%;background-repeat:no-repeat;background-color:#fff}.tabs-menu{border-bottom:2px solid #e7e9ed}.tabs-menu-link{display:block;margin-bottom:-2px;padding:.5rem 3rem;border-bottom:4px solid transparent;color:#212529;background:transparent;text-decoration:none;border-radius:0 0 0 0;-webkit-transition:.25s;transition:.25s;-webkit-transition-property:color,border,background-color;transition-property:color,border,background-color}.tabs-menu-link.is-active{background:transparent}.tabs-menu-link.is-active,.tabs-menu-link:focus{border-bottom-color:#333;color:#333;outline:0}@media (min-width:576px){.tabs-menu-link{display:inline-block}}.tabs-content-item{padding-top:1rem}.tabs-content-item[aria-hidden=true]{visibility:hidden}.tabs-content-item[aria-hidden=false]{visibility:visible}[class*=icon-arrow--]{vertical-align:middle}[class*=icon-arrow--]:after{content:"";display:inline-block;width:1em;height:1em;-webkit-mask-size:cover;mask-size:cover;background-color:#000;line-height:1}.icon-arrow--down:after{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' style='isolation:isolate' width='20' height='20'%3E%3Cpath d='M9.96 11.966L3.523 5.589C2.464 4.627.495 6.842 1.505 7.771l6.989 6.992c.644.587 2.161.606 2.796 0l7.2-6.992c1.067-1.019-1.126-3.091-2.228-2.046L9.96 11.966z' fill='inherit'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' style='isolation:isolate' width='20' height='20'%3E%3Cpath d='M9.96 11.966L3.523 5.589C2.464 4.627.495 6.842 1.505 7.771l6.989 6.992c.644.587 2.161.606 2.796 0l7.2-6.992c1.067-1.019-1.126-3.091-2.228-2.046L9.96 11.966z' fill='inherit'/%3E%3C/svg%3E")}.icon-arrow--up:after{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' style='isolation:isolate' width='20' height='20'%3E%3Cpath d='M9.96 8.596l-6.437 6.377c-1.059.962-3.028-1.253-2.018-2.182l6.989-6.992c.644-.587 2.161-.606 2.796 0l7.2 6.992c1.067 1.018-1.126 3.091-2.228 2.046L9.96 8.596z' fill='inherit'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' style='isolation:isolate' width='20' height='20'%3E%3Cpath d='M9.96 8.596l-6.437 6.377c-1.059.962-3.028-1.253-2.018-2.182l6.989-6.992c.644-.587 2.161-.606 2.796 0l7.2 6.992c1.067 1.018-1.126 3.091-2.228 2.046L9.96 8.596z' fill='inherit'/%3E%3C/svg%3E")}.icon-arrow--right:after{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' style='isolation:isolate' width='20' height='20'%3E%3Cpath d='M11.685 10.321l-6.377 6.437c-.962 1.059 1.253 3.028 2.182 2.018l6.992-6.989c.587-.645.606-2.161 0-2.796l-6.992-7.2C6.472.724 4.399 2.916 5.444 4.019l6.241 6.302z' fill='inherit'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' style='isolation:isolate' width='20' height='20'%3E%3Cpath d='M11.685 10.321l-6.377 6.437c-.962 1.059 1.253 3.028 2.182 2.018l6.992-6.989c.587-.645.606-2.161 0-2.796l-6.992-7.2C6.472.724 4.399 2.916 5.444 4.019l6.241 6.302z' fill='inherit'/%3E%3C/svg%3E")}.icon-arrow--left:after{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' style='isolation:isolate' width='20' height='20'%3E%3Cpath d='M8.315 10.321l6.377 6.437c.962 1.059-1.253 3.028-2.182 2.018l-6.992-6.989c-.587-.645-.606-2.161 0-2.796l6.992-7.2c1.018-1.067 3.091 1.125 2.046 2.228l-6.241 6.302z' fill='inherit'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' style='isolation:isolate' width='20' height='20'%3E%3Cpath d='M8.315 10.321l6.377 6.437c.962 1.059-1.253 3.028-2.182 2.018l-6.992-6.989c-.587-.645-.606-2.161 0-2.796l6.992-7.2c1.018-1.067 3.091 1.125 2.046 2.228l-6.241 6.302z' fill='inherit'/%3E%3C/svg%3E")}.tag,.tag--danger,.tag--ghost,.tag--info,.tag--inverse,.tag--primary,.tag--success,.tag--warning{display:inline-block;padding:3px .5rem;vertical-align:baseline;white-space:nowrap;color:#212529;border-radius:0;background-color:#e7e9ed;line-height:1}.tag--primary{background-color:#0275d8}.tag--primary,.tag--success{color:#fff;-webkit-box-shadow:none;box-shadow:none}.tag--success{background-color:#5cb85c}.tag--info{background-color:#5bc0de}.tag--info,.tag--warning{color:#000;-webkit-box-shadow:none;box-shadow:none}.tag--warning{background-color:#f0ad4e}.tag--danger{background-color:#d9534f}.tag--danger,.tag--inverse{color:#fff;-webkit-box-shadow:none;box-shadow:none}.tag--inverse{background-color:#333}.tag--ghost{background-color:transparent;color:#fff;-webkit-box-shadow:0 0 0 1px #fff inset;box-shadow:inset 0 0 0 1px #fff}.tag--small{font-size:1.2rem}.tag--big{font-size:2rem}.tag--block{width:100%!important;display:block}.disabled.tag--danger,.disabled.tag--ghost,.disabled.tag--info,.disabled.tag--inverse,.disabled.tag--primary,.disabled.tag--success,.disabled.tag--warning,.tag--disabled,.tag.disabled{opacity:.5;cursor:not-allowed}.tag--danger:empty,.tag--ghost:empty,.tag--info:empty,.tag--inverse:empty,.tag--primary:empty,.tag--success:empty,.tag--warning:empty,.tag:empty{display:none}.badge,.badge--danger,.badge--ghost,.badge--info,.badge--inverse,.badge--primary,.badge--success,.badge--warning{display:inline-block;padding:.5rem;border-radius:50%;color:#212529;background-color:#e7e9ed;line-height:1}.badge--danger:before,.badge--ghost:before,.badge--info:before,.badge--inverse:before,.badge--primary:before,.badge--success:before,.badge--warning:before,.badge:before{content:"";display:inline-block;vertical-align:middle;padding-top:100%}.badge--primary{background-color:#0275d8;color:#fff}.badge--success{background-color:#5cb85c;color:#fff}.badge--info{background-color:#5bc0de;color:#000}.badge--warning{background-color:#f0ad4e;color:#000}.badge--danger{background-color:#d9534f;color:#fff}.badge--inverse{background-color:#333;color:#fff}.badge--ghost{background-color:transparent;color:#fff}.badge--small{font-size:1.2rem}.badge--big{font-size:2rem}.badge--disabled,.badge.disabled,.disabled.badge--danger,.disabled.badge--ghost,.disabled.badge--info,.disabled.badge--inverse,.disabled.badge--primary,.disabled.badge--success,.disabled.badge--warning{opacity:.5;cursor:not-allowed}.badge--danger:empty,.badge--ghost:empty,.badge--info:empty,.badge--inverse:empty,.badge--primary:empty,.badge--success:empty,.badge--warning:empty,.badge:empty{display:none}.alert,.alert--danger,.alert--ghost,.alert--info,.alert--inverse,.alert--primary,.alert--success,.alert--warning{padding:1rem;margin-top:.75em;margin-bottom:0;color:#212529;border-radius:0;background-color:#e7e9ed}.alert--danger a,.alert--ghost a,.alert--info a,.alert--inverse a,.alert--primary a,.alert--success a,.alert--warning a,.alert a{color:inherit;text-decoration:underline}.alert--primary{background-color:#0275d8}.alert--primary,.alert--success{color:#fff;-webkit-box-shadow:none;box-shadow:none}.alert--success{background-color:#5cb85c}.alert--info{background-color:#5bc0de}.alert--info,.alert--warning{color:#000;-webkit-box-shadow:none;box-shadow:none}.alert--warning{background-color:#f0ad4e}.alert--danger{background-color:#d9534f}.alert--danger,.alert--inverse{color:#fff;-webkit-box-shadow:none;box-shadow:none}.alert--inverse{background-color:#333}.alert--ghost{background-color:transparent;color:#fff;-webkit-box-shadow:0 0 0 1px #fff inset;box-shadow:inset 0 0 0 1px #fff}.alert--small{font-size:1.2rem}.alert--big{font-size:2rem}.alert--block{width:100%!important;display:block}.alert--disabled,.alert.disabled,.disabled.alert--danger,.disabled.alert--ghost,.disabled.alert--info,.disabled.alert--inverse,.disabled.alert--primary,.disabled.alert--success,.disabled.alert--warning{opacity:.5;cursor:not-allowed}.alert--danger:empty,.alert--ghost:empty,.alert--info:empty,.alert--inverse:empty,.alert--primary:empty,.alert--success:empty,.alert--warning:empty,.alert:empty{display:none} --------------------------------------------------------------------------------