| 1 | 1 | # Change Log |
| 2 | 2 | |
| 3 | ## 0.7 | |
| 3 | ## 0.8 | |
| 4 | 4 | |
| 5 | 5 | - Load YAML variables from files |
| 6 | - Upgraded to Apache Commons Configuration 2.1 | |
| 7 | - Fixed bug with settings using comma-separated file extensions | |
| 8 | ||
| 9 | ## 0.7 | |
| 10 | ||
| 6 | 11 | - Added cursor to the preview pane |
| 7 | 12 | - Reconfigured constants to use settings |
| 1 | 1 |  |
| 2 | 2 | |
| 3 | Scrivenvar | |
| 3 | $application.title$ | |
| 4 | 4 | === |
| 5 | 5 |
| 30 | 30 | compile group: 'org.yaml', name: 'snakeyaml', version: '1.17' |
| 31 | 31 | compile group: 'com.googlecode.juniversalchardet', name: 'juniversalchardet', version: '1.0.3' |
| 32 | compile group: 'commons-configuration', name: 'commons-configuration', version: '1.10' | |
| 32 | compile group: 'org.apache.commons', name: 'commons-configuration2', version: '2.1' | |
| 33 | 33 | } |
| 34 | 34 |
| 1 | application: | |
| 2 | title: Scrivenvar | |
| 3 | ||
| 1 | 4 |
| 68 | 68 | |
| 69 | 69 | public static final String PREFS_ROOT = get( "preferences.root" ); |
| 70 | public static final String PREFS_ROOT_STATE = get( "preferences.root.state" ); | |
| 71 | public static final String PREFS_ROOT_OPTIONS = get( "preferences.root.options" ); | |
| 70 | public static final String PREFS_STATE = get( "preferences.root.state" ); | |
| 71 | public static final String PREFS_OPTIONS = get( "preferences.root.options" ); | |
| 72 | public static final String PREFS_DEFINITION_SOURCE = get( "preferences.root.definition.source" ); | |
| 72 | 73 | } |
| 73 | 74 |
| 28 | 28 | package com.scrivenvar; |
| 29 | 29 | |
| 30 | import com.scrivenvar.predicates.files.FileTypePredicate; | |
| 31 | import com.scrivenvar.service.Options; | |
| 32 | import com.scrivenvar.service.Settings; | |
| 33 | import com.scrivenvar.service.events.AlertMessage; | |
| 34 | import com.scrivenvar.service.events.AlertService; | |
| 35 | import static com.scrivenvar.service.events.AlertService.NO; | |
| 36 | import static com.scrivenvar.service.events.AlertService.YES; | |
| 37 | import com.scrivenvar.util.Utils; | |
| 38 | import java.io.File; | |
| 39 | import java.nio.file.Path; | |
| 40 | import java.util.ArrayList; | |
| 41 | import java.util.List; | |
| 42 | import java.util.function.Consumer; | |
| 43 | import java.util.prefs.Preferences; | |
| 44 | import java.util.stream.Collectors; | |
| 45 | import javafx.beans.property.ReadOnlyBooleanProperty; | |
| 46 | import javafx.beans.property.ReadOnlyBooleanWrapper; | |
| 47 | import javafx.beans.property.ReadOnlyObjectProperty; | |
| 48 | import javafx.beans.property.ReadOnlyObjectWrapper; | |
| 49 | import javafx.beans.value.ChangeListener; | |
| 50 | import javafx.beans.value.ObservableValue; | |
| 51 | import javafx.collections.ListChangeListener; | |
| 52 | import javafx.collections.ObservableList; | |
| 53 | import javafx.event.Event; | |
| 54 | import javafx.scene.Node; | |
| 55 | import javafx.scene.control.Alert; | |
| 56 | import javafx.scene.control.ButtonType; | |
| 57 | import javafx.scene.control.Tab; | |
| 58 | import javafx.scene.control.TabPane; | |
| 59 | import javafx.scene.control.TabPane.TabClosingPolicy; | |
| 60 | import javafx.scene.input.InputEvent; | |
| 61 | import javafx.stage.FileChooser; | |
| 62 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 63 | import javafx.stage.Window; | |
| 64 | import org.fxmisc.richtext.StyledTextArea; | |
| 65 | import org.fxmisc.wellbehaved.event.EventPattern; | |
| 66 | import org.fxmisc.wellbehaved.event.InputMap; | |
| 67 | import static com.scrivenvar.Messages.get; | |
| 68 | ||
| 69 | /** | |
| 70 | * Tab pane for file editors. | |
| 71 | * | |
| 72 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 73 | */ | |
| 74 | public final class FileEditorTabPane extends TabPane { | |
| 75 | ||
| 76 | private final static String FILTER_EXTENSIONS = "filter.file"; | |
| 77 | private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter"; | |
| 78 | ||
| 79 | private final Options options = Services.load( Options.class ); | |
| 80 | private final Settings settings = Services.load( Settings.class ); | |
| 81 | private final AlertService alertService = Services.load( AlertService.class ); | |
| 82 | ||
| 83 | private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>(); | |
| 84 | private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>(); | |
| 85 | private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper(); | |
| 86 | ||
| 87 | /** | |
| 88 | * Constructs a new file editor tab pane. | |
| 89 | */ | |
| 90 | public FileEditorTabPane() { | |
| 91 | final ObservableList<Tab> tabs = getTabs(); | |
| 92 | ||
| 93 | setFocusTraversable( false ); | |
| 94 | setTabClosingPolicy( TabClosingPolicy.ALL_TABS ); | |
| 95 | ||
| 96 | addTabSelectionListener( | |
| 97 | (ObservableValue<? extends Tab> tabPane, | |
| 98 | final Tab oldTab, final Tab newTab) -> { | |
| 99 | ||
| 100 | if( newTab != null ) { | |
| 101 | activeFileEditor.set( (FileEditorTab)newTab ); | |
| 102 | } | |
| 103 | } | |
| 104 | ); | |
| 105 | ||
| 106 | final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> { | |
| 107 | for( final Tab tab : tabs ) { | |
| 108 | if( ((FileEditorTab)tab).isModified() ) { | |
| 109 | this.anyFileEditorModified.set( true ); | |
| 110 | break; | |
| 111 | } | |
| 112 | } | |
| 113 | }; | |
| 114 | ||
| 115 | tabs.addListener( | |
| 116 | (ListChangeListener<Tab>)change -> { | |
| 117 | while( change.next() ) { | |
| 118 | if( change.wasAdded() ) { | |
| 119 | change.getAddedSubList().stream().forEach( (tab) -> { | |
| 120 | ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener ); | |
| 121 | } ); | |
| 122 | } else if( change.wasRemoved() ) { | |
| 123 | change.getRemoved().stream().forEach( (tab) -> { | |
| 124 | ((FileEditorTab)tab).modifiedProperty().removeListener( modifiedListener ); | |
| 125 | } ); | |
| 126 | } | |
| 127 | } | |
| 128 | ||
| 129 | // Changes in the tabs may also change anyFileEditorModified property | |
| 130 | // (e.g. closed modified file) | |
| 131 | modifiedListener.changed( null, null, null ); | |
| 132 | } | |
| 133 | ); | |
| 134 | } | |
| 135 | ||
| 136 | /** | |
| 137 | * Delegates to the active file editor. | |
| 138 | * | |
| 139 | * @param <T> Event type. | |
| 140 | * @param <U> Consumer type. | |
| 141 | * @param event Event to pass to the editor. | |
| 142 | * @param consumer Consumer to pass to the editor. | |
| 143 | */ | |
| 144 | public <T extends Event, U extends T> void addEventListener( | |
| 145 | final EventPattern<? super T, ? extends U> event, | |
| 146 | final Consumer<? super U> consumer ) { | |
| 147 | getActiveFileEditor().addEventListener( event, consumer ); | |
| 148 | } | |
| 149 | ||
| 150 | /** | |
| 151 | * Delegates to the active file editor pane, and, ultimately, to its text | |
| 152 | * area. | |
| 153 | * | |
| 154 | * @param map The map of methods to events. | |
| 155 | */ | |
| 156 | public void addEventListener( final InputMap<InputEvent> map ) { | |
| 157 | getActiveFileEditor().addEventListener( map ); | |
| 158 | } | |
| 159 | ||
| 160 | /** | |
| 161 | * Remove a keyboard event listener from the active file editor. | |
| 162 | * | |
| 163 | * @param map The keyboard events to remove. | |
| 164 | */ | |
| 165 | public void removeEventListener( final InputMap<InputEvent> map ) { | |
| 166 | getActiveFileEditor().removeEventListener( map ); | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Allows observers to be notified when the current file editor tab changes. | |
| 171 | * | |
| 172 | * @param listener The listener to notify of tab change events. | |
| 173 | */ | |
| 174 | public void addTabSelectionListener( final ChangeListener<Tab> listener ) { | |
| 175 | // Observe the tab so that when a new tab is opened or selected, | |
| 176 | // a notification is kicked off. | |
| 177 | getSelectionModel().selectedItemProperty().addListener( listener ); | |
| 178 | } | |
| 179 | ||
| 180 | /** | |
| 181 | * Allows clients to manipulate the editor content directly. | |
| 182 | * | |
| 183 | * @return The text area for the active file editor. | |
| 184 | */ | |
| 185 | public StyledTextArea getEditor() { | |
| 186 | return getActiveFileEditor().getEditorPane().getEditor(); | |
| 187 | } | |
| 188 | ||
| 189 | public FileEditorTab getActiveFileEditor() { | |
| 190 | return this.activeFileEditor.get(); | |
| 191 | } | |
| 192 | ||
| 193 | public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() { | |
| 194 | return this.activeFileEditor.getReadOnlyProperty(); | |
| 195 | } | |
| 196 | ||
| 197 | ReadOnlyBooleanProperty anyFileEditorModifiedProperty() { | |
| 198 | return this.anyFileEditorModified.getReadOnlyProperty(); | |
| 199 | } | |
| 200 | ||
| 201 | private FileEditorTab createFileEditor( final Path path ) { | |
| 202 | final FileEditorTab tab = new FileEditorTab( path ); | |
| 203 | ||
| 204 | tab.setOnCloseRequest( e -> { | |
| 205 | if( !canCloseEditor( tab ) ) { | |
| 206 | e.consume(); | |
| 207 | } | |
| 208 | } ); | |
| 209 | ||
| 210 | return tab; | |
| 211 | } | |
| 212 | ||
| 213 | /** | |
| 214 | * Called when the user selects New from the File menu. | |
| 215 | * | |
| 216 | * @return The newly added tab. | |
| 217 | */ | |
| 218 | void newEditor() { | |
| 219 | final FileEditorTab tab = createFileEditor( null ); | |
| 220 | ||
| 221 | getTabs().add( tab ); | |
| 222 | getSelectionModel().select( tab ); | |
| 223 | } | |
| 224 | ||
| 225 | void openFileDialog() { | |
| 226 | final String title = get( "Dialog.file.choose.open.title" ); | |
| 227 | final FileChooser dialog = createFileChooser( title ); | |
| 228 | openFiles( dialog.showOpenMultipleDialog( getWindow() ) ); | |
| 229 | } | |
| 230 | ||
| 231 | /** | |
| 232 | * Opens the files into new editors, unless one of those files was a | |
| 233 | * definition file. The definition file is loaded into the definition pane, | |
| 234 | * but only the first one selected (multiple definition files will result in a | |
| 235 | * warning). | |
| 236 | * | |
| 237 | * @param files The list of non-definition files that the were requested to | |
| 238 | * open. | |
| 239 | * | |
| 240 | * @return A list of files that can be opened in text editors. | |
| 241 | */ | |
| 242 | private void openFiles( final List<File> files ) { | |
| 243 | final FileTypePredicate predicate | |
| 244 | = new FileTypePredicate( createExtensionFilter( "definition" ).getExtensions() ); | |
| 245 | ||
| 246 | // The user might have opened multiple definitions files. These will | |
| 247 | // be discarded from the text editable files. | |
| 248 | final List<File> definitions | |
| 249 | = files.stream().filter( predicate ).collect( Collectors.toList() ); | |
| 250 | ||
| 251 | // Create a modifiable list to remove any definition files that were | |
| 252 | // opened. | |
| 253 | final List<File> editors = new ArrayList<>( files ); | |
| 254 | ||
| 255 | if( editors.size() > 0 ) { | |
| 256 | saveLastDirectory( editors.get( 0 ) ); | |
| 257 | } | |
| 258 | ||
| 259 | editors.removeAll( definitions ); | |
| 260 | ||
| 261 | // Open editor-friendly files (e.g,. Markdown, XML) in new tabs. | |
| 262 | if( editors.size() > 0 ) { | |
| 263 | openEditors( editors, 0 ); | |
| 264 | } | |
| 265 | ||
| 266 | if( definitions.size() > 0 ) { | |
| 267 | openDefinition( definitions.get( 0 ) ); | |
| 268 | } | |
| 269 | } | |
| 270 | ||
| 271 | private void openEditors( final List<File> files, final int activeIndex ) { | |
| 272 | final int fileTally = files.size(); | |
| 273 | final List<Tab> tabs = getTabs(); | |
| 274 | ||
| 275 | // Close single unmodified "Untitled" tab. | |
| 276 | if( tabs.size() == 1 ) { | |
| 277 | final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 )); | |
| 278 | ||
| 279 | if( fileEditor.getPath() == null && !fileEditor.isModified() ) { | |
| 280 | closeEditor( fileEditor, false ); | |
| 281 | } | |
| 282 | } | |
| 283 | ||
| 284 | for( int i = 0; i < fileTally; i++ ) { | |
| 285 | final Path path = files.get( i ).toPath(); | |
| 286 | ||
| 287 | FileEditorTab fileEditorTab = findEditor( path ); | |
| 288 | ||
| 289 | // Only open new files. | |
| 290 | if( fileEditorTab == null ) { | |
| 291 | fileEditorTab = createFileEditor( path ); | |
| 292 | getTabs().add( fileEditorTab ); | |
| 293 | } | |
| 294 | ||
| 295 | // Select the first file in the list. | |
| 296 | if( i == activeIndex ) { | |
| 297 | getSelectionModel().select( fileEditorTab ); | |
| 298 | } | |
| 299 | } | |
| 300 | } | |
| 301 | ||
| 302 | /** | |
| 303 | * Returns a property that changes when a new definition file is opened. | |
| 304 | * | |
| 305 | * @return The path to a definition file that was opened. | |
| 306 | */ | |
| 307 | public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() { | |
| 308 | return getOnOpenDefinitionFile().getReadOnlyProperty(); | |
| 309 | } | |
| 310 | ||
| 311 | private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() { | |
| 312 | return this.openDefinition; | |
| 313 | } | |
| 314 | ||
| 315 | /** | |
| 316 | * Called when the user has opened a definition file (using the file open | |
| 317 | * dialog box). This will replace the current set of definitions for the | |
| 318 | * active tab. | |
| 319 | * | |
| 320 | * @param definition The file to open. | |
| 321 | */ | |
| 322 | private void openDefinition( final File definition ) { | |
| 323 | // TODO: Prevent reading this file twice when a new text document is opened. | |
| 324 | // (might be a matter of checking the value first). | |
| 325 | getOnOpenDefinitionFile().set( definition.toPath() ); | |
| 326 | } | |
| 327 | ||
| 328 | boolean saveEditor( final FileEditorTab fileEditor ) { | |
| 329 | if( fileEditor == null || !fileEditor.isModified() ) { | |
| 330 | return true; | |
| 331 | } | |
| 332 | ||
| 333 | if( fileEditor.getPath() == null ) { | |
| 334 | getSelectionModel().select( fileEditor ); | |
| 335 | ||
| 336 | final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) ); | |
| 337 | final File file = fileChooser.showSaveDialog( getWindow() ); | |
| 338 | if( file == null ) { | |
| 339 | return false; | |
| 340 | } | |
| 341 | ||
| 342 | saveLastDirectory( file ); | |
| 343 | fileEditor.setPath( file.toPath() ); | |
| 344 | } | |
| 345 | ||
| 346 | return fileEditor.save(); | |
| 347 | } | |
| 348 | ||
| 349 | boolean saveAllEditors() { | |
| 350 | boolean success = true; | |
| 351 | ||
| 352 | for( FileEditorTab fileEditor : getAllEditors() ) { | |
| 353 | if( !saveEditor( fileEditor ) ) { | |
| 354 | success = false; | |
| 355 | } | |
| 356 | } | |
| 357 | ||
| 358 | return success; | |
| 359 | } | |
| 360 | ||
| 361 | /** | |
| 362 | * Answers whether the file has had modifications. ' | |
| 363 | * | |
| 364 | * @param tab THe tab to check for modifications. | |
| 365 | * | |
| 366 | * @return false The file is unmodified. | |
| 367 | */ | |
| 368 | boolean canCloseEditor( final FileEditorTab tab ) { | |
| 369 | if( !tab.isModified() ) { | |
| 370 | return true; | |
| 371 | } | |
| 372 | ||
| 373 | final AlertMessage message = getAlertService().createAlertMessage( | |
| 374 | Messages.get( "Alert.file.close.title" ), | |
| 375 | Messages.get( "Alert.file.close.text" ), | |
| 376 | tab.getText() | |
| 377 | ); | |
| 378 | ||
| 379 | final Alert alert = getAlertService().createAlertConfirmation( message ); | |
| 380 | final ButtonType response = alert.showAndWait().get(); | |
| 381 | ||
| 382 | return response == YES ? saveEditor( tab ) : response == NO; | |
| 383 | } | |
| 384 | ||
| 385 | private AlertService getAlertService() { | |
| 386 | return this.alertService; | |
| 387 | } | |
| 388 | ||
| 389 | boolean closeEditor( FileEditorTab fileEditor, boolean save ) { | |
| 390 | if( fileEditor == null ) { | |
| 391 | return true; | |
| 392 | } | |
| 393 | ||
| 394 | final Tab tab = fileEditor; | |
| 395 | ||
| 396 | if( save ) { | |
| 397 | Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT ); | |
| 398 | Event.fireEvent( tab, event ); | |
| 399 | ||
| 400 | if( event.isConsumed() ) { | |
| 401 | return false; | |
| 402 | } | |
| 403 | } | |
| 404 | ||
| 405 | getTabs().remove( tab ); | |
| 406 | ||
| 407 | if( tab.getOnClosed() != null ) { | |
| 408 | Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) ); | |
| 409 | } | |
| 410 | ||
| 411 | return true; | |
| 412 | } | |
| 413 | ||
| 414 | boolean closeAllEditors() { | |
| 415 | final FileEditorTab[] allEditors = getAllEditors(); | |
| 416 | final FileEditorTab activeEditor = getActiveFileEditor(); | |
| 417 | ||
| 418 | // try to save active tab first because in case the user decides to cancel, | |
| 419 | // then it stays active | |
| 420 | if( activeEditor != null && !canCloseEditor( activeEditor ) ) { | |
| 421 | return false; | |
| 422 | } | |
| 423 | ||
| 424 | // This should be called any time a tab changes. | |
| 425 | persistPreferences(); | |
| 426 | ||
| 427 | // save modified tabs | |
| 428 | for( int i = 0; i < allEditors.length; i++ ) { | |
| 429 | final FileEditorTab fileEditor = allEditors[ i ]; | |
| 430 | ||
| 431 | if( fileEditor == activeEditor ) { | |
| 432 | continue; | |
| 433 | } | |
| 434 | ||
| 435 | if( fileEditor.isModified() ) { | |
| 436 | // activate the modified tab to make its modified content visible to the user | |
| 437 | getSelectionModel().select( i ); | |
| 438 | ||
| 439 | if( !canCloseEditor( fileEditor ) ) { | |
| 440 | return false; | |
| 441 | } | |
| 442 | } | |
| 443 | } | |
| 444 | ||
| 445 | // Close all tabs. | |
| 446 | for( final FileEditorTab fileEditor : allEditors ) { | |
| 447 | if( !closeEditor( fileEditor, false ) ) { | |
| 448 | return false; | |
| 449 | } | |
| 450 | } | |
| 451 | ||
| 452 | return getTabs().isEmpty(); | |
| 453 | } | |
| 454 | ||
| 455 | private FileEditorTab[] getAllEditors() { | |
| 456 | final ObservableList<Tab> tabs = getTabs(); | |
| 457 | final int length = tabs.size(); | |
| 458 | final FileEditorTab[] allEditors = new FileEditorTab[ length ]; | |
| 459 | ||
| 460 | for( int i = 0; i < length; i++ ) { | |
| 461 | allEditors[ i ] = (FileEditorTab)tabs.get( i ); | |
| 462 | } | |
| 463 | ||
| 464 | return allEditors; | |
| 465 | } | |
| 466 | ||
| 467 | /** | |
| 468 | * Returns the file editor tab that has the given path. | |
| 469 | * | |
| 470 | * @return null No file editor tab for the given path was found. | |
| 471 | */ | |
| 472 | private FileEditorTab findEditor( final Path path ) { | |
| 473 | for( final Tab tab : getTabs() ) { | |
| 474 | final FileEditorTab fileEditor = (FileEditorTab)tab; | |
| 475 | ||
| 476 | if( fileEditor.isPath( path ) ) { | |
| 477 | return fileEditor; | |
| 478 | } | |
| 479 | } | |
| 480 | ||
| 481 | return null; | |
| 482 | } | |
| 483 | ||
| 484 | private FileChooser createFileChooser( String title ) { | |
| 485 | final FileChooser fileChooser = new FileChooser(); | |
| 486 | ||
| 487 | fileChooser.setTitle( title ); | |
| 488 | fileChooser.getExtensionFilters().addAll( | |
| 489 | createExtensionFilters() ); | |
| 490 | ||
| 491 | final String lastDirectory = getState().get( "lastDirectory", null ); | |
| 492 | File file = new File( (lastDirectory != null) ? lastDirectory : "." ); | |
| 493 | ||
| 494 | if( !file.isDirectory() ) { | |
| 495 | file = new File( "." ); | |
| 496 | } | |
| 497 | ||
| 498 | fileChooser.setInitialDirectory( file ); | |
| 499 | return fileChooser; | |
| 500 | } | |
| 501 | ||
| 502 | private List<ExtensionFilter> createExtensionFilters() { | |
| 503 | final List<ExtensionFilter> list = new ArrayList<>(); | |
| 504 | ||
| 505 | // TODO: Return a list of all properties that match the filter prefix. | |
| 506 | // This will allow dynamic filters to be added and removed just by | |
| 507 | // updating the properties file. | |
| 508 | list.add( createExtensionFilter( "markdown" ) ); | |
| 509 | list.add( createExtensionFilter( "definition" ) ); | |
| 510 | list.add( createExtensionFilter( "xml" ) ); | |
| 511 | list.add( createExtensionFilter( "all" ) ); | |
| 512 | return list; | |
| 513 | } | |
| 514 | ||
| 515 | private ExtensionFilter createExtensionFilter( final String filetype ) { | |
| 516 | final String tKey = String.format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype ); | |
| 517 | final String eKey = String.format( "%s.ext.%s", FILTER_EXTENSIONS, filetype ); | |
| 518 | ||
| 519 | return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) ); | |
| 520 | } | |
| 521 | ||
| 522 | private List<String> getExtensions( final String key ) { | |
| 523 | return getSettings().getStringSettingList( key ); | |
| 524 | } | |
| 525 | ||
| 526 | private void saveLastDirectory( final File file ) { | |
| 527 | getState().put( "lastDirectory", file.getParent() ); | |
| 528 | } | |
| 529 | ||
| 530 | public void restorePreferences() { | |
| 531 | int activeIndex = 0; | |
| 532 | ||
| 533 | final Preferences preferences = getState(); | |
| 534 | final String[] fileNames = Utils.getPrefsStrings( preferences, "file" ); | |
| 535 | final String activeFileName = preferences.get( "activeFile", null ); | |
| 536 | ||
| 537 | final ArrayList<File> files = new ArrayList<>( fileNames.length ); | |
| 538 | ||
| 539 | for( final String fileName : fileNames ) { | |
| 540 | final File file = new File( fileName ); | |
| 541 | ||
| 542 | if( file.exists() ) { | |
| 543 | files.add( file ); | |
| 544 | ||
| 545 | if( fileName.equals( activeFileName ) ) { | |
| 546 | activeIndex = files.size() - 1; | |
| 547 | } | |
| 548 | } | |
| 549 | } | |
| 550 | ||
| 551 | if( files.isEmpty() ) { | |
| 552 | newEditor(); | |
| 553 | } else { | |
| 554 | openEditors( files, activeIndex ); | |
| 555 | } | |
| 556 | } | |
| 557 | ||
| 558 | public void persistPreferences() { | |
| 559 | final ObservableList<Tab> allEditors = getTabs(); | |
| 560 | final List<String> fileNames = new ArrayList<>( allEditors.size() ); | |
| 561 | ||
| 562 | for( final Tab tab : allEditors ) { | |
| 563 | final FileEditorTab fileEditor = (FileEditorTab)tab; | |
| 564 | final Path filePath = fileEditor.getPath(); | |
| 565 | ||
| 566 | if( filePath != null ) { | |
| 567 | fileNames.add( filePath.toString() ); | |
| 568 | } | |
| 569 | } | |
| 570 | ||
| 571 | final Preferences preferences = getState(); | |
| 572 | Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) ); | |
| 573 | ||
| 574 | final FileEditorTab activeEditor = getActiveFileEditor(); | |
| 575 | final Path filePath = activeEditor == null ? null : activeEditor.getPath(); | |
| 576 | ||
| 577 | if( filePath == null ) { | |
| 578 | preferences.remove( "activeFile" ); | |
| 579 | } else { | |
| 580 | preferences.put( "activeFile", filePath.toString() ); | |
| 581 | } | |
| 582 | } | |
| 583 | ||
| 584 | private Settings getSettings() { | |
| 585 | return this.settings; | |
| 586 | } | |
| 587 | ||
| 588 | protected Options getOptions() { | |
| 589 | return this.options; | |
| 590 | } | |
| 591 | ||
| 592 | private Window getWindow() { | |
| 593 | return getScene().getWindow(); | |
| 594 | } | |
| 595 | ||
| 596 | protected Preferences getState() { | |
| 30 | import static com.scrivenvar.Messages.get; | |
| 31 | import com.scrivenvar.predicates.files.FileTypePredicate; | |
| 32 | import com.scrivenvar.service.Options; | |
| 33 | import com.scrivenvar.service.Settings; | |
| 34 | import com.scrivenvar.service.events.AlertMessage; | |
| 35 | import com.scrivenvar.service.events.AlertService; | |
| 36 | import static com.scrivenvar.service.events.AlertService.NO; | |
| 37 | import static com.scrivenvar.service.events.AlertService.YES; | |
| 38 | import com.scrivenvar.util.Utils; | |
| 39 | import java.io.File; | |
| 40 | import java.nio.file.Path; | |
| 41 | import java.util.ArrayList; | |
| 42 | import java.util.List; | |
| 43 | import java.util.function.Consumer; | |
| 44 | import java.util.prefs.Preferences; | |
| 45 | import java.util.stream.Collectors; | |
| 46 | import javafx.beans.property.ReadOnlyBooleanProperty; | |
| 47 | import javafx.beans.property.ReadOnlyBooleanWrapper; | |
| 48 | import javafx.beans.property.ReadOnlyObjectProperty; | |
| 49 | import javafx.beans.property.ReadOnlyObjectWrapper; | |
| 50 | import javafx.beans.value.ChangeListener; | |
| 51 | import javafx.beans.value.ObservableValue; | |
| 52 | import javafx.collections.ListChangeListener; | |
| 53 | import javafx.collections.ObservableList; | |
| 54 | import javafx.event.Event; | |
| 55 | import javafx.scene.Node; | |
| 56 | import javafx.scene.control.Alert; | |
| 57 | import javafx.scene.control.ButtonType; | |
| 58 | import javafx.scene.control.Tab; | |
| 59 | import javafx.scene.control.TabPane; | |
| 60 | import javafx.scene.control.TabPane.TabClosingPolicy; | |
| 61 | import javafx.scene.input.InputEvent; | |
| 62 | import javafx.stage.FileChooser; | |
| 63 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 64 | import javafx.stage.Window; | |
| 65 | import org.fxmisc.richtext.StyledTextArea; | |
| 66 | import org.fxmisc.wellbehaved.event.EventPattern; | |
| 67 | import org.fxmisc.wellbehaved.event.InputMap; | |
| 68 | ||
| 69 | /** | |
| 70 | * Tab pane for file editors. | |
| 71 | * | |
| 72 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 73 | */ | |
| 74 | public final class FileEditorTabPane extends TabPane { | |
| 75 | ||
| 76 | private final static String FILTER_EXTENSIONS = "filter.file"; | |
| 77 | private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter"; | |
| 78 | ||
| 79 | private final Options options = Services.load( Options.class ); | |
| 80 | private final Settings settings = Services.load( Settings.class ); | |
| 81 | private final AlertService alertService = Services.load( AlertService.class ); | |
| 82 | ||
| 83 | private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>(); | |
| 84 | private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>(); | |
| 85 | private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper(); | |
| 86 | ||
| 87 | /** | |
| 88 | * Constructs a new file editor tab pane. | |
| 89 | */ | |
| 90 | public FileEditorTabPane() { | |
| 91 | final ObservableList<Tab> tabs = getTabs(); | |
| 92 | ||
| 93 | setFocusTraversable( false ); | |
| 94 | setTabClosingPolicy( TabClosingPolicy.ALL_TABS ); | |
| 95 | ||
| 96 | addTabSelectionListener( | |
| 97 | (ObservableValue<? extends Tab> tabPane, | |
| 98 | final Tab oldTab, final Tab newTab) -> { | |
| 99 | ||
| 100 | if( newTab != null ) { | |
| 101 | activeFileEditor.set( (FileEditorTab)newTab ); | |
| 102 | } | |
| 103 | } | |
| 104 | ); | |
| 105 | ||
| 106 | final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> { | |
| 107 | for( final Tab tab : tabs ) { | |
| 108 | if( ((FileEditorTab)tab).isModified() ) { | |
| 109 | this.anyFileEditorModified.set( true ); | |
| 110 | break; | |
| 111 | } | |
| 112 | } | |
| 113 | }; | |
| 114 | ||
| 115 | tabs.addListener( | |
| 116 | (ListChangeListener<Tab>)change -> { | |
| 117 | while( change.next() ) { | |
| 118 | if( change.wasAdded() ) { | |
| 119 | change.getAddedSubList().stream().forEach( (tab) -> { | |
| 120 | ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener ); | |
| 121 | } ); | |
| 122 | } else if( change.wasRemoved() ) { | |
| 123 | change.getRemoved().stream().forEach( (tab) -> { | |
| 124 | ((FileEditorTab)tab).modifiedProperty().removeListener( modifiedListener ); | |
| 125 | } ); | |
| 126 | } | |
| 127 | } | |
| 128 | ||
| 129 | // Changes in the tabs may also change anyFileEditorModified property | |
| 130 | // (e.g. closed modified file) | |
| 131 | modifiedListener.changed( null, null, null ); | |
| 132 | } | |
| 133 | ); | |
| 134 | } | |
| 135 | ||
| 136 | /** | |
| 137 | * Delegates to the active file editor. | |
| 138 | * | |
| 139 | * @param <T> Event type. | |
| 140 | * @param <U> Consumer type. | |
| 141 | * @param event Event to pass to the editor. | |
| 142 | * @param consumer Consumer to pass to the editor. | |
| 143 | */ | |
| 144 | public <T extends Event, U extends T> void addEventListener( | |
| 145 | final EventPattern<? super T, ? extends U> event, | |
| 146 | final Consumer<? super U> consumer ) { | |
| 147 | getActiveFileEditor().addEventListener( event, consumer ); | |
| 148 | } | |
| 149 | ||
| 150 | /** | |
| 151 | * Delegates to the active file editor pane, and, ultimately, to its text | |
| 152 | * area. | |
| 153 | * | |
| 154 | * @param map The map of methods to events. | |
| 155 | */ | |
| 156 | public void addEventListener( final InputMap<InputEvent> map ) { | |
| 157 | getActiveFileEditor().addEventListener( map ); | |
| 158 | } | |
| 159 | ||
| 160 | /** | |
| 161 | * Remove a keyboard event listener from the active file editor. | |
| 162 | * | |
| 163 | * @param map The keyboard events to remove. | |
| 164 | */ | |
| 165 | public void removeEventListener( final InputMap<InputEvent> map ) { | |
| 166 | getActiveFileEditor().removeEventListener( map ); | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Allows observers to be notified when the current file editor tab changes. | |
| 171 | * | |
| 172 | * @param listener The listener to notify of tab change events. | |
| 173 | */ | |
| 174 | public void addTabSelectionListener( final ChangeListener<Tab> listener ) { | |
| 175 | // Observe the tab so that when a new tab is opened or selected, | |
| 176 | // a notification is kicked off. | |
| 177 | getSelectionModel().selectedItemProperty().addListener( listener ); | |
| 178 | } | |
| 179 | ||
| 180 | /** | |
| 181 | * Allows clients to manipulate the editor content directly. | |
| 182 | * | |
| 183 | * @return The text area for the active file editor. | |
| 184 | */ | |
| 185 | public StyledTextArea getEditor() { | |
| 186 | return getActiveFileEditor().getEditorPane().getEditor(); | |
| 187 | } | |
| 188 | ||
| 189 | public FileEditorTab getActiveFileEditor() { | |
| 190 | return this.activeFileEditor.get(); | |
| 191 | } | |
| 192 | ||
| 193 | public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() { | |
| 194 | return this.activeFileEditor.getReadOnlyProperty(); | |
| 195 | } | |
| 196 | ||
| 197 | ReadOnlyBooleanProperty anyFileEditorModifiedProperty() { | |
| 198 | return this.anyFileEditorModified.getReadOnlyProperty(); | |
| 199 | } | |
| 200 | ||
| 201 | private FileEditorTab createFileEditor( final Path path ) { | |
| 202 | final FileEditorTab tab = new FileEditorTab( path ); | |
| 203 | ||
| 204 | tab.setOnCloseRequest( e -> { | |
| 205 | if( !canCloseEditor( tab ) ) { | |
| 206 | e.consume(); | |
| 207 | } | |
| 208 | } ); | |
| 209 | ||
| 210 | return tab; | |
| 211 | } | |
| 212 | ||
| 213 | /** | |
| 214 | * Called when the user selects New from the File menu. | |
| 215 | * | |
| 216 | * @return The newly added tab. | |
| 217 | */ | |
| 218 | void newEditor() { | |
| 219 | final FileEditorTab tab = createFileEditor( null ); | |
| 220 | ||
| 221 | getTabs().add( tab ); | |
| 222 | getSelectionModel().select( tab ); | |
| 223 | } | |
| 224 | ||
| 225 | void openFileDialog() { | |
| 226 | final String title = get( "Dialog.file.choose.open.title" ); | |
| 227 | final FileChooser dialog = createFileChooser( title ); | |
| 228 | final List<File> files = dialog.showOpenMultipleDialog( getWindow() ); | |
| 229 | ||
| 230 | if( files != null ) { | |
| 231 | openFiles( files ); | |
| 232 | } | |
| 233 | } | |
| 234 | ||
| 235 | /** | |
| 236 | * Opens the files into new editors, unless one of those files was a | |
| 237 | * definition file. The definition file is loaded into the definition pane, | |
| 238 | * but only the first one selected (multiple definition files will result in a | |
| 239 | * warning). | |
| 240 | * | |
| 241 | * @param files The list of non-definition files that the were requested to | |
| 242 | * open. | |
| 243 | * | |
| 244 | * @return A list of files that can be opened in text editors. | |
| 245 | */ | |
| 246 | private void openFiles( final List<File> files ) { | |
| 247 | final FileTypePredicate predicate | |
| 248 | = new FileTypePredicate( createExtensionFilter( "definition" ).getExtensions() ); | |
| 249 | ||
| 250 | // The user might have opened multiple definitions files. These will | |
| 251 | // be discarded from the text editable files. | |
| 252 | final List<File> definitions | |
| 253 | = files.stream().filter( predicate ).collect( Collectors.toList() ); | |
| 254 | ||
| 255 | // Create a modifiable list to remove any definition files that were | |
| 256 | // opened. | |
| 257 | final List<File> editors = new ArrayList<>( files ); | |
| 258 | ||
| 259 | if( editors.size() > 0 ) { | |
| 260 | saveLastDirectory( editors.get( 0 ) ); | |
| 261 | } | |
| 262 | ||
| 263 | editors.removeAll( definitions ); | |
| 264 | ||
| 265 | // Open editor-friendly files (e.g,. Markdown, XML) in new tabs. | |
| 266 | if( editors.size() > 0 ) { | |
| 267 | openEditors( editors, 0 ); | |
| 268 | } | |
| 269 | ||
| 270 | if( definitions.size() > 0 ) { | |
| 271 | openDefinition( definitions.get( 0 ) ); | |
| 272 | } | |
| 273 | } | |
| 274 | ||
| 275 | private void openEditors( final List<File> files, final int activeIndex ) { | |
| 276 | final int fileTally = files.size(); | |
| 277 | final List<Tab> tabs = getTabs(); | |
| 278 | ||
| 279 | // Close single unmodified "Untitled" tab. | |
| 280 | if( tabs.size() == 1 ) { | |
| 281 | final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 )); | |
| 282 | ||
| 283 | if( fileEditor.getPath() == null && !fileEditor.isModified() ) { | |
| 284 | closeEditor( fileEditor, false ); | |
| 285 | } | |
| 286 | } | |
| 287 | ||
| 288 | for( int i = 0; i < fileTally; i++ ) { | |
| 289 | final Path path = files.get( i ).toPath(); | |
| 290 | ||
| 291 | FileEditorTab fileEditorTab = findEditor( path ); | |
| 292 | ||
| 293 | // Only open new files. | |
| 294 | if( fileEditorTab == null ) { | |
| 295 | fileEditorTab = createFileEditor( path ); | |
| 296 | getTabs().add( fileEditorTab ); | |
| 297 | } | |
| 298 | ||
| 299 | // Select the first file in the list. | |
| 300 | if( i == activeIndex ) { | |
| 301 | getSelectionModel().select( fileEditorTab ); | |
| 302 | } | |
| 303 | } | |
| 304 | } | |
| 305 | ||
| 306 | /** | |
| 307 | * Returns a property that changes when a new definition file is opened. | |
| 308 | * | |
| 309 | * @return The path to a definition file that was opened. | |
| 310 | */ | |
| 311 | public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() { | |
| 312 | return getOnOpenDefinitionFile().getReadOnlyProperty(); | |
| 313 | } | |
| 314 | ||
| 315 | private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() { | |
| 316 | return this.openDefinition; | |
| 317 | } | |
| 318 | ||
| 319 | /** | |
| 320 | * Called when the user has opened a definition file (using the file open | |
| 321 | * dialog box). This will replace the current set of definitions for the | |
| 322 | * active tab. | |
| 323 | * | |
| 324 | * @param definition The file to open. | |
| 325 | */ | |
| 326 | private void openDefinition( final File definition ) { | |
| 327 | // TODO: Prevent reading this file twice when a new text document is opened. | |
| 328 | // (might be a matter of checking the value first). | |
| 329 | getOnOpenDefinitionFile().set( definition.toPath() ); | |
| 330 | } | |
| 331 | ||
| 332 | boolean saveEditor( final FileEditorTab fileEditor ) { | |
| 333 | if( fileEditor == null || !fileEditor.isModified() ) { | |
| 334 | return true; | |
| 335 | } | |
| 336 | ||
| 337 | if( fileEditor.getPath() == null ) { | |
| 338 | getSelectionModel().select( fileEditor ); | |
| 339 | ||
| 340 | final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) ); | |
| 341 | final File file = fileChooser.showSaveDialog( getWindow() ); | |
| 342 | if( file == null ) { | |
| 343 | return false; | |
| 344 | } | |
| 345 | ||
| 346 | saveLastDirectory( file ); | |
| 347 | fileEditor.setPath( file.toPath() ); | |
| 348 | } | |
| 349 | ||
| 350 | return fileEditor.save(); | |
| 351 | } | |
| 352 | ||
| 353 | boolean saveAllEditors() { | |
| 354 | boolean success = true; | |
| 355 | ||
| 356 | for( FileEditorTab fileEditor : getAllEditors() ) { | |
| 357 | if( !saveEditor( fileEditor ) ) { | |
| 358 | success = false; | |
| 359 | } | |
| 360 | } | |
| 361 | ||
| 362 | return success; | |
| 363 | } | |
| 364 | ||
| 365 | /** | |
| 366 | * Answers whether the file has had modifications. ' | |
| 367 | * | |
| 368 | * @param tab THe tab to check for modifications. | |
| 369 | * | |
| 370 | * @return false The file is unmodified. | |
| 371 | */ | |
| 372 | boolean canCloseEditor( final FileEditorTab tab ) { | |
| 373 | if( !tab.isModified() ) { | |
| 374 | return true; | |
| 375 | } | |
| 376 | ||
| 377 | final AlertMessage message = getAlertService().createAlertMessage( | |
| 378 | Messages.get( "Alert.file.close.title" ), | |
| 379 | Messages.get( "Alert.file.close.text" ), | |
| 380 | tab.getText() | |
| 381 | ); | |
| 382 | ||
| 383 | final Alert alert = getAlertService().createAlertConfirmation( message ); | |
| 384 | final ButtonType response = alert.showAndWait().get(); | |
| 385 | ||
| 386 | return response == YES ? saveEditor( tab ) : response == NO; | |
| 387 | } | |
| 388 | ||
| 389 | private AlertService getAlertService() { | |
| 390 | return this.alertService; | |
| 391 | } | |
| 392 | ||
| 393 | boolean closeEditor( FileEditorTab fileEditor, boolean save ) { | |
| 394 | if( fileEditor == null ) { | |
| 395 | return true; | |
| 396 | } | |
| 397 | ||
| 398 | final Tab tab = fileEditor; | |
| 399 | ||
| 400 | if( save ) { | |
| 401 | Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT ); | |
| 402 | Event.fireEvent( tab, event ); | |
| 403 | ||
| 404 | if( event.isConsumed() ) { | |
| 405 | return false; | |
| 406 | } | |
| 407 | } | |
| 408 | ||
| 409 | getTabs().remove( tab ); | |
| 410 | ||
| 411 | if( tab.getOnClosed() != null ) { | |
| 412 | Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) ); | |
| 413 | } | |
| 414 | ||
| 415 | return true; | |
| 416 | } | |
| 417 | ||
| 418 | boolean closeAllEditors() { | |
| 419 | final FileEditorTab[] allEditors = getAllEditors(); | |
| 420 | final FileEditorTab activeEditor = getActiveFileEditor(); | |
| 421 | ||
| 422 | // try to save active tab first because in case the user decides to cancel, | |
| 423 | // then it stays active | |
| 424 | if( activeEditor != null && !canCloseEditor( activeEditor ) ) { | |
| 425 | return false; | |
| 426 | } | |
| 427 | ||
| 428 | // This should be called any time a tab changes. | |
| 429 | persistPreferences(); | |
| 430 | ||
| 431 | // save modified tabs | |
| 432 | for( int i = 0; i < allEditors.length; i++ ) { | |
| 433 | final FileEditorTab fileEditor = allEditors[ i ]; | |
| 434 | ||
| 435 | if( fileEditor == activeEditor ) { | |
| 436 | continue; | |
| 437 | } | |
| 438 | ||
| 439 | if( fileEditor.isModified() ) { | |
| 440 | // activate the modified tab to make its modified content visible to the user | |
| 441 | getSelectionModel().select( i ); | |
| 442 | ||
| 443 | if( !canCloseEditor( fileEditor ) ) { | |
| 444 | return false; | |
| 445 | } | |
| 446 | } | |
| 447 | } | |
| 448 | ||
| 449 | // Close all tabs. | |
| 450 | for( final FileEditorTab fileEditor : allEditors ) { | |
| 451 | if( !closeEditor( fileEditor, false ) ) { | |
| 452 | return false; | |
| 453 | } | |
| 454 | } | |
| 455 | ||
| 456 | return getTabs().isEmpty(); | |
| 457 | } | |
| 458 | ||
| 459 | private FileEditorTab[] getAllEditors() { | |
| 460 | final ObservableList<Tab> tabs = getTabs(); | |
| 461 | final int length = tabs.size(); | |
| 462 | final FileEditorTab[] allEditors = new FileEditorTab[ length ]; | |
| 463 | ||
| 464 | for( int i = 0; i < length; i++ ) { | |
| 465 | allEditors[ i ] = (FileEditorTab)tabs.get( i ); | |
| 466 | } | |
| 467 | ||
| 468 | return allEditors; | |
| 469 | } | |
| 470 | ||
| 471 | /** | |
| 472 | * Returns the file editor tab that has the given path. | |
| 473 | * | |
| 474 | * @return null No file editor tab for the given path was found. | |
| 475 | */ | |
| 476 | private FileEditorTab findEditor( final Path path ) { | |
| 477 | for( final Tab tab : getTabs() ) { | |
| 478 | final FileEditorTab fileEditor = (FileEditorTab)tab; | |
| 479 | ||
| 480 | if( fileEditor.isPath( path ) ) { | |
| 481 | return fileEditor; | |
| 482 | } | |
| 483 | } | |
| 484 | ||
| 485 | return null; | |
| 486 | } | |
| 487 | ||
| 488 | private FileChooser createFileChooser( String title ) { | |
| 489 | final FileChooser fileChooser = new FileChooser(); | |
| 490 | ||
| 491 | fileChooser.setTitle( title ); | |
| 492 | fileChooser.getExtensionFilters().addAll( | |
| 493 | createExtensionFilters() ); | |
| 494 | ||
| 495 | final String lastDirectory = getPreferences().get( "lastDirectory", null ); | |
| 496 | File file = new File( (lastDirectory != null) ? lastDirectory : "." ); | |
| 497 | ||
| 498 | if( !file.isDirectory() ) { | |
| 499 | file = new File( "." ); | |
| 500 | } | |
| 501 | ||
| 502 | fileChooser.setInitialDirectory( file ); | |
| 503 | return fileChooser; | |
| 504 | } | |
| 505 | ||
| 506 | private List<ExtensionFilter> createExtensionFilters() { | |
| 507 | final List<ExtensionFilter> list = new ArrayList<>(); | |
| 508 | ||
| 509 | // TODO: Return a list of all properties that match the filter prefix. | |
| 510 | // This will allow dynamic filters to be added and removed just by | |
| 511 | // updating the properties file. | |
| 512 | list.add( createExtensionFilter( "definition" ) ); | |
| 513 | list.add( createExtensionFilter( "markdown" ) ); | |
| 514 | list.add( createExtensionFilter( "xml" ) ); | |
| 515 | list.add( createExtensionFilter( "all" ) ); | |
| 516 | return list; | |
| 517 | } | |
| 518 | ||
| 519 | private ExtensionFilter createExtensionFilter( final String filetype ) { | |
| 520 | final String tKey = String.format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype ); | |
| 521 | final String eKey = String.format( "%s.ext.%s", FILTER_EXTENSIONS, filetype ); | |
| 522 | ||
| 523 | return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) ); | |
| 524 | } | |
| 525 | ||
| 526 | private List<String> getExtensions( final String key ) { | |
| 527 | return getSettings().getStringSettingList( key ); | |
| 528 | } | |
| 529 | ||
| 530 | private void saveLastDirectory( final File file ) { | |
| 531 | getPreferences().put( "lastDirectory", file.getParent() ); | |
| 532 | } | |
| 533 | ||
| 534 | public void restorePreferences() { | |
| 535 | int activeIndex = 0; | |
| 536 | ||
| 537 | final Preferences preferences = getPreferences(); | |
| 538 | final String[] fileNames = Utils.getPrefsStrings( preferences, "file" ); | |
| 539 | final String activeFileName = preferences.get( "activeFile", null ); | |
| 540 | ||
| 541 | final ArrayList<File> files = new ArrayList<>( fileNames.length ); | |
| 542 | ||
| 543 | for( final String fileName : fileNames ) { | |
| 544 | final File file = new File( fileName ); | |
| 545 | ||
| 546 | if( file.exists() ) { | |
| 547 | files.add( file ); | |
| 548 | ||
| 549 | if( fileName.equals( activeFileName ) ) { | |
| 550 | activeIndex = files.size() - 1; | |
| 551 | } | |
| 552 | } | |
| 553 | } | |
| 554 | ||
| 555 | if( files.isEmpty() ) { | |
| 556 | newEditor(); | |
| 557 | } else { | |
| 558 | openEditors( files, activeIndex ); | |
| 559 | } | |
| 560 | } | |
| 561 | ||
| 562 | public void persistPreferences() { | |
| 563 | final ObservableList<Tab> allEditors = getTabs(); | |
| 564 | final List<String> fileNames = new ArrayList<>( allEditors.size() ); | |
| 565 | ||
| 566 | for( final Tab tab : allEditors ) { | |
| 567 | final FileEditorTab fileEditor = (FileEditorTab)tab; | |
| 568 | final Path filePath = fileEditor.getPath(); | |
| 569 | ||
| 570 | if( filePath != null ) { | |
| 571 | fileNames.add( filePath.toString() ); | |
| 572 | } | |
| 573 | } | |
| 574 | ||
| 575 | final Preferences preferences = getPreferences(); | |
| 576 | Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) ); | |
| 577 | ||
| 578 | final FileEditorTab activeEditor = getActiveFileEditor(); | |
| 579 | final Path filePath = activeEditor == null ? null : activeEditor.getPath(); | |
| 580 | ||
| 581 | if( filePath == null ) { | |
| 582 | preferences.remove( "activeFile" ); | |
| 583 | } else { | |
| 584 | preferences.put( "activeFile", filePath.toString() ); | |
| 585 | } | |
| 586 | } | |
| 587 | ||
| 588 | private Settings getSettings() { | |
| 589 | return this.settings; | |
| 590 | } | |
| 591 | ||
| 592 | protected Options getOptions() { | |
| 593 | return this.options; | |
| 594 | } | |
| 595 | ||
| 596 | private Window getWindow() { | |
| 597 | return getScene().getWindow(); | |
| 598 | } | |
| 599 | ||
| 600 | private Preferences getPreferences() { | |
| 597 | 601 | return getOptions().getState(); |
| 598 | 602 | } |
| 29 | 29 | |
| 30 | 30 | import static com.scrivenvar.Constants.FILE_LOGO_32; |
| 31 | import static com.scrivenvar.Constants.STYLESHEET_SCENE; | |
| 32 | import static com.scrivenvar.Messages.get; | |
| 33 | import com.scrivenvar.definition.DefinitionFactory; | |
| 34 | import com.scrivenvar.definition.DefinitionPane; | |
| 35 | import com.scrivenvar.definition.DefinitionSource; | |
| 36 | import com.scrivenvar.editors.VariableNameInjector; | |
| 37 | import com.scrivenvar.editors.markdown.MarkdownEditorPane; | |
| 38 | import com.scrivenvar.preview.HTMLPreviewPane; | |
| 39 | import com.scrivenvar.processors.HTMLPreviewProcessor; | |
| 40 | import com.scrivenvar.processors.MarkdownCaretInsertionProcessor; | |
| 41 | import com.scrivenvar.processors.MarkdownCaretReplacementProcessor; | |
| 42 | import com.scrivenvar.processors.MarkdownProcessor; | |
| 43 | import com.scrivenvar.processors.Processor; | |
| 44 | import com.scrivenvar.processors.VariableProcessor; | |
| 45 | import com.scrivenvar.service.Options; | |
| 46 | import com.scrivenvar.util.Action; | |
| 47 | import com.scrivenvar.util.ActionUtils; | |
| 48 | import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION; | |
| 49 | import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR; | |
| 50 | import static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW; | |
| 51 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD; | |
| 52 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE; | |
| 53 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT; | |
| 54 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT; | |
| 55 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT; | |
| 56 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT; | |
| 57 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER; | |
| 58 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC; | |
| 59 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK; | |
| 60 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL; | |
| 61 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL; | |
| 62 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT; | |
| 63 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT; | |
| 64 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT; | |
| 65 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH; | |
| 66 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO; | |
| 67 | import java.io.File; | |
| 68 | import java.nio.file.Path; | |
| 69 | import java.util.HashMap; | |
| 70 | import java.util.Map; | |
| 71 | import java.util.function.Function; | |
| 72 | import java.util.prefs.Preferences; | |
| 73 | import javafx.beans.binding.Bindings; | |
| 74 | import javafx.beans.binding.BooleanBinding; | |
| 75 | import javafx.beans.property.BooleanProperty; | |
| 76 | import javafx.beans.property.SimpleBooleanProperty; | |
| 77 | import javafx.beans.value.ObservableBooleanValue; | |
| 78 | import javafx.beans.value.ObservableValue; | |
| 79 | import javafx.collections.ListChangeListener.Change; | |
| 80 | import javafx.collections.ObservableList; | |
| 81 | import javafx.event.Event; | |
| 82 | import javafx.scene.Node; | |
| 83 | import javafx.scene.Scene; | |
| 84 | import javafx.scene.control.Alert; | |
| 85 | import javafx.scene.control.Alert.AlertType; | |
| 86 | import javafx.scene.control.Menu; | |
| 87 | import javafx.scene.control.MenuBar; | |
| 88 | import javafx.scene.control.SplitPane; | |
| 89 | import javafx.scene.control.Tab; | |
| 90 | import javafx.scene.control.ToolBar; | |
| 91 | import javafx.scene.control.TreeView; | |
| 92 | import javafx.scene.image.Image; | |
| 93 | import javafx.scene.image.ImageView; | |
| 94 | import static javafx.scene.input.KeyCode.ESCAPE; | |
| 95 | import javafx.scene.input.KeyEvent; | |
| 96 | import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED; | |
| 97 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 98 | import javafx.scene.layout.BorderPane; | |
| 99 | import javafx.scene.layout.VBox; | |
| 100 | import javafx.stage.Window; | |
| 101 | import javafx.stage.WindowEvent; | |
| 102 | ||
| 103 | /** | |
| 104 | * Main window containing a tab pane in the center for file editors. | |
| 105 | * | |
| 106 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 107 | */ | |
| 108 | public class MainWindow { | |
| 109 | ||
| 110 | private final Options options = Services.load( Options.class ); | |
| 111 | ||
| 112 | private Scene scene; | |
| 113 | ||
| 114 | private DefinitionPane definitionPane; | |
| 115 | private FileEditorTabPane fileEditorPane; | |
| 116 | private HTMLPreviewPane previewPane; | |
| 117 | ||
| 118 | private VariableNameInjector variableNameInjector; | |
| 119 | ||
| 120 | private MenuBar menuBar; | |
| 121 | ||
| 122 | public MainWindow() { | |
| 123 | initLayout(); | |
| 124 | initOpenDefinitionListener(); | |
| 125 | initTabAddedListener(); | |
| 126 | initTabChangeListener(); | |
| 127 | initPreferences(); | |
| 128 | initVariableNameInjector(); | |
| 129 | } | |
| 130 | ||
| 131 | /** | |
| 132 | * Listen for file editor tab pane to receive an open definition source event. | |
| 133 | */ | |
| 134 | private void initOpenDefinitionListener() { | |
| 135 | getFileEditorPane().onOpenDefinitionFileProperty().addListener( | |
| 136 | (ObservableValue<? extends Path> definitionFile, | |
| 137 | final Path oldPath, final Path newPath) -> { | |
| 138 | final DefinitionSource ds = createDefinitionSource( newPath ); | |
| 139 | associate( ds, getActiveFileEditor() ); | |
| 140 | } ); | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * When tabs are added, hook the various change listeners onto the new tab so | |
| 145 | * that the preview pane refreshes as necessary. | |
| 146 | */ | |
| 147 | private void initTabAddedListener() { | |
| 148 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 149 | ||
| 150 | // Make sure the text processor kicks off when new files are opened. | |
| 151 | final ObservableList<Tab> tabs = editorPane.getTabs(); | |
| 152 | ||
| 153 | // Update the preview pane on tab changes. | |
| 154 | tabs.addListener( | |
| 155 | (final Change<? extends Tab> change) -> { | |
| 156 | while( change.next() ) { | |
| 157 | if( change.wasAdded() ) { | |
| 158 | // Multiple tabs can be added simultaneously. | |
| 159 | for( final Tab newTab : change.getAddedSubList() ) { | |
| 160 | final FileEditorTab tab = (FileEditorTab)newTab; | |
| 161 | ||
| 162 | initTextChangeListener( tab ); | |
| 163 | initCaretParagraphListener( tab ); | |
| 164 | } | |
| 165 | } | |
| 166 | } | |
| 167 | } | |
| 168 | ); | |
| 169 | } | |
| 170 | ||
| 171 | /** | |
| 172 | * Reloads the preferences from the previous load. | |
| 173 | */ | |
| 174 | private void initPreferences() { | |
| 175 | getFileEditorPane().restorePreferences(); | |
| 176 | } | |
| 177 | ||
| 178 | /** | |
| 179 | * Listen for new tab selection events. | |
| 180 | */ | |
| 181 | private void initTabChangeListener() { | |
| 182 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 183 | ||
| 184 | // Update the preview pane changing tabs. | |
| 185 | editorPane.addTabSelectionListener( | |
| 186 | (ObservableValue<? extends Tab> tabPane, | |
| 187 | final Tab oldTab, final Tab newTab) -> { | |
| 188 | ||
| 189 | // If there was no old tab, then this is a first time load, which | |
| 190 | // can be ignored. | |
| 191 | if( oldTab != null ) { | |
| 192 | if( newTab == null ) { | |
| 193 | closeRemainingTab(); | |
| 194 | } else { | |
| 195 | // Synchronize the preview with the edited text. | |
| 196 | refreshSelectedTab( (FileEditorTab)newTab ); | |
| 197 | } | |
| 198 | } | |
| 199 | } | |
| 200 | ); | |
| 201 | } | |
| 202 | ||
| 203 | /** | |
| 204 | * Initialize the variable name editor. | |
| 205 | */ | |
| 206 | private void initVariableNameInjector() { | |
| 207 | setVariableNameInjector( | |
| 208 | new VariableNameInjector( getFileEditorPane(), getDefinitionPane() ) | |
| 209 | ); | |
| 210 | } | |
| 211 | ||
| 212 | private void initTextChangeListener( final FileEditorTab tab ) { | |
| 213 | tab.addTextChangeListener( | |
| 214 | (ObservableValue<? extends String> editor, | |
| 215 | final String oldValue, final String newValue) -> { | |
| 216 | refreshSelectedTab( tab ); | |
| 217 | } | |
| 218 | ); | |
| 219 | } | |
| 220 | ||
| 221 | private void initCaretParagraphListener( final FileEditorTab tab ) { | |
| 222 | tab.addCaretParagraphListener( | |
| 223 | (ObservableValue<? extends Integer> editor, | |
| 224 | final Integer oldValue, final Integer newValue) -> { | |
| 225 | refreshSelectedTab( tab ); | |
| 226 | } | |
| 227 | ); | |
| 228 | } | |
| 229 | ||
| 230 | /** | |
| 231 | * Called whenever the preview pane becomes out of sync with the file editor | |
| 232 | * tab. This can be called when the text changes, the caret paragraph changes, | |
| 233 | * or the file tab changes. | |
| 234 | * | |
| 235 | * @param tab The file editor tab that has been changed in some fashion. | |
| 236 | */ | |
| 237 | private void refreshSelectedTab( final FileEditorTab tab ) { | |
| 238 | final HTMLPreviewPane preview = getPreviewPane(); | |
| 239 | preview.setPath( tab.getPath() ); | |
| 240 | ||
| 241 | final Processor<String> hpp = new HTMLPreviewProcessor( preview ); | |
| 242 | final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp ); | |
| 243 | final Processor<String> mp = new MarkdownProcessor( mcrp ); | |
| 244 | final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, tab.getCaretPosition() ); | |
| 245 | final Processor<String> vp = new VariableProcessor( mcip, getResolvedMap() ); | |
| 246 | ||
| 247 | vp.processChain( tab.getEditorText() ); | |
| 248 | } | |
| 249 | ||
| 250 | /** | |
| 251 | * TODO: Patch into loading of definition source. | |
| 252 | * | |
| 253 | * @return | |
| 254 | */ | |
| 255 | private Map<String, String> getResolvedMap() { | |
| 256 | return new HashMap<>(); | |
| 257 | } | |
| 258 | ||
| 259 | /** | |
| 260 | * TODO: Patch into loading of definition source. | |
| 261 | * | |
| 262 | * @return | |
| 263 | */ | |
| 264 | private TreeView<String> getTreeView() { | |
| 265 | return new TreeView<>(); | |
| 266 | } | |
| 267 | ||
| 268 | /** | |
| 269 | * Called when the tab has changed to a new editor to replace the current | |
| 270 | * definition pane with the | |
| 271 | * | |
| 272 | * @param tab Reference to the tab that has the file being edited. | |
| 273 | */ | |
| 274 | private void updateDefinitionPane( final FileEditorTab tab ) { | |
| 275 | // Look up the path to the variable definition file associated with the | |
| 276 | // given tab. | |
| 277 | final Path path = getVariableDefinitionPath( tab.getPath() ); | |
| 278 | final DefinitionSource ds = createDefinitionSource( path ); | |
| 279 | ||
| 280 | associate( ds, tab ); | |
| 281 | } | |
| 282 | ||
| 283 | private void associate( final DefinitionSource ds, final FileEditorTab tab ) { | |
| 284 | System.out.println( "Associate " + ds + " with " + tab ); | |
| 285 | } | |
| 286 | ||
| 287 | /** | |
| 288 | * Searches the persistent settings for the variable definition file that is | |
| 289 | * associated with the given path. | |
| 290 | * | |
| 291 | * @param tabPath The path that may be associated with some variables. | |
| 292 | * | |
| 293 | * @return A path to the variable definition file for the given document path. | |
| 294 | */ | |
| 295 | private Path getVariableDefinitionPath( final Path tabPath ) { | |
| 296 | return new File( "/tmp/variables.yaml" ).toPath(); | |
| 297 | } | |
| 298 | ||
| 299 | /** | |
| 300 | * Creates a boolean property that is bound to another boolean value of the | |
| 301 | * active editor. | |
| 302 | */ | |
| 303 | private BooleanProperty createActiveBooleanProperty( | |
| 304 | final Function<FileEditorTab, ObservableBooleanValue> func ) { | |
| 305 | ||
| 306 | final BooleanProperty b = new SimpleBooleanProperty(); | |
| 307 | final FileEditorTab tab = getActiveFileEditor(); | |
| 308 | ||
| 309 | if( tab != null ) { | |
| 310 | b.bind( func.apply( tab ) ); | |
| 311 | } | |
| 312 | ||
| 313 | getFileEditorPane().activeFileEditorProperty().addListener( | |
| 314 | (observable, oldFileEditor, newFileEditor) -> { | |
| 315 | b.unbind(); | |
| 316 | ||
| 317 | if( newFileEditor != null ) { | |
| 318 | b.bind( func.apply( newFileEditor ) ); | |
| 319 | } else { | |
| 320 | b.set( false ); | |
| 321 | } | |
| 322 | } | |
| 323 | ); | |
| 324 | ||
| 325 | return b; | |
| 326 | } | |
| 327 | ||
| 328 | /** | |
| 329 | * Called when the last open tab is closed. This clears out the preview pane | |
| 330 | * and the definition pane. | |
| 331 | */ | |
| 332 | private void closeRemainingTab() { | |
| 333 | getPreviewPane().clear(); | |
| 334 | getDefinitionPane().clear(); | |
| 335 | } | |
| 336 | ||
| 337 | //---- File actions ------------------------------------------------------- | |
| 338 | private void fileNew() { | |
| 339 | getFileEditorPane().newEditor(); | |
| 340 | } | |
| 341 | ||
| 342 | private void fileOpen() { | |
| 343 | getFileEditorPane().openFileDialog(); | |
| 344 | } | |
| 345 | ||
| 346 | private void fileClose() { | |
| 347 | getFileEditorPane().closeEditor( getActiveFileEditor(), true ); | |
| 348 | } | |
| 349 | ||
| 350 | private void fileCloseAll() { | |
| 351 | getFileEditorPane().closeAllEditors(); | |
| 352 | } | |
| 353 | ||
| 354 | private void fileSave() { | |
| 355 | getFileEditorPane().saveEditor( getActiveFileEditor() ); | |
| 356 | } | |
| 357 | ||
| 358 | private void fileSaveAll() { | |
| 359 | getFileEditorPane().saveAllEditors(); | |
| 360 | } | |
| 361 | ||
| 362 | private void fileExit() { | |
| 363 | final Window window = getWindow(); | |
| 364 | Event.fireEvent( window, | |
| 365 | new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) ); | |
| 366 | } | |
| 367 | ||
| 368 | //---- Help actions ------------------------------------------------------- | |
| 369 | private void helpAbout() { | |
| 370 | Alert alert = new Alert( AlertType.INFORMATION ); | |
| 371 | alert.setTitle( get( "Dialog.about.title" ) ); | |
| 372 | alert.setHeaderText( get( "Dialog.about.header" ) ); | |
| 373 | alert.setContentText( get( "Dialog.about.content" ) ); | |
| 374 | alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) ); | |
| 375 | alert.initOwner( getWindow() ); | |
| 376 | ||
| 377 | alert.showAndWait(); | |
| 378 | } | |
| 379 | ||
| 380 | //---- Convenience accessors ---------------------------------------------- | |
| 381 | private float getFloat( final String key, final float defaultValue ) { | |
| 382 | return getPreferences().getFloat( key, defaultValue ); | |
| 383 | } | |
| 384 | ||
| 385 | private Preferences getPreferences() { | |
| 386 | return getOptions().getState(); | |
| 387 | } | |
| 388 | ||
| 389 | private Window getWindow() { | |
| 390 | return getScene().getWindow(); | |
| 391 | } | |
| 392 | ||
| 393 | private MarkdownEditorPane getActiveEditor() { | |
| 394 | return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane()); | |
| 395 | } | |
| 396 | ||
| 397 | private FileEditorTab getActiveFileEditor() { | |
| 398 | return getFileEditorPane().getActiveFileEditor(); | |
| 399 | } | |
| 400 | ||
| 401 | //---- Member accessors --------------------------------------------------- | |
| 402 | public Scene getScene() { | |
| 403 | return this.scene; | |
| 404 | } | |
| 405 | ||
| 406 | private void setScene( Scene scene ) { | |
| 407 | this.scene = scene; | |
| 408 | } | |
| 409 | ||
| 410 | private FileEditorTabPane getFileEditorPane() { | |
| 411 | if( this.fileEditorPane == null ) { | |
| 412 | this.fileEditorPane = createFileEditorPane(); | |
| 413 | } | |
| 414 | ||
| 415 | return this.fileEditorPane; | |
| 416 | } | |
| 417 | ||
| 418 | private synchronized HTMLPreviewPane getPreviewPane() { | |
| 419 | if( this.previewPane == null ) { | |
| 420 | this.previewPane = createPreviewPane(); | |
| 421 | } | |
| 422 | ||
| 423 | return this.previewPane; | |
| 424 | } | |
| 425 | ||
| 426 | private DefinitionPane getDefinitionPane() { | |
| 427 | if( this.definitionPane == null ) { | |
| 428 | this.definitionPane = createDefinitionPane(); | |
| 429 | } | |
| 430 | ||
| 431 | return this.definitionPane; | |
| 432 | } | |
| 433 | ||
| 434 | public VariableNameInjector getVariableNameInjector() { | |
| 435 | return this.variableNameInjector; | |
| 436 | } | |
| 437 | ||
| 438 | public void setVariableNameInjector( final VariableNameInjector injector ) { | |
| 439 | this.variableNameInjector = injector; | |
| 440 | } | |
| 441 | ||
| 442 | private Options getOptions() { | |
| 443 | return this.options; | |
| 444 | } | |
| 445 | ||
| 446 | public MenuBar getMenuBar() { | |
| 447 | return this.menuBar; | |
| 448 | } | |
| 449 | ||
| 450 | public void setMenuBar( MenuBar menuBar ) { | |
| 451 | this.menuBar = menuBar; | |
| 452 | } | |
| 453 | ||
| 454 | //---- Member creators ---------------------------------------------------- | |
| 455 | private DefinitionSource createDefinitionSource( final Path path ) { | |
| 456 | return createDefinitionFactory().fileDefinitionSource( path ); | |
| 457 | } | |
| 458 | ||
| 459 | /** | |
| 460 | * Create an editor pane to hold file editor tabs. | |
| 461 | * | |
| 462 | * @return A new instance, never null. | |
| 463 | */ | |
| 464 | private FileEditorTabPane createFileEditorPane() { | |
| 465 | return new FileEditorTabPane(); | |
| 466 | } | |
| 467 | ||
| 468 | private HTMLPreviewPane createPreviewPane() { | |
| 469 | return new HTMLPreviewPane(); | |
| 470 | } | |
| 471 | ||
| 472 | protected DefinitionPane createDefinitionPane() { | |
| 473 | return new DefinitionPane( getTreeView() ); | |
| 474 | } | |
| 475 | ||
| 476 | private DefinitionFactory createDefinitionFactory() { | |
| 477 | return new DefinitionFactory(); | |
| 478 | } | |
| 479 | ||
| 480 | private Node createMenuBar() { | |
| 481 | final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull(); | |
| 482 | ||
| 483 | // File actions | |
| 484 | Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() ); | |
| 485 | Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() ); | |
| 486 | Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull ); | |
| 487 | Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull ); | |
| 488 | Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(), | |
| 489 | createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() ); | |
| 490 | Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(), | |
| 491 | Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) ); | |
| 492 | Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() ); | |
| 493 | ||
| 494 | // Edit actions | |
| 495 | Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO, | |
| 496 | e -> getActiveEditor().undo(), | |
| 497 | createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() ); | |
| 498 | Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT, | |
| 499 | e -> getActiveEditor().redo(), | |
| 500 | createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() ); | |
| 501 | ||
| 502 | // Insert actions | |
| 503 | Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD, | |
| 504 | e -> getActiveEditor().surroundSelection( "**", "**" ), | |
| 505 | activeFileEditorIsNull ); | |
| 506 | Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC, | |
| 507 | e -> getActiveEditor().surroundSelection( "*", "*" ), | |
| 508 | activeFileEditorIsNull ); | |
| 509 | Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH, | |
| 510 | e -> getActiveEditor().surroundSelection( "~~", "~~" ), | |
| 511 | activeFileEditorIsNull ); | |
| 512 | Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac | |
| 513 | e -> getActiveEditor().surroundSelection( "\n\n> ", "" ), | |
| 514 | activeFileEditorIsNull ); | |
| 515 | Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE, | |
| 516 | e -> getActiveEditor().surroundSelection( "`", "`" ), | |
| 517 | activeFileEditorIsNull ); | |
| 518 | Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT, | |
| 519 | e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ), | |
| 520 | activeFileEditorIsNull ); | |
| 521 | ||
| 522 | Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK, | |
| 523 | e -> getActiveEditor().insertLink(), | |
| 524 | activeFileEditorIsNull ); | |
| 525 | Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT, | |
| 526 | e -> getActiveEditor().insertImage(), | |
| 527 | activeFileEditorIsNull ); | |
| 528 | ||
| 529 | final Action[] headers = new Action[ 6 ]; | |
| 530 | ||
| 531 | // Insert header actions (H1 ... H6) | |
| 532 | for( int i = 1; i <= 6; i++ ) { | |
| 533 | final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | |
| 534 | final String markup = String.format( "\n\n%s ", hashes ); | |
| 535 | final String text = get( "Main.menu.insert.header_" + i ); | |
| 536 | final String accelerator = "Shortcut+" + i; | |
| 537 | final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" ); | |
| 538 | ||
| 539 | headers[ i - 1 ] = new Action( text, accelerator, HEADER, | |
| 540 | e -> getActiveEditor().surroundSelection( markup, "", prompt ), | |
| 541 | activeFileEditorIsNull ); | |
| 542 | } | |
| 543 | ||
| 544 | Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL, | |
| 545 | e -> getActiveEditor().surroundSelection( "\n\n* ", "" ), | |
| 546 | activeFileEditorIsNull ); | |
| 547 | Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL, | |
| 548 | e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ), | |
| 549 | activeFileEditorIsNull ); | |
| 550 | Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null, | |
| 551 | e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ), | |
| 552 | activeFileEditorIsNull ); | |
| 553 | ||
| 554 | // Help actions | |
| 555 | Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() ); | |
| 556 | ||
| 557 | //---- MenuBar ---- | |
| 558 | Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ), | |
| 559 | fileNewAction, | |
| 560 | fileOpenAction, | |
| 561 | null, | |
| 562 | fileCloseAction, | |
| 563 | fileCloseAllAction, | |
| 564 | null, | |
| 565 | fileSaveAction, | |
| 566 | fileSaveAllAction, | |
| 567 | null, | |
| 568 | fileExitAction ); | |
| 569 | ||
| 570 | Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ), | |
| 571 | editUndoAction, | |
| 572 | editRedoAction ); | |
| 573 | ||
| 574 | Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ), | |
| 575 | insertBoldAction, | |
| 576 | insertItalicAction, | |
| 577 | insertStrikethroughAction, | |
| 578 | insertBlockquoteAction, | |
| 579 | insertCodeAction, | |
| 580 | insertFencedCodeBlockAction, | |
| 581 | null, | |
| 582 | insertLinkAction, | |
| 583 | insertImageAction, | |
| 584 | null, | |
| 585 | headers[ 0 ], | |
| 586 | headers[ 1 ], | |
| 587 | headers[ 2 ], | |
| 588 | headers[ 3 ], | |
| 589 | headers[ 4 ], | |
| 590 | headers[ 5 ], | |
| 591 | null, | |
| 592 | insertUnorderedListAction, | |
| 593 | insertOrderedListAction, | |
| 594 | insertHorizontalRuleAction ); | |
| 595 | ||
| 596 | Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ), | |
| 597 | helpAboutAction ); | |
| 598 | ||
| 599 | menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu ); | |
| 600 | ||
| 601 | //---- ToolBar ---- | |
| 602 | ToolBar toolBar = ActionUtils.createToolBar( | |
| 603 | fileNewAction, | |
| 604 | fileOpenAction, | |
| 605 | fileSaveAction, | |
| 606 | null, | |
| 607 | editUndoAction, | |
| 608 | editRedoAction, | |
| 609 | null, | |
| 610 | insertBoldAction, | |
| 611 | insertItalicAction, | |
| 612 | insertBlockquoteAction, | |
| 613 | insertCodeAction, | |
| 614 | insertFencedCodeBlockAction, | |
| 615 | null, | |
| 616 | insertLinkAction, | |
| 617 | insertImageAction, | |
| 618 | null, | |
| 619 | headers[ 0 ], | |
| 620 | null, | |
| 621 | insertUnorderedListAction, | |
| 622 | insertOrderedListAction ); | |
| 623 | ||
| 624 | return new VBox( menuBar, toolBar ); | |
| 31 | import static com.scrivenvar.Constants.PREFS_DEFINITION_SOURCE; | |
| 32 | import static com.scrivenvar.Constants.STYLESHEET_SCENE; | |
| 33 | import static com.scrivenvar.Messages.get; | |
| 34 | import com.scrivenvar.definition.DefinitionFactory; | |
| 35 | import com.scrivenvar.definition.DefinitionPane; | |
| 36 | import com.scrivenvar.definition.DefinitionSource; | |
| 37 | import com.scrivenvar.definition.EmptyDefinitionSource; | |
| 38 | import com.scrivenvar.editors.VariableNameInjector; | |
| 39 | import com.scrivenvar.editors.markdown.MarkdownEditorPane; | |
| 40 | import com.scrivenvar.preview.HTMLPreviewPane; | |
| 41 | import com.scrivenvar.processors.HTMLPreviewProcessor; | |
| 42 | import com.scrivenvar.processors.MarkdownCaretInsertionProcessor; | |
| 43 | import com.scrivenvar.processors.MarkdownCaretReplacementProcessor; | |
| 44 | import com.scrivenvar.processors.MarkdownProcessor; | |
| 45 | import com.scrivenvar.processors.Processor; | |
| 46 | import com.scrivenvar.processors.VariableProcessor; | |
| 47 | import com.scrivenvar.service.Options; | |
| 48 | import com.scrivenvar.util.Action; | |
| 49 | import com.scrivenvar.util.ActionUtils; | |
| 50 | import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION; | |
| 51 | import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR; | |
| 52 | import static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW; | |
| 53 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD; | |
| 54 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE; | |
| 55 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT; | |
| 56 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT; | |
| 57 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT; | |
| 58 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT; | |
| 59 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER; | |
| 60 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC; | |
| 61 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK; | |
| 62 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL; | |
| 63 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL; | |
| 64 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT; | |
| 65 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT; | |
| 66 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT; | |
| 67 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH; | |
| 68 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO; | |
| 69 | import java.net.MalformedURLException; | |
| 70 | import java.nio.file.Path; | |
| 71 | import java.util.Map; | |
| 72 | import java.util.function.Function; | |
| 73 | import java.util.prefs.Preferences; | |
| 74 | import javafx.beans.binding.Bindings; | |
| 75 | import javafx.beans.binding.BooleanBinding; | |
| 76 | import javafx.beans.property.BooleanProperty; | |
| 77 | import javafx.beans.property.SimpleBooleanProperty; | |
| 78 | import javafx.beans.value.ObservableBooleanValue; | |
| 79 | import javafx.beans.value.ObservableValue; | |
| 80 | import javafx.collections.ListChangeListener.Change; | |
| 81 | import javafx.collections.ObservableList; | |
| 82 | import javafx.event.Event; | |
| 83 | import javafx.scene.Node; | |
| 84 | import javafx.scene.Scene; | |
| 85 | import javafx.scene.control.Alert; | |
| 86 | import javafx.scene.control.Alert.AlertType; | |
| 87 | import javafx.scene.control.Menu; | |
| 88 | import javafx.scene.control.MenuBar; | |
| 89 | import javafx.scene.control.SplitPane; | |
| 90 | import javafx.scene.control.Tab; | |
| 91 | import javafx.scene.control.ToolBar; | |
| 92 | import javafx.scene.control.TreeView; | |
| 93 | import javafx.scene.image.Image; | |
| 94 | import javafx.scene.image.ImageView; | |
| 95 | import static javafx.scene.input.KeyCode.ESCAPE; | |
| 96 | import javafx.scene.input.KeyEvent; | |
| 97 | import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED; | |
| 98 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 99 | import javafx.scene.layout.BorderPane; | |
| 100 | import javafx.scene.layout.VBox; | |
| 101 | import javafx.stage.Window; | |
| 102 | import javafx.stage.WindowEvent; | |
| 103 | ||
| 104 | /** | |
| 105 | * Main window containing a tab pane in the center for file editors. | |
| 106 | * | |
| 107 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 108 | */ | |
| 109 | public class MainWindow { | |
| 110 | ||
| 111 | private final Options options = Services.load( Options.class ); | |
| 112 | ||
| 113 | private Scene scene; | |
| 114 | private MenuBar menuBar; | |
| 115 | ||
| 116 | private DefinitionPane definitionPane; | |
| 117 | private FileEditorTabPane fileEditorPane; | |
| 118 | private HTMLPreviewPane previewPane; | |
| 119 | ||
| 120 | private VariableNameInjector variableNameInjector; | |
| 121 | private DefinitionSource definitionSource; | |
| 122 | ||
| 123 | public MainWindow() { | |
| 124 | initLayout(); | |
| 125 | initOpenDefinitionListener(); | |
| 126 | initTabAddedListener(); | |
| 127 | initTabChangedListener(); | |
| 128 | initPreferences(); | |
| 129 | initVariableNameInjector(); | |
| 130 | } | |
| 131 | ||
| 132 | /** | |
| 133 | * Listen for file editor tab pane to receive an open definition source event. | |
| 134 | */ | |
| 135 | private void initOpenDefinitionListener() { | |
| 136 | getFileEditorPane().onOpenDefinitionFileProperty().addListener( | |
| 137 | (ObservableValue<? extends Path> definitionFile, | |
| 138 | final Path oldPath, final Path newPath) -> { | |
| 139 | openDefinition( newPath ); | |
| 140 | } ); | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * When tabs are added, hook the various change listeners onto the new tab so | |
| 145 | * that the preview pane refreshes as necessary. | |
| 146 | */ | |
| 147 | private void initTabAddedListener() { | |
| 148 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 149 | ||
| 150 | // Make sure the text processor kicks off when new files are opened. | |
| 151 | final ObservableList<Tab> tabs = editorPane.getTabs(); | |
| 152 | ||
| 153 | // Update the preview pane on tab changes. | |
| 154 | tabs.addListener( | |
| 155 | (final Change<? extends Tab> change) -> { | |
| 156 | while( change.next() ) { | |
| 157 | if( change.wasAdded() ) { | |
| 158 | // Multiple tabs can be added simultaneously. | |
| 159 | for( final Tab newTab : change.getAddedSubList() ) { | |
| 160 | final FileEditorTab tab = (FileEditorTab)newTab; | |
| 161 | ||
| 162 | initTextChangeListener( tab ); | |
| 163 | initCaretParagraphListener( tab ); | |
| 164 | } | |
| 165 | } | |
| 166 | } | |
| 167 | } | |
| 168 | ); | |
| 169 | } | |
| 170 | ||
| 171 | /** | |
| 172 | * Reloads the preferences from the previous load. | |
| 173 | */ | |
| 174 | private void initPreferences() { | |
| 175 | getFileEditorPane().restorePreferences(); | |
| 176 | restoreDefinitionSource(); | |
| 177 | } | |
| 178 | ||
| 179 | /** | |
| 180 | * Listen for new tab selection events. | |
| 181 | */ | |
| 182 | private void initTabChangedListener() { | |
| 183 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 184 | ||
| 185 | // Update the preview pane changing tabs. | |
| 186 | editorPane.addTabSelectionListener( | |
| 187 | (ObservableValue<? extends Tab> tabPane, | |
| 188 | final Tab oldTab, final Tab newTab) -> { | |
| 189 | ||
| 190 | // If there was no old tab, then this is a first time load, which | |
| 191 | // can be ignored. | |
| 192 | if( oldTab != null ) { | |
| 193 | if( newTab == null ) { | |
| 194 | closeRemainingTab(); | |
| 195 | } else { | |
| 196 | // Synchronize the preview with the edited text. | |
| 197 | refreshSelectedTab( (FileEditorTab)newTab ); | |
| 198 | } | |
| 199 | } | |
| 200 | } | |
| 201 | ); | |
| 202 | } | |
| 203 | ||
| 204 | /** | |
| 205 | * Initialize the variable name editor. | |
| 206 | */ | |
| 207 | private void initVariableNameInjector() { | |
| 208 | setVariableNameInjector( | |
| 209 | new VariableNameInjector( getFileEditorPane(), getDefinitionPane() ) | |
| 210 | ); | |
| 211 | } | |
| 212 | ||
| 213 | private void initTextChangeListener( final FileEditorTab tab ) { | |
| 214 | tab.addTextChangeListener( | |
| 215 | (ObservableValue<? extends String> editor, | |
| 216 | final String oldValue, final String newValue) -> { | |
| 217 | refreshSelectedTab( tab ); | |
| 218 | } | |
| 219 | ); | |
| 220 | } | |
| 221 | ||
| 222 | private void initCaretParagraphListener( final FileEditorTab tab ) { | |
| 223 | tab.addCaretParagraphListener( | |
| 224 | (ObservableValue<? extends Integer> editor, | |
| 225 | final Integer oldValue, final Integer newValue) -> { | |
| 226 | refreshSelectedTab( tab ); | |
| 227 | } | |
| 228 | ); | |
| 229 | } | |
| 230 | ||
| 231 | /** | |
| 232 | * Called whenever the preview pane becomes out of sync with the file editor | |
| 233 | * tab. This can be called when the text changes, the caret paragraph changes, | |
| 234 | * or the file tab changes. | |
| 235 | * | |
| 236 | * @param tab The file editor tab that has been changed in some fashion. | |
| 237 | */ | |
| 238 | private void refreshSelectedTab( final FileEditorTab tab ) { | |
| 239 | final HTMLPreviewPane preview = getPreviewPane(); | |
| 240 | preview.setPath( tab.getPath() ); | |
| 241 | ||
| 242 | final Processor<String> hpp = new HTMLPreviewProcessor( preview ); | |
| 243 | final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp ); | |
| 244 | final Processor<String> mp = new MarkdownProcessor( mcrp ); | |
| 245 | final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, tab.getCaretPosition() ); | |
| 246 | final Processor<String> vp = new VariableProcessor( mcip, getResolvedMap() ); | |
| 247 | ||
| 248 | vp.processChain( tab.getEditorText() ); | |
| 249 | } | |
| 250 | ||
| 251 | /** | |
| 252 | * Returns the variable map of interpolated definitions. | |
| 253 | * | |
| 254 | * @return A map to help dereference variables. | |
| 255 | */ | |
| 256 | private Map<String, String> getResolvedMap() { | |
| 257 | return getDefinitionSource().getResolvedMap(); | |
| 258 | } | |
| 259 | ||
| 260 | /** | |
| 261 | * Returns the root node for the hierarchical definition source. | |
| 262 | * | |
| 263 | * @return Data to display in the definition pane. | |
| 264 | */ | |
| 265 | private TreeView<String> getTreeView() { | |
| 266 | try { | |
| 267 | return getDefinitionSource().asTreeView(); | |
| 268 | } catch( Exception e ) { | |
| 269 | alert( e ); | |
| 270 | } | |
| 271 | ||
| 272 | return new TreeView<>(); | |
| 273 | } | |
| 274 | ||
| 275 | private void openDefinition( final Path path ) { | |
| 276 | openDefinition( path.toString() ); | |
| 277 | } | |
| 278 | ||
| 279 | private void openDefinition( final String path ) { | |
| 280 | try { | |
| 281 | final DefinitionSource ds = createDefinitionSource( path ); | |
| 282 | setDefinitionSource( ds ); | |
| 283 | storeDefinitionSource(); | |
| 284 | ||
| 285 | getDefinitionPane().setRoot( ds.asTreeView() ); | |
| 286 | } catch( Exception e ) { | |
| 287 | alert( e ); | |
| 288 | } | |
| 289 | } | |
| 290 | ||
| 291 | private void restoreDefinitionSource() { | |
| 292 | final Preferences preferences = getPreferences(); | |
| 293 | final String source = preferences.get( PREFS_DEFINITION_SOURCE, null ); | |
| 294 | ||
| 295 | if( source != null ) { | |
| 296 | openDefinition( source ); | |
| 297 | } | |
| 298 | } | |
| 299 | ||
| 300 | private void storeDefinitionSource() { | |
| 301 | final Preferences preferences = getPreferences(); | |
| 302 | final DefinitionSource ds = getDefinitionSource(); | |
| 303 | ||
| 304 | preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() ); | |
| 305 | } | |
| 306 | ||
| 307 | /** | |
| 308 | * Called when the last open tab is closed. This clears out the preview pane | |
| 309 | * and the definition pane. | |
| 310 | */ | |
| 311 | private void closeRemainingTab() { | |
| 312 | getPreviewPane().clear(); | |
| 313 | getDefinitionPane().clear(); | |
| 314 | } | |
| 315 | ||
| 316 | /** | |
| 317 | * Called when an exception occurs that warrants the user's attention. | |
| 318 | * | |
| 319 | * @param e The exception with a message that the user should know about. | |
| 320 | */ | |
| 321 | private void alert( final Exception e ) { | |
| 322 | // TODO: Raise a notice. | |
| 323 | } | |
| 324 | ||
| 325 | //---- File actions ------------------------------------------------------- | |
| 326 | private void fileNew() { | |
| 327 | getFileEditorPane().newEditor(); | |
| 328 | } | |
| 329 | ||
| 330 | private void fileOpen() { | |
| 331 | getFileEditorPane().openFileDialog(); | |
| 332 | } | |
| 333 | ||
| 334 | private void fileClose() { | |
| 335 | getFileEditorPane().closeEditor( getActiveFileEditor(), true ); | |
| 336 | } | |
| 337 | ||
| 338 | private void fileCloseAll() { | |
| 339 | getFileEditorPane().closeAllEditors(); | |
| 340 | } | |
| 341 | ||
| 342 | private void fileSave() { | |
| 343 | getFileEditorPane().saveEditor( getActiveFileEditor() ); | |
| 344 | } | |
| 345 | ||
| 346 | private void fileSaveAll() { | |
| 347 | getFileEditorPane().saveAllEditors(); | |
| 348 | } | |
| 349 | ||
| 350 | private void fileExit() { | |
| 351 | final Window window = getWindow(); | |
| 352 | Event.fireEvent( window, | |
| 353 | new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) ); | |
| 354 | } | |
| 355 | ||
| 356 | //---- Help actions ------------------------------------------------------- | |
| 357 | private void helpAbout() { | |
| 358 | Alert alert = new Alert( AlertType.INFORMATION ); | |
| 359 | alert.setTitle( get( "Dialog.about.title" ) ); | |
| 360 | alert.setHeaderText( get( "Dialog.about.header" ) ); | |
| 361 | alert.setContentText( get( "Dialog.about.content" ) ); | |
| 362 | alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) ); | |
| 363 | alert.initOwner( getWindow() ); | |
| 364 | ||
| 365 | alert.showAndWait(); | |
| 366 | } | |
| 367 | ||
| 368 | //---- Convenience accessors ---------------------------------------------- | |
| 369 | private float getFloat( final String key, final float defaultValue ) { | |
| 370 | return getPreferences().getFloat( key, defaultValue ); | |
| 371 | } | |
| 372 | ||
| 373 | private Preferences getPreferences() { | |
| 374 | return getOptions().getState(); | |
| 375 | } | |
| 376 | ||
| 377 | private Window getWindow() { | |
| 378 | return getScene().getWindow(); | |
| 379 | } | |
| 380 | ||
| 381 | private MarkdownEditorPane getActiveEditor() { | |
| 382 | return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane()); | |
| 383 | } | |
| 384 | ||
| 385 | private FileEditorTab getActiveFileEditor() { | |
| 386 | return getFileEditorPane().getActiveFileEditor(); | |
| 387 | } | |
| 388 | ||
| 389 | //---- Member accessors --------------------------------------------------- | |
| 390 | public Scene getScene() { | |
| 391 | return this.scene; | |
| 392 | } | |
| 393 | ||
| 394 | private void setScene( Scene scene ) { | |
| 395 | this.scene = scene; | |
| 396 | } | |
| 397 | ||
| 398 | private FileEditorTabPane getFileEditorPane() { | |
| 399 | if( this.fileEditorPane == null ) { | |
| 400 | this.fileEditorPane = createFileEditorPane(); | |
| 401 | } | |
| 402 | ||
| 403 | return this.fileEditorPane; | |
| 404 | } | |
| 405 | ||
| 406 | private synchronized HTMLPreviewPane getPreviewPane() { | |
| 407 | if( this.previewPane == null ) { | |
| 408 | this.previewPane = createPreviewPane(); | |
| 409 | } | |
| 410 | ||
| 411 | return this.previewPane; | |
| 412 | } | |
| 413 | ||
| 414 | private void setDefinitionSource( final DefinitionSource definitionSource ) { | |
| 415 | this.definitionSource = definitionSource; | |
| 416 | } | |
| 417 | ||
| 418 | private synchronized DefinitionSource getDefinitionSource() { | |
| 419 | if( this.definitionSource == null ) { | |
| 420 | this.definitionSource = new EmptyDefinitionSource(); | |
| 421 | } | |
| 422 | ||
| 423 | return this.definitionSource; | |
| 424 | } | |
| 425 | ||
| 426 | private DefinitionPane getDefinitionPane() { | |
| 427 | if( this.definitionPane == null ) { | |
| 428 | this.definitionPane = createDefinitionPane(); | |
| 429 | } | |
| 430 | ||
| 431 | return this.definitionPane; | |
| 432 | } | |
| 433 | ||
| 434 | public VariableNameInjector getVariableNameInjector() { | |
| 435 | return this.variableNameInjector; | |
| 436 | } | |
| 437 | ||
| 438 | public void setVariableNameInjector( final VariableNameInjector injector ) { | |
| 439 | this.variableNameInjector = injector; | |
| 440 | } | |
| 441 | ||
| 442 | private Options getOptions() { | |
| 443 | return this.options; | |
| 444 | } | |
| 445 | ||
| 446 | public MenuBar getMenuBar() { | |
| 447 | return this.menuBar; | |
| 448 | } | |
| 449 | ||
| 450 | public void setMenuBar( MenuBar menuBar ) { | |
| 451 | this.menuBar = menuBar; | |
| 452 | } | |
| 453 | ||
| 454 | //---- Member creators ---------------------------------------------------- | |
| 455 | private DefinitionSource createDefinitionSource( final String path ) | |
| 456 | throws MalformedURLException { | |
| 457 | return createDefinitionFactory().createDefinitionSource( path ); | |
| 458 | } | |
| 459 | ||
| 460 | /** | |
| 461 | * Create an editor pane to hold file editor tabs. | |
| 462 | * | |
| 463 | * @return A new instance, never null. | |
| 464 | */ | |
| 465 | private FileEditorTabPane createFileEditorPane() { | |
| 466 | return new FileEditorTabPane(); | |
| 467 | } | |
| 468 | ||
| 469 | private HTMLPreviewPane createPreviewPane() { | |
| 470 | return new HTMLPreviewPane(); | |
| 471 | } | |
| 472 | ||
| 473 | private DefinitionPane createDefinitionPane() { | |
| 474 | return new DefinitionPane( getTreeView() ); | |
| 475 | } | |
| 476 | ||
| 477 | private DefinitionFactory createDefinitionFactory() { | |
| 478 | return new DefinitionFactory(); | |
| 479 | } | |
| 480 | ||
| 481 | private Node createMenuBar() { | |
| 482 | final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull(); | |
| 483 | ||
| 484 | // File actions | |
| 485 | Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() ); | |
| 486 | Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() ); | |
| 487 | Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull ); | |
| 488 | Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull ); | |
| 489 | Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(), | |
| 490 | createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() ); | |
| 491 | Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(), | |
| 492 | Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) ); | |
| 493 | Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() ); | |
| 494 | ||
| 495 | // Edit actions | |
| 496 | Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO, | |
| 497 | e -> getActiveEditor().undo(), | |
| 498 | createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() ); | |
| 499 | Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT, | |
| 500 | e -> getActiveEditor().redo(), | |
| 501 | createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() ); | |
| 502 | ||
| 503 | // Insert actions | |
| 504 | Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD, | |
| 505 | e -> getActiveEditor().surroundSelection( "**", "**" ), | |
| 506 | activeFileEditorIsNull ); | |
| 507 | Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC, | |
| 508 | e -> getActiveEditor().surroundSelection( "*", "*" ), | |
| 509 | activeFileEditorIsNull ); | |
| 510 | Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH, | |
| 511 | e -> getActiveEditor().surroundSelection( "~~", "~~" ), | |
| 512 | activeFileEditorIsNull ); | |
| 513 | Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac | |
| 514 | e -> getActiveEditor().surroundSelection( "\n\n> ", "" ), | |
| 515 | activeFileEditorIsNull ); | |
| 516 | Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE, | |
| 517 | e -> getActiveEditor().surroundSelection( "`", "`" ), | |
| 518 | activeFileEditorIsNull ); | |
| 519 | Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT, | |
| 520 | e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ), | |
| 521 | activeFileEditorIsNull ); | |
| 522 | ||
| 523 | Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK, | |
| 524 | e -> getActiveEditor().insertLink(), | |
| 525 | activeFileEditorIsNull ); | |
| 526 | Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT, | |
| 527 | e -> getActiveEditor().insertImage(), | |
| 528 | activeFileEditorIsNull ); | |
| 529 | ||
| 530 | final Action[] headers = new Action[ 6 ]; | |
| 531 | ||
| 532 | // Insert header actions (H1 ... H6) | |
| 533 | for( int i = 1; i <= 6; i++ ) { | |
| 534 | final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | |
| 535 | final String markup = String.format( "\n\n%s ", hashes ); | |
| 536 | final String text = get( "Main.menu.insert.header_" + i ); | |
| 537 | final String accelerator = "Shortcut+" + i; | |
| 538 | final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" ); | |
| 539 | ||
| 540 | headers[ i - 1 ] = new Action( text, accelerator, HEADER, | |
| 541 | e -> getActiveEditor().surroundSelection( markup, "", prompt ), | |
| 542 | activeFileEditorIsNull ); | |
| 543 | } | |
| 544 | ||
| 545 | Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL, | |
| 546 | e -> getActiveEditor().surroundSelection( "\n\n* ", "" ), | |
| 547 | activeFileEditorIsNull ); | |
| 548 | Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL, | |
| 549 | e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ), | |
| 550 | activeFileEditorIsNull ); | |
| 551 | Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null, | |
| 552 | e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ), | |
| 553 | activeFileEditorIsNull ); | |
| 554 | ||
| 555 | // Help actions | |
| 556 | Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() ); | |
| 557 | ||
| 558 | //---- MenuBar ---- | |
| 559 | Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ), | |
| 560 | fileNewAction, | |
| 561 | fileOpenAction, | |
| 562 | null, | |
| 563 | fileCloseAction, | |
| 564 | fileCloseAllAction, | |
| 565 | null, | |
| 566 | fileSaveAction, | |
| 567 | fileSaveAllAction, | |
| 568 | null, | |
| 569 | fileExitAction ); | |
| 570 | ||
| 571 | Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ), | |
| 572 | editUndoAction, | |
| 573 | editRedoAction ); | |
| 574 | ||
| 575 | Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ), | |
| 576 | insertBoldAction, | |
| 577 | insertItalicAction, | |
| 578 | insertStrikethroughAction, | |
| 579 | insertBlockquoteAction, | |
| 580 | insertCodeAction, | |
| 581 | insertFencedCodeBlockAction, | |
| 582 | null, | |
| 583 | insertLinkAction, | |
| 584 | insertImageAction, | |
| 585 | null, | |
| 586 | headers[ 0 ], | |
| 587 | headers[ 1 ], | |
| 588 | headers[ 2 ], | |
| 589 | headers[ 3 ], | |
| 590 | headers[ 4 ], | |
| 591 | headers[ 5 ], | |
| 592 | null, | |
| 593 | insertUnorderedListAction, | |
| 594 | insertOrderedListAction, | |
| 595 | insertHorizontalRuleAction ); | |
| 596 | ||
| 597 | Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ), | |
| 598 | helpAboutAction ); | |
| 599 | ||
| 600 | menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu ); | |
| 601 | ||
| 602 | //---- ToolBar ---- | |
| 603 | ToolBar toolBar = ActionUtils.createToolBar( | |
| 604 | fileNewAction, | |
| 605 | fileOpenAction, | |
| 606 | fileSaveAction, | |
| 607 | null, | |
| 608 | editUndoAction, | |
| 609 | editRedoAction, | |
| 610 | null, | |
| 611 | insertBoldAction, | |
| 612 | insertItalicAction, | |
| 613 | insertBlockquoteAction, | |
| 614 | insertCodeAction, | |
| 615 | insertFencedCodeBlockAction, | |
| 616 | null, | |
| 617 | insertLinkAction, | |
| 618 | insertImageAction, | |
| 619 | null, | |
| 620 | headers[ 0 ], | |
| 621 | null, | |
| 622 | insertUnorderedListAction, | |
| 623 | insertOrderedListAction ); | |
| 624 | ||
| 625 | return new VBox( menuBar, toolBar ); | |
| 626 | } | |
| 627 | ||
| 628 | /** | |
| 629 | * Creates a boolean property that is bound to another boolean value of the | |
| 630 | * active editor. | |
| 631 | */ | |
| 632 | private BooleanProperty createActiveBooleanProperty( | |
| 633 | final Function<FileEditorTab, ObservableBooleanValue> func ) { | |
| 634 | ||
| 635 | final BooleanProperty b = new SimpleBooleanProperty(); | |
| 636 | final FileEditorTab tab = getActiveFileEditor(); | |
| 637 | ||
| 638 | if( tab != null ) { | |
| 639 | b.bind( func.apply( tab ) ); | |
| 640 | } | |
| 641 | ||
| 642 | getFileEditorPane().activeFileEditorProperty().addListener( | |
| 643 | (observable, oldFileEditor, newFileEditor) -> { | |
| 644 | b.unbind(); | |
| 645 | ||
| 646 | if( newFileEditor != null ) { | |
| 647 | b.bind( func.apply( newFileEditor ) ); | |
| 648 | } else { | |
| 649 | b.set( false ); | |
| 650 | } | |
| 651 | } | |
| 652 | ); | |
| 653 | ||
| 654 | return b; | |
| 625 | 655 | } |
| 626 | 656 |
| 32 | 32 | import com.scrivenvar.predicates.files.FileTypePredicate; |
| 33 | 33 | import com.scrivenvar.service.Settings; |
| 34 | import java.io.File; | |
| 35 | import java.net.MalformedURLException; | |
| 36 | import java.net.URI; | |
| 37 | import java.net.URISyntaxException; | |
| 38 | import java.net.URL; | |
| 34 | 39 | import java.nio.file.Path; |
| 40 | import java.nio.file.Paths; | |
| 35 | 41 | import java.util.Iterator; |
| 36 | 42 | import java.util.List; |
| ... | ||
| 71 | 77 | final Iterator<String> keys = properties.getKeys( EXTENSIONS_PREFIX ); |
| 72 | 78 | |
| 73 | DefinitionSource definitions = null; | |
| 79 | DefinitionSource result = new EmptyDefinitionSource(); | |
| 74 | 80 | |
| 75 | 81 | while( keys.hasNext() ) { |
| 76 | 82 | final String key = keys.next(); |
| 77 | 83 | final List<String> patterns = properties.getStringSettingList( key ); |
| 78 | 84 | final FileTypePredicate predicate = new FileTypePredicate( patterns ); |
| 79 | 85 | |
| 80 | 86 | if( predicate.test( path.toFile() ) ) { |
| 81 | 87 | final String filetype = key.replace( EXTENSIONS_PREFIX + ".", "" ); |
| 82 | 88 | |
| 83 | definitions = createFileDefinitionSource( filetype, path ); | |
| 89 | result = createFileDefinitionSource( filetype, path ); | |
| 84 | 90 | } |
| 85 | 91 | } |
| 86 | 92 | |
| 87 | return definitions; | |
| 93 | return result; | |
| 94 | } | |
| 95 | ||
| 96 | public DefinitionSource createDefinitionSource( final String path ) { | |
| 97 | ||
| 98 | final String protocol = getProtocol( path ); | |
| 99 | DefinitionSource result = new EmptyDefinitionSource(); | |
| 100 | ||
| 101 | switch( protocol ) { | |
| 102 | case "file": | |
| 103 | result = fileDefinitionSource( Paths.get( path ) ); | |
| 104 | break; | |
| 105 | ||
| 106 | default: | |
| 107 | unknownDefinitionSource( protocol, path ); | |
| 108 | break; | |
| 109 | } | |
| 110 | ||
| 111 | return result; | |
| 88 | 112 | } |
| 89 | 113 | |
| ... | ||
| 98 | 122 | private DefinitionSource createFileDefinitionSource( |
| 99 | 123 | final String filetype, final Path path ) { |
| 100 | final DefinitionSource result; | |
| 124 | DefinitionSource result = new EmptyDefinitionSource(); | |
| 101 | 125 | |
| 102 | 126 | switch( filetype ) { |
| 103 | 127 | case "yaml": |
| 104 | 128 | result = new YamlFileDefinitionSource( path ); |
| 105 | 129 | break; |
| 106 | 130 | |
| 107 | 131 | default: |
| 108 | result = new EmptyDefinitionSource(); | |
| 132 | unknownDefinitionSource( filetype, path.toString() ); | |
| 109 | 133 | break; |
| 110 | 134 | } |
| 111 | 135 | |
| 112 | 136 | return result; |
| 137 | } | |
| 138 | ||
| 139 | /** | |
| 140 | * Throws IllegalArgumentException because the given path could not be | |
| 141 | * recognized. | |
| 142 | * | |
| 143 | * @param type The detected path type (protocol, file extension, etc.). | |
| 144 | * @param path The path to a source of definitions. | |
| 145 | */ | |
| 146 | private void unknownDefinitionSource( final String type, final String path ) { | |
| 147 | throw new IllegalArgumentException( | |
| 148 | "Unknown type '" + type + "' for " + path + "." | |
| 149 | ); | |
| 113 | 150 | } |
| 114 | 151 | |
| 115 | 152 | private Settings getSettings() { |
| 116 | 153 | return this.settings; |
| 154 | } | |
| 155 | ||
| 156 | /** | |
| 157 | * Returns the protocol for a given URI or filename. | |
| 158 | * | |
| 159 | * @param source Determine the protocol for this URI or filename. | |
| 160 | * | |
| 161 | * @return The protocol for the given source. | |
| 162 | */ | |
| 163 | private String getProtocol( final String source ) { | |
| 164 | String protocol = null; | |
| 165 | ||
| 166 | try { | |
| 167 | final URI uri = new URI( source ); | |
| 168 | ||
| 169 | if( uri.isAbsolute() ) { | |
| 170 | protocol = uri.getScheme(); | |
| 171 | } else { | |
| 172 | final URL url = new URL( source ); | |
| 173 | protocol = url.getProtocol(); | |
| 174 | } | |
| 175 | } catch( final URISyntaxException | MalformedURLException e ) { | |
| 176 | // Could be HTTP, HTTPS? | |
| 177 | if( source.startsWith( "//" ) ) { | |
| 178 | throw new IllegalArgumentException( "Relative context: " + source ); | |
| 179 | } else { | |
| 180 | final File file = new File( source ); | |
| 181 | protocol = getProtocol( file ); | |
| 182 | } | |
| 183 | } | |
| 184 | ||
| 185 | return protocol; | |
| 186 | } | |
| 187 | ||
| 188 | /** | |
| 189 | * Returns the protocol for a given file. | |
| 190 | * | |
| 191 | * @param file Determine the protocol for this file. | |
| 192 | * | |
| 193 | * @return The protocol for the given file. | |
| 194 | */ | |
| 195 | private String getProtocol( final File file ) { | |
| 196 | String result; | |
| 197 | ||
| 198 | try { | |
| 199 | result = file.toURI().toURL().getProtocol(); | |
| 200 | } catch( Exception e ) { | |
| 201 | result = "unknown"; | |
| 202 | } | |
| 203 | ||
| 204 | return result; | |
| 117 | 205 | } |
| 118 | 206 | } |
| 67 | 67 | } |
| 68 | 68 | |
| 69 | /** | |
| 70 | * Changes the root node of the tree view. Swaps the current root node for the | |
| 71 | * root node of the given | |
| 72 | * | |
| 73 | * @param treeView The treeview containing a new root node; if the parameter | |
| 74 | * is null, the tree is cleared. | |
| 75 | */ | |
| 76 | public void setRoot( final TreeView<String> treeView ) { | |
| 77 | getTreeView().setRoot( treeView == null ? null : treeView.getRoot() ); | |
| 78 | } | |
| 79 | ||
| 80 | /** | |
| 81 | * Clears the treeview by setting the root node to null. | |
| 82 | */ | |
| 69 | 83 | public void clear() { |
| 70 | getTreeView().setRoot( null ); | |
| 84 | setRoot( null ); | |
| 71 | 85 | } |
| 72 | 86 | |
| ... | ||
| 321 | 335 | * @param treeView |
| 322 | 336 | */ |
| 323 | private void setTreeView( TreeView<String> treeView ) { | |
| 337 | private void setTreeView( final TreeView<String> treeView ) { | |
| 324 | 338 | if( treeView != null ) { |
| 325 | 339 | this.treeView = treeView; |
| 49 | 49 | */ |
| 50 | 50 | public TreeView<String> asTreeView() throws IOException; |
| 51 | ||
| 51 | ||
| 52 | 52 | /** |
| 53 | 53 | * Returns all the strings with their values resolved in a flat hierarchy. |
| 54 | 54 | * This copies all the keys and resolved values into a new map. |
| 55 | 55 | * |
| 56 | 56 | * @return The new map created with all values having been resolved, |
| 57 | 57 | * recursively. |
| 58 | 58 | */ |
| 59 | 59 | public Map<String, String> getResolvedMap(); |
| 60 | ||
| 61 | /** | |
| 62 | * Must return a re-loadable path to the data source. For a file, this is the | |
| 63 | * absolute file path. For a database, this could be the JDBC connection. For | |
| 64 | * a web site, this might be the GET url. | |
| 65 | * | |
| 66 | * @return A non-null, non-empty string. | |
| 67 | */ | |
| 68 | @Override | |
| 69 | public String toString(); | |
| 60 | 70 | } |
| 61 | 71 |
| 168 | 168 | final JsonNode rootNode, final String path, final Map<String, String> map ) { |
| 169 | 169 | |
| 170 | rootNode.fields().forEachRemaining( | |
| 171 | (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map ) | |
| 172 | ); | |
| 170 | if( rootNode != null ) { | |
| 171 | rootNode.fields().forEachRemaining( | |
| 172 | (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map ) | |
| 173 | ); | |
| 174 | } | |
| 173 | 175 | } |
| 174 | 176 |
| 63 | 63 | import static org.fxmisc.wellbehaved.event.InputMap.consume; |
| 64 | 64 | import static org.fxmisc.wellbehaved.event.InputMap.sequence; |
| 65 | import static com.scrivenvar.util.Lists.getFirst; | |
| 66 | import static com.scrivenvar.util.Lists.getLast; | |
| 67 | import static java.lang.Character.isSpaceChar; | |
| 68 | import static java.lang.Character.isWhitespace; | |
| 69 | import static java.lang.Math.min; | |
| 70 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 71 | import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped; | |
| 72 | import static org.fxmisc.wellbehaved.event.InputMap.consume; | |
| 73 | 65 | |
| 74 | 66 | /** |
| 28 | 28 | package com.scrivenvar.editors.markdown; |
| 29 | 29 | |
| 30 | import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN; | |
| 30 | 31 | import com.scrivenvar.dialogs.ImageDialog; |
| 31 | 32 | import com.scrivenvar.dialogs.LinkDialog; |
| ... | ||
| 46 | 47 | import javafx.stage.Window; |
| 47 | 48 | import org.fxmisc.richtext.StyleClassedTextArea; |
| 48 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 49 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 50 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 51 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 52 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 53 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 54 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 55 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 56 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 57 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 58 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 59 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 60 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 61 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 62 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 63 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 64 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 65 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 66 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 67 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 68 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 69 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 70 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 71 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 72 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 73 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 74 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 75 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 76 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 77 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 78 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 79 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 80 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 81 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 82 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 83 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 84 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 85 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 86 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 87 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 88 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 89 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 90 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 91 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 92 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 93 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 94 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 95 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 96 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 97 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 98 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 99 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 100 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 101 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 102 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 103 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 104 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 105 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 106 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 107 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 108 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 109 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 110 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 111 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 112 | import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN; | |
| 113 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 114 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 115 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 116 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 117 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 118 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 119 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 120 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 121 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 122 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 123 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 124 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 125 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 126 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 127 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 128 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 129 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 130 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 131 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 132 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 133 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 134 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 135 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 136 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 137 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 138 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 139 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 140 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 141 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 142 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 143 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 144 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 145 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 146 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 147 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 148 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 149 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 150 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 151 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 152 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 153 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 154 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 155 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 156 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 157 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 158 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 159 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 160 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 161 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 162 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 163 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 164 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 165 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 166 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 167 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 168 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 169 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 170 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 171 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 172 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 173 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 174 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 175 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 176 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 177 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 178 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 179 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 180 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 181 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 182 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 183 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 184 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 185 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 186 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 187 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 188 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 189 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 190 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 191 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 192 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 193 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 194 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 195 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 196 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 197 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 198 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 199 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 200 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 201 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 202 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 203 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 204 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 205 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 206 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 207 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 208 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 209 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 210 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 211 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 212 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 213 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 214 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 215 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 216 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 217 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 218 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 219 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 220 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 221 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 222 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 223 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 224 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 225 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 226 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 227 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 228 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 229 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 230 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 231 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 232 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 233 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 234 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 235 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 236 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 237 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 238 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 239 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 240 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 241 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 242 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 243 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 244 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 245 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 246 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 247 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 248 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 249 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 250 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 251 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 252 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 253 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 254 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 255 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 256 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 257 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 258 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 259 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 260 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 261 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 262 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 263 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 264 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 265 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 266 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 267 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 268 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 269 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 270 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 271 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 272 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 273 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 274 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 275 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 276 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 277 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 278 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 279 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 280 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 281 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 282 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 283 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 284 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 285 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 286 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 287 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 288 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 289 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 290 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 291 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 292 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 293 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 294 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 295 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 296 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 297 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 298 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 299 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 300 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 301 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 302 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 303 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 304 | 49 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; |
| 305 | 50 | |
| 30 | 30 | import static com.scrivenvar.Constants.CARET_POSITION_MD; |
| 31 | 31 | import static java.lang.Character.isLetter; |
| 32 | import static java.lang.Math.min; | |
| 32 | 33 | |
| 33 | 34 | /** |
| ... | ||
| 65 | 66 | @Override |
| 66 | 67 | public String processLink( final String t ) { |
| 67 | int offset = getCaretPosition(); | |
| 68 | 68 | final int length = t.length(); |
| 69 | int offset = min( getCaretPosition(), length ); | |
| 70 | ||
| 71 | // TODO: Ensure that the caret position is outside of an element, | |
| 72 | // so that a caret inserted in the image doesn't corrupt it. Such as: | |
| 73 | // | |
| 74 | //  | |
| 75 | // | |
| 76 | // 1. Scan back to the previous EOL, which will be the MD AST start point. | |
| 77 | // 2. Scan forward until EOF or EOL, which will be the MD AST ending point. | |
| 78 | // 3. Convert the text between start and end into MD AST. | |
| 79 | // 4. Find the nearest text node to the caret. | |
| 80 | // 5. Insert the CARET_POSITION_MD value in the text at that offsset. | |
| 69 | 81 | |
| 70 | 82 | // Insert the caret at the closest non-Markdown delimiter (i.e., the |
| 71 | 83 | // closest character from the caret position forward). |
| 72 | 84 | while( offset < length && !isLetter( t.charAt( offset ) ) ) { |
| 73 | 85 | offset++; |
| 74 | 86 | } |
| 75 | 87 | |
| 76 | // TODO: Ensure that the caret position is outside of an element, | |
| 77 | // so that a caret inserted in the image doesn't corrupt it. Such as: | |
| 78 | // | |
| 79 | //  | |
| 80 | 88 | // Insert the caret position into the Markdown text, but don't interfere |
| 81 | 89 | // with the Markdown iteself. |
| 29 | 29 | |
| 30 | 30 | import java.util.Map; |
| 31 | import static org.apache.commons.lang.StringUtils.replaceEach; | |
| 31 | import static org.apache.commons.lang3.StringUtils.replaceEach; | |
| 32 | 32 | |
| 33 | 33 | /** |
| 61 | 61 | |
| 62 | 62 | /** |
| 63 | * Returns a setting property or its default value. | |
| 64 | * | |
| 65 | * @param property The property key name to obtain its value. | |
| 66 | * @param defaults The default values to return iff the property cannot be | |
| 67 | * found. | |
| 68 | * | |
| 69 | * @return The property values for the given property key. | |
| 70 | */ | |
| 71 | public List<Object> getSettingList( String property, List<String> defaults ); | |
| 72 | ||
| 73 | /** | |
| 74 | 63 | * Returns a list of property names that begin with the given prefix. The |
| 75 | 64 | * prefix is included in any matching results. This will return keys that |
| 28 | 28 | |
| 29 | 29 | import static com.scrivenvar.Constants.PREFS_ROOT; |
| 30 | import static com.scrivenvar.Constants.PREFS_ROOT_OPTIONS; | |
| 31 | import static com.scrivenvar.Constants.PREFS_ROOT_STATE; | |
| 32 | 30 | import com.scrivenvar.service.Options; |
| 33 | 31 | import java.util.prefs.Preferences; |
| 34 | 32 | import static java.util.prefs.Preferences.userRoot; |
| 33 | import static com.scrivenvar.Constants.PREFS_STATE; | |
| 34 | import static com.scrivenvar.Constants.PREFS_OPTIONS; | |
| 35 | 35 | |
| 36 | 36 | /** |
| ... | ||
| 43 | 43 | |
| 44 | 44 | public DefaultOptions() { |
| 45 | setPreferences( getRootPreferences().node( PREFS_ROOT_OPTIONS ) ); | |
| 45 | setPreferences(getRootPreferences().node(PREFS_OPTIONS ) ); | |
| 46 | 46 | } |
| 47 | 47 | |
| ... | ||
| 66 | 66 | @Override |
| 67 | 67 | public Preferences getState() { |
| 68 | return getRootPreferences().node( PREFS_ROOT_STATE ); | |
| 68 | return getRootPreferences().node(PREFS_STATE ); | |
| 69 | 69 | } |
| 70 | 70 | |
| 31 | 31 | import com.scrivenvar.service.Settings; |
| 32 | 32 | import java.io.IOException; |
| 33 | import java.io.InputStreamReader; | |
| 34 | import java.io.Reader; | |
| 33 | 35 | import java.net.URISyntaxException; |
| 34 | 36 | import java.net.URL; |
| 35 | import java.util.ArrayList; | |
| 36 | 37 | import java.util.Iterator; |
| 37 | 38 | import java.util.List; |
| 38 | import java.util.Objects; | |
| 39 | import java.util.stream.Collectors; | |
| 40 | import org.apache.commons.configuration.ConfigurationException; | |
| 41 | import org.apache.commons.configuration.PropertiesConfiguration; | |
| 39 | import org.apache.commons.configuration2.PropertiesConfiguration; | |
| 40 | import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler; | |
| 41 | import org.apache.commons.configuration2.convert.ListDelimiterHandler; | |
| 42 | import org.apache.commons.configuration2.ex.ConfigurationException; | |
| 42 | 43 | |
| 43 | 44 | /** |
| 44 | 45 | * Responsible for loading settings that help avoid hard-coded assumptions. |
| 45 | 46 | * |
| 46 | 47 | * @author White Magic Software, Ltd. |
| 47 | 48 | */ |
| 48 | 49 | public class DefaultSettings implements Settings { |
| 50 | private static final char VALUE_SEPARATOR = ','; | |
| 49 | 51 | |
| 50 | 52 | private PropertiesConfiguration properties; |
| ... | ||
| 79 | 81 | public int getSetting( final String property, final int defaultValue ) { |
| 80 | 82 | return getSettings().getInt( property, defaultValue ); |
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Returns a list of objects for a given setting. | |
| 85 | * | |
| 86 | * @param property The setting key name. | |
| 87 | * @param defaults The default values to return, which may be null. | |
| 88 | * | |
| 89 | * @return A list, possibly empty, never null. | |
| 90 | */ | |
| 91 | @Override | |
| 92 | public List<Object> getSettingList( final String property, List<String> defaults ) { | |
| 93 | if( defaults == null ) { | |
| 94 | defaults = new ArrayList<>(); | |
| 95 | } | |
| 96 | ||
| 97 | return getSettings().getList( property, defaults ); | |
| 98 | 83 | } |
| 99 | 84 | |
| ... | ||
| 109 | 94 | public List<String> getStringSettingList( |
| 110 | 95 | final String property, final List<String> defaults ) { |
| 111 | final List<Object> settings = getSettingList( property, defaults ); | |
| 112 | ||
| 113 | return settings.stream() | |
| 114 | .map( object -> Objects.toString( object, null ) ) | |
| 115 | .collect( Collectors.toList() ); | |
| 96 | return getSettings().getList( String.class, property, defaults ); | |
| 116 | 97 | } |
| 117 | 98 | |
| ... | ||
| 142 | 123 | private PropertiesConfiguration createProperties() |
| 143 | 124 | throws ConfigurationException { |
| 125 | ||
| 144 | 126 | final URL url = getPropertySource(); |
| 127 | final PropertiesConfiguration configuration = new PropertiesConfiguration(); | |
| 145 | 128 | |
| 146 | return url == null | |
| 147 | ? new PropertiesConfiguration() | |
| 148 | : new PropertiesConfiguration( url ); | |
| 129 | if( url != null ) { | |
| 130 | try( final Reader r = new InputStreamReader( url.openStream() ) ) { | |
| 131 | configuration.setListDelimiterHandler( createListDelimiterHandler() ); | |
| 132 | configuration.read( r ); | |
| 133 | ||
| 134 | } catch( IOException e ) { | |
| 135 | throw new ConfigurationException( e ); | |
| 136 | } | |
| 137 | } | |
| 138 | ||
| 139 | return configuration; | |
| 140 | } | |
| 141 | ||
| 142 | protected ListDelimiterHandler createListDelimiterHandler() { | |
| 143 | return new DefaultListDelimiterHandler( VALUE_SEPARATOR ); | |
| 149 | 144 | } |
| 150 | 145 | |
| 1 | package com.scrivenvar.test; | |
| 2 | ||
| 3 | import java.io.IOException; | |
| 4 | import java.io.StringReader; | |
| 5 | import java.util.Arrays; | |
| 6 | import org.apache.commons.configuration2.PropertiesConfiguration; | |
| 7 | import org.apache.commons.configuration2.ex.ConfigurationException; | |
| 8 | ||
| 9 | public class TestProperties { | |
| 10 | ||
| 11 | public static void main( final String args[] ) throws ConfigurationException, IOException { | |
| 12 | final String p = "" | |
| 13 | + "file.ext.definition.yaml=*.yml,*.yaml\n" | |
| 14 | + "filter.file.ext.definition=${file.ext.definition.yaml}\n"; | |
| 15 | ||
| 16 | try( final StringReader r = new StringReader( p ) ) { | |
| 17 | ||
| 18 | PropertiesConfiguration config = new PropertiesConfiguration(); | |
| 19 | config.read( r ); | |
| 20 | ||
| 21 | System.out.println( config.getList( "filter.file.ext.definition" ) ); | |
| 22 | System.out.println( config.getString( "filter.file.ext.definition" ) ); | |
| 23 | System.out.println( Arrays.toString( config.getStringArray( "filter.file.ext.definition" ) ) ); | |
| 24 | } | |
| 25 | } | |
| 26 | } | |
| 1 | 27 |
| 46 | 46 | import org.ahocorasick.trie.*; |
| 47 | 47 | import org.ahocorasick.trie.Trie.TrieBuilder; |
| 48 | import static org.apache.commons.lang.RandomStringUtils.randomNumeric; | |
| 49 | import org.apache.commons.lang.StringUtils; | |
| 48 | import static org.apache.commons.lang3.RandomStringUtils.randomNumeric; | |
| 49 | import org.apache.commons.lang3.StringUtils; | |
| 50 | 50 | |
| 51 | 51 | /** |
| 18 | 18 | preferences.root.state=state |
| 19 | 19 | preferences.root.options=options |
| 20 | preferences.root.definition.source=definition.source | |
| 20 | 21 | |
| 21 | 22 | # ######################################################################## |