diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..fb50116 --- /dev/null +++ b/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/README.md b/README.md index e38ab71..1c00b77 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # lab_browser Duke CompSci 308 Lab : A simple GUI example: a web browser + +Mario Oliver, Collin Duffy +mao26, cpd20 \ No newline at end of file diff --git a/src/BrowserException.java b/src/BrowserException.java new file mode 100644 index 0000000..1a3867c --- /dev/null +++ b/src/BrowserException.java @@ -0,0 +1,28 @@ + +public class BrowserException extends RuntimeException { + + public BrowserException() { + // TODO Auto-generated constructor stub + } + + public BrowserException(String message) { + super(message); + // TODO Auto-generated constructor stub + } + + public BrowserException(Throwable cause) { + super(cause); + // TODO Auto-generated constructor stub + } + + public BrowserException(String message, Throwable cause) { + super(message, cause); + // TODO Auto-generated constructor stub + } + + public BrowserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + // TODO Auto-generated constructor stub + } + +} diff --git a/src/BrowserModel.java b/src/BrowserModel.java index 9263bb3..bd55363 100755 --- a/src/BrowserModel.java +++ b/src/BrowserModel.java @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.ResourceBundle; /** @@ -15,12 +16,14 @@ public class BrowserModel { // constants public static final String PROTOCOL_PREFIX = "http://"; + public static final String DEFAULT_RESOURCE_PACKAGE = "resources/"; // state private URL myHome; private URL myCurrentURL; private int myCurrentIndex; private List myHistory; private Map myFavorites; + private ResourceBundle myResources; /** @@ -32,8 +35,9 @@ public BrowserModel () { myCurrentIndex = -1; myHistory = new ArrayList<>(); myFavorites = new HashMap<>(); + myResources = ResourceBundle.getBundle(DEFAULT_RESOURCE_PACKAGE + "Exception"); } - + /** * Returns the first page in next history, null if next history is empty. */ @@ -42,7 +46,7 @@ public URL next () { myCurrentIndex++; return myHistory.get(myCurrentIndex); } - return null; + throw new BrowserException(); } /** @@ -53,13 +57,13 @@ public URL back () { myCurrentIndex--; return myHistory.get(myCurrentIndex); } - return null; + throw new BrowserException(); } /** * Changes current page to given URL, removing next history. */ - public URL go (String url) { + public URL go (String url) throws BrowserException{ try { URL tmp = completeURL(url); // unfortunately, completeURL may not have returned a valid URL, so test it @@ -76,7 +80,7 @@ public URL go (String url) { return myCurrentURL; } catch (Exception e) { - return null; + throw new BrowserException(String.format(myResources.getString("ErrorOnGo"), url)); } } @@ -121,6 +125,10 @@ public void addFavorite (String name) { } } + public ArrayList getFavNames(){ + return new ArrayList( myFavorites.keySet()); + } + /** * Returns URL from favorites associated with given name, null if none set. */ @@ -128,7 +136,7 @@ public URL getFavorite (String name) { if (name != null && !name.equals("") && myFavorites.containsKey(name)) { return myFavorites.get(name); } - return null; + throw new BrowserException(); } // deal with a potentially incomplete URL @@ -150,5 +158,8 @@ private URL completeURL (String possible) { } } } + catch (Exception e) { + throw new BrowserException(String.format(myResources.getString("ErrorOnGo"))); + } } } diff --git a/src/BrowserView.java b/src/BrowserView.java index f9d591d..9343a1f 100644 --- a/src/BrowserView.java +++ b/src/BrowserView.java @@ -1,9 +1,12 @@ +import java.awt.Color; import java.awt.Dimension; import java.net.URL; import java.util.Optional; import java.util.ResourceBundle; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.concurrent.Worker; import javafx.concurrent.Worker.State; import javafx.event.ActionEvent; @@ -14,7 +17,10 @@ import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; import javafx.scene.control.TextField; import javafx.scene.control.TextInputDialog; import javafx.scene.image.Image; @@ -22,7 +28,10 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; +import javafx.scene.shape.Rectangle; import javafx.scene.web.WebView; +import javafx.util.Callback; + import javax.imageio.ImageIO; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -30,12 +39,13 @@ import org.w3c.dom.events.EventListener; import org.w3c.dom.events.EventTarget; +import javafx.geometry.*; /** * A class used to display the viewer for a simple HTML browser. * * See this tutorial for help on how to use the variety of components: - * http://download.oracle.com/otndocs/products/javafx/2/samples/Ensemble/ + * http://download.oracle.com/otndocs/products/javafx/2/samples/Ensemble/ * * @author Owen Astrachan * @author Marcin Dobosz @@ -44,263 +54,289 @@ * @author Robert C. Duvall */ public class BrowserView { - // constants - public static final Dimension DEFAULT_SIZE = new Dimension(800, 600); - public static final String DEFAULT_RESOURCE_PACKAGE = "resources/"; - public static final String STYLESHEET = "default.css"; - public static final String BLANK = " "; + // constants + public static final Dimension DEFAULT_SIZE = new Dimension(800, 600); + public static final String DEFAULT_RESOURCE_PACKAGE = "resources/"; + public static final String STYLESHEET = "default.css"; + public static final String BLANK = " "; - // scene, needed to report back to Application - private Scene myScene; - // web page - private WebView myPage; - // information area - private Label myStatus; - // navigation - private TextField myURLDisplay; - private Button myBackButton; - private Button myNextButton; - private Button myHomeButton; - // favorites - private ComboBox myFavorites; - // get strings from resource file - private ResourceBundle myResources; - // the data - private BrowserModel myModel; + // scene, needed to report back to Application + private Scene myScene; + // web page + private WebView myPage; + // information area + private Label myStatus; + // navigation + private TextField myURLDisplay; + private Button myBackButton; + private Button myNextButton; + private Button myHomeButton; + private Button myAddFavButton; + private Button myShowFavButton; + // favorites + private ComboBox myFavorites; + // get strings from resource file + private ResourceBundle myResources; + // the data + private BrowserModel myModel; - /** - * Create a view of the given model of a web browser. - */ - public BrowserView (BrowserModel model, String language) { - myModel = model; - // use resources for labels - myResources = ResourceBundle.getBundle(DEFAULT_RESOURCE_PACKAGE + language); - BorderPane root = new BorderPane(); - // must be first since other panels may refer to page - root.setCenter(makePageDisplay()); - root.setTop(makeInputPanel()); - root.setBottom(makeInformationPanel()); - // control the navigation - enableButtons(); - // create scene to hold UI - myScene = new Scene(root, DEFAULT_SIZE.width, DEFAULT_SIZE.height); - //myScene.getStylesheets().add(DEFAULT_RESOURCE_PACKAGE + STYLESHEET); - } + /** + * Create a view of the given model of a web browser. + */ + public BrowserView(BrowserModel model, String language) { + myModel = model; + // use resources for labels + myResources = ResourceBundle.getBundle(DEFAULT_RESOURCE_PACKAGE + language); + BorderPane root = new BorderPane(); + // must be first since other panels may refer to page + root.setCenter(makePageDisplay()); + root.setTop(makeInputPanel()); + root.setBottom(makeInformationPanel()); + // control the navigation + enableButtons(); + // create scene to hold UI + myScene = new Scene(root, DEFAULT_SIZE.width, DEFAULT_SIZE.height); + myScene.getStylesheets().add(DEFAULT_RESOURCE_PACKAGE + STYLESHEET); + } - /** - * Display given URL. - */ - public void showPage (String url) { - URL valid = myModel.go(url); - if (valid != null) { - update(valid); - } - else { - showError("Could not load " + url); - } - } + /** + * Display given URL. + */ + public void showPage(String url) { + try { + URL valid = myModel.go(url); + update(valid); + } catch (BrowserException e) { + showError("Could not load " + url); + } + } - /** - * Returns scene for this view so it can be added to stage. - */ - public Scene getScene () { - return myScene; - } + /** + * Returns scene for this view so it can be added to stage. + */ + public Scene getScene() { + return myScene; + } - /** - * Display given message as information in the GUI. - */ - public void showStatus (String message) { - myStatus.setText(message); - } + /** + * Display given message as information in the GUI. + */ + public void showStatus(String message) { + myStatus.setText(message); + } - /** - * Display given message as an error in the GUI. - */ - public void showError (String message) { - Alert alert = new Alert(AlertType.ERROR); - alert.setTitle(myResources.getString("ErrorTitle")); - alert.setContentText(message); - alert.showAndWait(); - } + /** + * Display given message as an error in the GUI. + */ + public void showError(String message) { + Alert alert = new Alert(AlertType.ERROR); + alert.setTitle(myResources.getString("ErrorTitle")); + alert.setContentText(message); + alert.showAndWait(); + } - // move to the next URL in the history - private void next () { - update(myModel.next()); - } + // move to the next URL in the history + private void next() { + try { + update(myModel.next()); + } catch (BrowserException e) { + showError("Could not load next page"); + } + } - // move to the previous URL in the history - private void back () { - update(myModel.back()); - } + // move to the previous URL in the history + private void back() { + try { + update(myModel.back()); + } catch (BrowserException e) { + showError("Could not load next page"); + } + } - // change current URL to the home page, if set - private void home () { - showPage(myModel.getHome().toString()); - } + // change current URL to the home page, if set + private void home() { + showPage(myModel.getHome().toString()); + } - // change page to favorite choice - private void showFavorite (String favorite) { - showPage(myModel.getFavorite(favorite).toString()); - // reset favorites ComboBox so the same choice can be made again - myFavorites.setValue(null); - } + // change page to favorite choice + private void showFavorite(String favorite) { + showPage(myModel.getFavorite(favorite).toString()); + // reset favorites ComboBox so the same choice can be made again + // myFavorites.setValue(null); + } - // update just the view to display given URL - private void update (URL url) { - String urlText = url.toString(); - myPage.getEngine().load(urlText); - myURLDisplay.setText(urlText); - enableButtons(); - } + // update just the view to display given URL + private void update(URL url) { + String urlText = url.toString(); + myPage.getEngine().load(urlText); + myURLDisplay.setText(urlText); + enableButtons(); + } - // prompt user for name of favorite to add to collection - private void addFavorite () { - TextInputDialog input = new TextInputDialog(""); - input.setTitle(myResources.getString("FavoritePromptTitle")); - input.setContentText(myResources.getString("FavoritePrompt")); - Optional response = input.showAndWait(); - // did user make a choice? - if (response.isPresent()) { - myModel.addFavorite(response.get()); - myFavorites.getItems().add(response.get()); - } - } + private void comboBox(ComboBox combo) { + String currFav = ""; + ObservableList myList = FXCollections.observableArrayList(myModel.getFavNames()); + combo.getItems().addAll(myList); + if(!combo.getValue().equals(null)) + showFavorite(combo.getValue().toString()); + // did user make a choice? + /* + * if (response.isPresent()) { myModel.getFavorite(response.get()); + * myFavorites.getItems().add(response.get()); } + */ + } - // only enable buttons when useful to user - private void enableButtons () { - myBackButton.setDisable(! myModel.hasPrevious()); - myNextButton.setDisable(! myModel.hasNext()); - myHomeButton.setDisable(myModel.getHome() == null); - } + // prompt user for name of favorite to add to collection + private void addFavorite() { + TextInputDialog input = new TextInputDialog(""); + input.setTitle(myResources.getString("FavoritePromptTitle")); + input.setContentText(myResources.getString("FavoritePrompt")); + Optional response = input.showAndWait(); + // did user make a choice? + if (response.isPresent()) { + myModel.addFavorite(response.get()); + myFavorites.getItems().add(response.get()); + } + } - // convenience method to create HTML page display - private Node makePageDisplay () { - myPage = new WebView(); - // catch "browsing" events within web page - myPage.getEngine().getLoadWorker().stateProperty().addListener(new LinkListener()); - return myPage; - } + // only enable buttons when useful to user + private void enableButtons() { + myBackButton.setDisable(!myModel.hasPrevious()); + myNextButton.setDisable(!myModel.hasNext()); + myHomeButton.setDisable(myModel.getHome() == null); + } - // organize user's options for controlling/giving input to model - private Node makeInputPanel () { - VBox result = new VBox(); - result.getChildren().addAll(makeNavigationPanel(), makePreferencesPanel()); - return result; - } + // convenience method to create HTML page display + private Node makePageDisplay() { + myPage = new WebView(); + // catch "browsing" events within web page + myPage.getEngine().getLoadWorker().stateProperty().addListener(new LinkListener()); + return myPage; + } - // make the panel where "would-be" clicked URL is displayed - private Node makeInformationPanel () { - // BLANK must be non-empty or status label will not be displayed in GUI - myStatus = new Label(BLANK); - return myStatus; - } + // organize user's options for controlling/giving input to model + private Node makeInputPanel() { + VBox result = new VBox(); + result.getChildren().addAll(makeNavigationPanel(), makePreferencesPanel()); + return result; + } - // make user-entered URL/text field and back/next buttons - private Node makeNavigationPanel () { - HBox result = new HBox(); - // create buttons, with their associated actions - // old style way to do set up callback (anonymous class) - myBackButton = makeButton("BackCommand", new EventHandler() { - @Override - public void handle (ActionEvent event) { - back(); - } - }); - result.getChildren().add(myBackButton); - // new style way to do set up callback (lambdas) - myNextButton = makeButton("NextCommand", event -> next()); - result.getChildren().add(myNextButton); - myHomeButton = makeButton("HomeCommand", event -> home()); - result.getChildren().add(myHomeButton); - // if user presses button or enter in text field, load/show the URL - EventHandler showHandler = new ShowPage(); - result.getChildren().add(makeButton("GoCommand", showHandler)); - myURLDisplay = makeInputField(40, showHandler); - result.getChildren().add(myURLDisplay); - return result; - } + // make the panel where "would-be" clicked URL is displayed + private Node makeInformationPanel() { + // BLANK must be non-empty or status label will not be displayed in GUI + myStatus = new Label(BLANK); + return myStatus; + } - // make buttons for setting favorites/home URLs - private Node makePreferencesPanel () { - HBox result = new HBox(); - myFavorites = new ComboBox(); - // ADD REST OF CODE HERE - result.getChildren().add(makeButton("SetHomeCommand", event -> { - myModel.setHome(); - enableButtons(); - })); - return result; - } + // make user-entered URL/text field and back/next buttons + private Node makeNavigationPanel() { + HBox result = new HBox(); + // create buttons, with their associated actions + // old style way to do set up callback (anonymous class) + myBackButton = makeButton("BackCommand", new EventHandler() { + @Override + public void handle(ActionEvent event) { + back(); + } + }); + result.getChildren().add(myBackButton); + // new style way to do set up callback (lambdas) + myNextButton = makeButton("NextCommand", event -> next()); + result.getChildren().add(myNextButton); + myHomeButton = makeButton("HomeCommand", event -> home()); + result.getChildren().add(myHomeButton); + myAddFavButton = makeButton("AddFavoriteCommand", event -> addFavorite()); + ComboBox combo = new ComboBox(); + myShowFavButton = makeButton("FavoriteFirstItem", event -> comboBox(combo)); + // if user presses button or enter in text field, load/show the URL + EventHandler showHandler = new ShowPage(); + result.getChildren().add(myAddFavButton); + result.getChildren().add(combo); + result.getChildren().add(myShowFavButton); + result.getChildren().add(makeButton("GoCommand", showHandler)); + myURLDisplay = makeInputField(40, showHandler); + result.getChildren().add(myURLDisplay); + return result; + } - // makes a button using either an image or a label - private Button makeButton (String property, EventHandler handler) { - // represent all supported image suffixes - final String IMAGEFILE_SUFFIXES = - String.format(".*\\.(%s)", String.join("|", ImageIO.getReaderFileSuffixes())); + // make buttons for setting favorites/home URLs + private Node makePreferencesPanel() { + HBox result = new HBox(); + myFavorites = new ComboBox(); + // ADD REST OF CODE HERE + result.getChildren().add(makeButton("SetHomeCommand", event -> { + myModel.setHome(); + enableButtons(); + })); + return result; + } - Button result = new Button(); - String label = myResources.getString(property); - if (label.matches(IMAGEFILE_SUFFIXES)) { - result.setGraphic(new ImageView( - new Image(getClass().getResourceAsStream(DEFAULT_RESOURCE_PACKAGE + label)))); - } else { - result.setText(label); - } - result.setOnAction(handler); - return result; - } + // makes a button using either an image or a label + private Button makeButton(String property, EventHandler handler) { + // represent all supported image suffixes + final String IMAGEFILE_SUFFIXES = String.format(".*\\.(%s)", String.join("|", ImageIO.getReaderFileSuffixes())); - // make text field for input - private TextField makeInputField (int width, EventHandler handler) { - TextField result = new TextField(); - result.setPrefColumnCount(width); - result.setOnAction(handler); - return result; - } + Button result = new Button(); + String label = myResources.getString(property); + if (label.matches(IMAGEFILE_SUFFIXES)) { + result.setGraphic( + new ImageView(new Image(getClass().getResourceAsStream(DEFAULT_RESOURCE_PACKAGE + label)))); + } else { + result.setText(label); + } + result.setOnAction(handler); + return result; + } - // display page - // very old style way create a callback (inner class) - private class ShowPage implements EventHandler { - @Override - public void handle (ActionEvent event) { - showPage(myURLDisplay.getText()); - } - } + // make text field for input + private TextField makeInputField(int width, EventHandler handler) { + TextField result = new TextField(); + result.setPrefColumnCount(width); + result.setOnAction(handler); + return result; + } + // display page + // very old style way create a callback (inner class) + private class ShowPage implements EventHandler { + @Override + public void handle(ActionEvent event) { + showPage(myURLDisplay.getText()); + } + } - // Inner class to deal with link-clicks and mouse-overs Mostly taken from - // http://blogs.kiyut.com/tonny/2013/07/30/javafx-webview-addhyperlinklistener/ - private class LinkListener implements ChangeListener { - public static final String EVENT_CLICK = "click"; - public static final String EVENT_MOUSEOVER = "mouseover"; - public static final String EVENT_MOUSEOUT = "mouseout"; + // Inner class to deal with link-clicks and mouse-overs Mostly taken from + // http://blogs.kiyut.com/tonny/2013/07/30/javafx-webview-addhyperlinklistener/ + private class LinkListener implements ChangeListener { + public static final String EVENT_CLICK = "click"; + public static final String EVENT_MOUSEOVER = "mouseover"; + public static final String EVENT_MOUSEOUT = "mouseout"; - @Override - public void changed (ObservableValue ov, State oldState, State newState) { - if (newState == Worker.State.SUCCEEDED) { - EventListener listener = event -> { - final String href = ((Element)event.getTarget()).getAttribute("href"); - if (href != null) { - String domEventType = event.getType(); - if (domEventType.equals(EVENT_CLICK)) { - showPage(href); - } else if (domEventType.equals(EVENT_MOUSEOVER)) { - showStatus(href); - } else if (domEventType.equals(EVENT_MOUSEOUT)) { - showStatus(BLANK); - } - } - }; - Document doc = myPage.getEngine().getDocument(); - NodeList nodes = doc.getElementsByTagName("a"); - for (int i = 0; i < nodes.getLength(); i++) { - EventTarget node = (EventTarget)nodes.item(i); - node.addEventListener(EVENT_CLICK, listener, false); - node.addEventListener(EVENT_MOUSEOVER, listener, false); - node.addEventListener(EVENT_MOUSEOUT, listener, false); - } - } - } - }; + @Override + public void changed(ObservableValue ov, State oldState, State newState) { + if (newState == Worker.State.SUCCEEDED) { + EventListener listener = event -> { + final String href = ((Element) event.getTarget()).getAttribute("href"); + if (href != null) { + String domEventType = event.getType(); + if (domEventType.equals(EVENT_CLICK)) { + showPage(href); + } else if (domEventType.equals(EVENT_MOUSEOVER)) { + showStatus(href); + } else if (domEventType.equals(EVENT_MOUSEOUT)) { + showStatus(BLANK); + } + } + }; + Document doc = myPage.getEngine().getDocument(); + NodeList nodes = doc.getElementsByTagName("a"); + for (int i = 0; i < nodes.getLength(); i++) { + EventTarget node = (EventTarget) nodes.item(i); + node.addEventListener(EVENT_CLICK, listener, false); + node.addEventListener(EVENT_MOUSEOVER, listener, false); + node.addEventListener(EVENT_MOUSEOUT, listener, false); + } + } + } + }; } diff --git a/src/resources/English.properties b/src/resources/English.properties index 5cb3530..712793d 100755 --- a/src/resources/English.properties +++ b/src/resources/English.properties @@ -8,3 +8,5 @@ ErrorTitle=Browser Error FavoritePromptTitle=Add Favorite FavoriteFirstItem=All Favorites SetHomeCommand=Set Home +ErrorOnGo = Error On Go + diff --git a/src/resources/Exception.properties b/src/resources/Exception.properties new file mode 100644 index 0000000..5cb3530 --- /dev/null +++ b/src/resources/Exception.properties @@ -0,0 +1,10 @@ +BackCommand=Back +NextCommand=Next +HomeCommand=Home +GoCommand=Go +AddFavoriteCommand=Add Favorites +FavoritePrompt=Enter name +ErrorTitle=Browser Error +FavoritePromptTitle=Add Favorite +FavoriteFirstItem=All Favorites +SetHomeCommand=Set Home diff --git a/src/resources/cool.css b/src/resources/cool.css new file mode 100644 index 0000000..88bb5d6 --- /dev/null +++ b/src/resources/cool.css @@ -0,0 +1,40 @@ +.root { + -fx-font-size: 14pt; + -fx-font-family: "Courier New"; + -fx-base: rgb(132, 145, 47); + -fx-background: rgb(225, 228, 203); +} + +.button { + -fx-text-fill: #006464; + -fx-background-color: #DFB951; + -fx-border-radius: 20; + -fx-background-radius: 20; + -fx-padding: 8; +} + +.button:hover { + -fx-background-color: #3a3a3a; +} + +.combo-box-base { + -fx-text-base-color: #006464; + -fx-background-color: #DFB951; + -fx-border-radius: 20; + -fx-background-radius: 20; +} +.combo-box-base:hover { + -fx-background-color: #3a3a3a; +} + +.label { + -fx-font-size: 11pt; + -fx-font-family: "Segoe UI Semibold"; + -fx-text-fill: #006464; + -fx-opacity: 0.6; +} + +.text-field { + -fx-font-size: 14pt; + -fx-font-family: "Segoe UI Semibold"; +} \ No newline at end of file diff --git a/src/resources/default.css b/src/resources/default.css index 0d8badb..34a829f 100644 --- a/src/resources/default.css +++ b/src/resources/default.css @@ -1,24 +1,24 @@ .root { -fx-font-size: 14pt; - -fx-font-family: "Courier New"; - -fx-base: rgb(132, 145, 47); - -fx-background: rgb(225, 228, 203); + -fx-font-family: "Helvectica"; + -fx-base: rgb(200, 120, 33); + -fx-background: rgb(325, 129, 56); } .button { - -fx-text-fill: #006464; - -fx-background-color: #DFB951; + -fx-text-fill: #006564; + -fx-background-color: #DF8951; -fx-border-radius: 20; -fx-background-radius: 20; -fx-padding: 8; } .button:hover { - -fx-background-color: #3a3a3a; + -fx-background-color: #3a3b3a; } .combo-box-base { - -fx-text-base-color: #006464; + -fx-text-base-color: #006564; -fx-background-color: #DFB951; -fx-border-radius: 20; -fx-background-radius: 20; @@ -28,13 +28,13 @@ } .label { - -fx-font-size: 11pt; + -fx-font-size: 16pt; -fx-font-family: "Segoe UI Semibold"; -fx-text-fill: #006464; -fx-opacity: 0.6; } .text-field { - -fx-font-size: 14pt; + -fx-font-size: 12pt; -fx-font-family: "Segoe UI Semibold"; }