Automate switching between languages

This commit is contained in:
Oystein Kristoffer Tveit 2021-02-23 00:46:43 +01:00
parent 1860d4c57f
commit 4e24363e63
14 changed files with 418 additions and 74 deletions

View File

@ -9,41 +9,70 @@ import javafx.scene.Scene;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.stage.Stage; import javafx.stage.Stage;
import app.events.LanguageChangedEvent;
import app.model.Model;
public class Main extends Application { public class Main extends Application {
private Stage window;
private Scene scene;
private FXMLLoader fxmlLoader;
private Parent fxmlRoot;
private static final String TITLE = "Banana Editor";
private static final String ICON_PATH = "/graphics/logo.png";
/** /**
* Boilerplate function to launch the application * Boilerplate function to launch the application.
*/ */
public static void main(String[] args) { public static void main(String[] args) {
launch(args); launch(args);
} }
/** /**
* The entrypoint of the application * Set up a window with title and icon.
*/
private void setupWindow(Stage window) {
this.window = window;
window.setTitle(TITLE);
window.getIcons().add(new Image(getClass().getResourceAsStream(ICON_PATH)));
}
private void loadFXML() throws IOException {
this.fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/Main.fxml"));
this.fxmlRoot = fxmlLoader.load();
}
private void createScene() {
this.scene = new Scene(fxmlRoot);
Model.setScene(scene);
}
private void applyCSS() {
scene.getStylesheets().add(
getClass().getResource("/styling/app.css").toExternalForm());
}
/**
* Set up default values and settings for the editor.
*/
private void setupDefaults() {
MainController mainController = fxmlLoader.getController();
mainController.getEventBus().post(new LanguageChangedEvent("Java"));
}
/**
* The entrypoint of the application.
* @param window The primary window of the application * @param window The primary window of the application
*/ */
@Override @Override
public void start(Stage window) throws IOException { public void start(Stage window) throws IOException {
window.setTitle("Banana Editor");
window.getIcons().add(new Image(getClass().getResourceAsStream("/graphics/logo.png"))); setupWindow(window);
loadFXML();
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/Main.fxml")); createScene();
Parent document; applyCSS();
setupDefaults();
try {
document = fxmlLoader.load();
} catch (IOException e) {
System.err.println(e);
throw new RuntimeException(
"Couldn\'t find or missing permissions to load "
+ getClass().getResource("/fxml/Main.fxml")
);
}
Scene scene = new Scene(document);
scene.getStylesheets().add(getClass().getResource("/styling/app.css").toExternalForm());
window.setScene(scene); window.setScene(scene);
window.show(); window.show();

View File

@ -5,8 +5,10 @@ import java.util.List;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import app.controllers.*; import app.controllers.*;
import app.events.LanguageChangedEvent;
import app.model.Model; import app.model.Model;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -26,11 +28,11 @@ public class MainController implements Initializable {
private MenubarController menubarController; private MenubarController menubarController;
private EventBus eventBus; private EventBus eventBus;
private Model model;
@Override @Override
public void initialize(URL url, ResourceBundle resourceBundle) { public void initialize(URL url, ResourceBundle resourceBundle) {
this.eventBus = new EventBus(); this.eventBus = new EventBus();
this.eventBus.register(this);
List.of( List.of(
editorController, editorController,
@ -38,18 +40,35 @@ public class MainController implements Initializable {
modelineController, modelineController,
menubarController menubarController
).forEach(c -> c.setEventBus(this.eventBus)); ).forEach(c -> c.setEventBus(this.eventBus));
} }
/**
* Get the global eventbus.
* @return The eventbus
*/
public EventBus getEventBus() { public EventBus getEventBus() {
return this.eventBus; return this.eventBus;
} }
/** /**
* Links the controller to the global model * Change the CSS according to which language is being used.
* @param model The model to be linked * @param event The event containing data about the language to switch to
*/ */
public void setModel(Model model) { @Subscribe
this.model = model; private void handle(LanguageChangedEvent event) {
boolean containsSeveralStylesheets = Model.getScene().getStylesheets().size() != 1;
if (containsSeveralStylesheets)
Model.getScene().getStylesheets().remove(1);
String nextStyleSheet =
getClass()
.getResource("/styling/language/" + event.getLanguage().toLowerCase() + ".css")
.toExternalForm();
Model.getScene().getStylesheets().add(nextStyleSheet);
} }
} }

View File

@ -5,6 +5,7 @@ import java.util.Collection;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import org.fxmisc.richtext.CodeArea; import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory; import org.fxmisc.richtext.LineNumberFactory;
@ -13,6 +14,7 @@ import org.fxmisc.richtext.model.TwoDimensional.Bias;
import org.fxmisc.richtext.model.TwoDimensional.Position; import org.fxmisc.richtext.model.TwoDimensional.Position;
import app.events.EditorChangedEvent; import app.events.EditorChangedEvent;
import app.events.LanguageChangedEvent;
import app.model.Model; import app.model.Model;
import app.service.LanguageOperations; import app.service.LanguageOperations;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -39,20 +41,26 @@ public class EditorController implements Initializable, Controller {
} }
/** /**
* Links the controller to the global model * Applies highlighting to the editor.
* @param model The model to be linked * @param highlighting highlighting data
*/ */
public void setModel(Model model) { private void setHighlighting(StyleSpans<Collection<String>> highlighting) {
this.model = model;
}
public void setHighlighting(StyleSpans<Collection<String>> highlighting) {
this.editor.setStyleSpans(0, highlighting); this.editor.setStyleSpans(0, highlighting);
} }
/** /**
* Handles events from the editor, and reflects them in the model * Recalculates and refreshes the syntax highlighting of the editor.
* @param event The object containing metadata of the event */
private void refreshHighlighting() {
this.setHighlighting(
LanguageOperations.syntaxHighlight(
this.editor.getText(),
Model.getLanguage()));
}
/**
* Handles the event whenever the content of the editor is changed.
* @param event JavaFX event containing metadata
*/ */
@FXML @FXML
public void editorChanged(KeyEvent event) { public void editorChanged(KeyEvent event) {
@ -64,10 +72,16 @@ public class EditorController implements Initializable, Controller {
pos.getMajor() + 1, pos.getMajor() + 1,
pos.getMinor() pos.getMinor()
)); ));
this.setHighlighting( this.refreshHighlighting();
LanguageOperations.syntaxHighlight( }
this.editor.getText(),
Model.getLanguage())); /**
* Refreshes the editor whenever the language is changed.
* @param event Which language was switched to
*/
@Subscribe
private void handle(LanguageChangedEvent event) {
this.refreshHighlighting();
} }
} }

View File

@ -5,11 +5,16 @@ import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import app.events.LanguageChangedEvent;
import app.model.Model; import app.model.Model;
import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.MenuBar; import javafx.scene.control.MenuBar;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.ToggleGroup;
import javafx.stage.DirectoryChooser; import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
@ -17,11 +22,13 @@ import javafx.stage.Stage;
public class MenubarController implements Initializable, Controller { public class MenubarController implements Initializable, Controller {
private EventBus eventBus; private EventBus eventBus;
private Model model;
@FXML @FXML
private MenuBar menubar; private MenuBar menubar;
@FXML
private ToggleGroup languageToggleGroup;
@FXML @FXML
public String handleOpenFile() { public String handleOpenFile() {
FileChooser fc = new FileChooser(); FileChooser fc = new FileChooser();
@ -76,12 +83,27 @@ public class MenubarController implements Initializable, Controller {
} }
/** /**
* Links the controller to the global model * Handles the event where the language was change from the menu.
*
* @param model The model to be linked
*/ */
public void setModel(Model model) { @FXML
this.model = model; private void handleLanguageChange(ActionEvent event) {
this.eventBus.post(
new LanguageChangedEvent(
((RadioMenuItem) event.getSource()).getText()));
} }
/**
* Updates menubuttons whenever the language is changed
*/
@Subscribe
private void handle(LanguageChangedEvent event) {
this.languageToggleGroup
.getToggles()
.stream()
.map(RadioMenuItem.class::cast)
.filter(t -> t.getId().equals("toggle" + event.getLanguage()))
.findFirst()
.orElseThrow()
.setSelected(true);
}
} }

View File

@ -7,6 +7,7 @@ import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import app.events.EditorChangedEvent; import app.events.EditorChangedEvent;
import app.events.LanguageChangedEvent;
import app.model.Model; import app.model.Model;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -21,7 +22,6 @@ public class ModelineController implements Initializable, Controller {
private Label language; private Label language;
private EventBus eventBus; private EventBus eventBus;
private Model model;
@Override @Override
public void initialize(URL url, ResourceBundle resourceBundle) { public void initialize(URL url, ResourceBundle resourceBundle) {
@ -34,14 +34,6 @@ public class ModelineController implements Initializable, Controller {
this.eventBus.register(this); this.eventBus.register(this);
} }
/**
* Links the controller to the global model
* @param model The model to be linked
*/
public void setModel(Model model) {
this.model = model;
}
/** /**
* Update the colum row counter * Update the colum row counter
* @param column The column number * @param column The column number
@ -51,9 +43,23 @@ public class ModelineController implements Initializable, Controller {
this.columnrow.setText(String.format("[%d:%d]", row, column)); this.columnrow.setText(String.format("[%d:%d]", row, column));
} }
/**
* Updates the column-row number display whenever the editor cursor
* changes position.
* @param event
*/
@Subscribe @Subscribe
public void handle(EditorChangedEvent event) { private void handle(EditorChangedEvent event) {
System.out.println("Key pressed: " + event.getKeyCode());
this.setColumnRow(event.getColumn(), event.getLineNumber()); this.setColumnRow(event.getColumn(), event.getLineNumber());
} }
/**
* Updates the modeline to display a new language
* whenever it is changed.
* @param event
*/
@Subscribe
private void handle(LanguageChangedEvent event) {
this.language.setText(event.getLanguage());
}
} }

View File

@ -2,7 +2,7 @@ package app.events;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
public class EditorChangedEvent implements Event { public class EditorChangedEvent extends Event {
private KeyCode keyCode; private KeyCode keyCode;
private int lineNumber; private int lineNumber;

View File

@ -1,3 +1,3 @@
package app.events; package app.events;
interface Event {} class Event {}

View File

@ -0,0 +1,33 @@
package app.events;
import app.languages.Java;
import app.languages.Markdown;
import app.model.Model;
public class LanguageChangedEvent extends Event {
private String language;
public LanguageChangedEvent(String language) {
this.language = language;
switch (language) {
case "Java":
Model.setLanguage(new Java());
break;
case "Markdown":
Model.setLanguage(new Markdown());
break;
default:
break;
}
}
public String getLanguage() {
return language;
}
}

View File

@ -33,17 +33,20 @@ public class Java implements ProgrammingLanguage {
private static final Map<Pattern, String> pattern = private static final Map<Pattern, String> pattern =
Map.ofEntries( Map.ofEntries(
e("\\b(" + String.join("|", keywords) + ")\\b", "keyword"), // <public>, <static>, .... e("\"([^\"\\\\]|\\\\.)*\"", "string"),
e("this", "this"), // <this> e("\\bthis\\b", "this"),
e("true", "true"), // <true> e("\\btrue\\b", "true"),
e("false", "false"), // <false> e("\\bfalse\\b", "false"),
e("\\(|\\)", "paranthesis"), // <(*)> e("(?<=\\.?)\\w+(?=\\()", "method"),
e("\\{|\\}", "curlyBrackets"), // <{*}> e("\\(|\\)", "paranthesis"),
e("\\[|\\]", "squareBrackets"), // <[*]> e("\\{|\\}", "curlyBrackets"),
e(";", "semiColon"), // <;> e("\\[|\\]", "squareBrackets"),
e("[A-Z]\\w+", "identifier"), // <ClassName> or <InterfaceName> e(";", "semicolon"),
e("\\.\\w+", "property"), // <.property> e("\\.\\w+\\b(?!\\()", "property"),
e("(?<=this\\.)?\\w+(?=\\(*\\))", "method") // <this.function(*)> e("[A-Z]\\w+", "identifier"),
e("\\b(" + String.join("|", keywords) + ")\\b",
"keyword"),
e("(?://.*)|(?:\\/?\\s?\\*.*)", "comment")
); );
public String getName() { public String getName() {
@ -58,8 +61,6 @@ public class Java implements ProgrammingLanguage {
return Java.pattern; return Java.pattern;
} }
// https://stackoverflow.com/questions/16517689/java-regex-capturing-groups-indexes/16582083
public Pattern getPattern() { public Pattern getPattern() {
return Pattern.compile( return Pattern.compile(
Java.pattern Java.pattern

View File

@ -0,0 +1,60 @@
package app.languages;
import java.net.URL;
import java.util.AbstractMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import app.model.ProgrammingLanguage;
public class Markdown implements ProgrammingLanguage {
private String name = "Markdown";
private URL iconPath = this.getClass().getResource("");
private static Entry<Pattern, String> e(String k, String v) {
return new AbstractMap.SimpleEntry<>(Pattern.compile(k), v);
}
private static final Map<Pattern, String> pattern =
Map.ofEntries(
e("##### .*", "ssssheader"),
e("#### .*", "sssheader"),
e("### .*", "ssheader"),
e("## .*", "sheader"),
e("# .*", "header"),
e("\\*\\*\\*.*\\*\\*\\*|___.*___", "emphasizedItalic"),
e("\\*\\*(?!\\*).*\\*\\*|__(?!_).*__", "emphasized"),
e("\\*(?!\\*).*\\*|_(?!_).*_", "italic"),
e("~~.*~~", "strikethrough"),
e("[\\-*+] .*", "listItem"),
e("\\d+\\. .*", "numberedItem"),
e("(?<!\\!)\\[.*\\][\\[\\()].*[\\)\\]]", "link"),
e("!\\[.*\\][\\[\\()].*[\\)\\]]", "image"),
e("\\[\\d+\\]: .*", "source")
);
public String getName() {
return this.name;
}
public URL getIcon() {
return this.iconPath;
}
public Map<Pattern, String> getPatternMap() {
return Markdown.pattern;
}
public Pattern getPattern() {
return Pattern.compile(
Markdown.pattern
.entrySet()
.stream()
.map(e -> String.format("(?<%s>%s)", e.getValue(), e.getKey()))
.collect(Collectors.joining("|")));
}
}

View File

@ -1,12 +1,18 @@
package app.model; package app.model;
import javafx.scene.Scene;
/** /**
* Data model of the application * Data model of the application.
*
* Contains a static reference to state that has to be accessed
* by multiple pieces in the application, including the primary scene.
*/ */
public class Model { public class Model {
private static String activeFilePath; private static String activeFilePath;
private static String currentProjectPath; private static String currentProjectPath;
private static ProgrammingLanguage currentProgrammingLanguage; private static ProgrammingLanguage currentProgrammingLanguage;
private static Scene scene;
public static String getActiveFilePath() { public static String getActiveFilePath() {
return activeFilePath; return activeFilePath;
@ -20,7 +26,15 @@ public class Model {
return currentProgrammingLanguage; return currentProgrammingLanguage;
} }
public static Scene getScene() {
return scene;
}
public static void setLanguage(ProgrammingLanguage language) { public static void setLanguage(ProgrammingLanguage language) {
Model.currentProgrammingLanguage = language; Model.currentProgrammingLanguage = language;
} }
public static void setScene(Scene scene) {
Model.scene = scene;
}
} }

View File

@ -5,6 +5,8 @@
<?import javafx.scene.control.MenuItem?> <?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.Menu?> <?import javafx.scene.control.Menu?>
<?import javafx.scene.control.SeparatorMenuItem?> <?import javafx.scene.control.SeparatorMenuItem?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.control.RadioMenuItem?>
<MenuBar <MenuBar
fx:id="menubar" fx:id="menubar"
@ -24,10 +26,22 @@
<MenuItem mnemonicParsing="false" text="Save" accelerator="Shortcut+s"/> <MenuItem mnemonicParsing="false" text="Save" accelerator="Shortcut+s"/>
<MenuItem mnemonicParsing="false" text="Save as" accelerator="Shortcut+S"/> <MenuItem mnemonicParsing="false" text="Save as" accelerator="Shortcut+S"/>
<SeparatorMenuItem/> <SeparatorMenuItem/>
<fx:define>
<ToggleGroup fx:id="languageToggleGroup"/>
</fx:define>
<Menu mnemonicParsing="false" text="Change programming language"> <Menu mnemonicParsing="false" text="Change programming language">
<items> <items>
<MenuItem mnemonicParsing="false" text="Java"/> <!-- TODO: Generate buttons based on classes -->
<MenuItem mnemonicParsing="false" text="Markdown"/> <RadioMenuItem text="Java"
fx:id="toggleJava"
onAction="#handleLanguageChange"
toggleGroup="$languageToggleGroup"/>
<RadioMenuItem text="Markdown"
fx:id="toggleMarkdown"
onAction="#handleLanguageChange"
toggleGroup="$languageToggleGroup"/>
</items> </items>
</Menu> </Menu>
<SeparatorMenuItem/> <SeparatorMenuItem/>

View File

@ -0,0 +1,61 @@
.code-area .this {
-fx-fill: #FFFF00;
-fx-font-weight: bold;
}
.code-area .true {
-fx-fill: #00FF00;
-fx-font-weight: bold;
}
.code-area .false {
-fx-fill: #FF0000;
-fx-font-weight: bold;
}
.code-area .paranthesis {
-fx-fill: -main-fg-color;
}
.code-area .curlyBrackets {
-fx-fill: -main-fg-color;
}
.code-area .squareBrackets {
-fx-fill: -main-fg-color;
}
.code-area .semicolon {
-fx-fill: -main-fg-color;
}
.code-area .identifier {
-fx-fill: -main-blue-color;
-fx-font-weight: bold;
}
.code-area .property {
-fx-fill: -main-fg-color;
}
.code-area .method {
-fx-fill: -main-green-color;
}
.code-area .keyword {
-fx-fill: -main-red-color;
-fx-font-weight: bold;
}
.code-area .comment {
-fx-fill: -light-bg-color;
}
.code-area .string {
-fx-fill: -main-yellow-color;
}
.code-area .escapeChar {
-fx-fill: -main-magenta-color;
}

View File

@ -0,0 +1,71 @@
.code-area {
-test-color: #193814
}
.code-area .header {
-fx-fill: -main-green-color;
-fx-font-size: 200%;
-fx-font-weight: bold;
}
.code-area .sheader {
-fx-fill: -main-blue-color;
-fx-font-size: 150%;
-fx-font-weight: bold;
}
.code-area .ssheader {
-fx-fill: -main-red-color;
-fx-font-size: 130%;
}
.code-area .sssheader {
-fx-fill: -main-magenta-color;
-fx-font-size: 120%;
}
.code-area .ssssheader {
-fx-fill: -main-cyan-color;
-fx-font-size: 110%;
}
.code-area .emphasized {
-fx-fill: -main-blue-color;
-fx-font-weight: bold;
}
.code-area .italic {
-fx-fill: -main-cyan-color;
-fx-font-style: italic;
}
.code-area .emphasizedItalic {
-fx-fill: -main-magenta-color;
-fx-font-weight: bold;
-fx-font-style: italic;
}
.code-area .strikethrough {
-fx-fill: -main-red-color;
-fx-strikethrough: true;
}
.code-area .listItem {
-fx-fill: -main-yellow-color;
}
.code-area .numberedItem {
-fx-fill: -main-yellow-color;
}
.code-area .link {
-fx-fill: -main-red-color;
}
.code-area .image {
-fx-fill: -main-magenta-color;
}
.code-area .source {
-fx-fill: -main-magenta-color;
}