mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Use javafx panel in study tool window
This commit is contained in:
@@ -32,7 +32,7 @@
|
||||
<extensionPoints>
|
||||
<extensionPoint qualifiedName="Edu.StudyExecutor" beanClass="com.intellij.lang.LanguageExtensionPoint"/>
|
||||
<extensionPoint qualifiedName="Edu.StudyLanguageManager" beanClass="com.intellij.lang.LanguageExtensionPoint"/>
|
||||
<extensionPoint qualifiedName="Edu.studyActionProvider" interface="com.jetbrains.edu.learning.StudyActionProvider"/>
|
||||
<extensionPoint qualifiedName="Edu.studyToolWindowConfigurator" interface="com.jetbrains.edu.learning.StudyToolWindowConfigurator"/>
|
||||
</extensionPoints>
|
||||
|
||||
<actions>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
browser.action.back=Click to go back
|
||||
browser.action.forward=Click to go forward
|
||||
browser.action.open.link=Click to open link in browser
|
||||
@@ -0,0 +1,374 @@
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: #3c3f41;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #3c3f41;
|
||||
font-size: 12pt;
|
||||
font-family: Serif;
|
||||
font-weight: normal;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
color: #dddddd
|
||||
}
|
||||
|
||||
.progress-bar > .track {
|
||||
-fx-text-box-border: #bbbbbb;
|
||||
-fx-control-inner-background: #bbbbbb;
|
||||
}
|
||||
|
||||
p img {
|
||||
background-color: #bbbbbb;
|
||||
}
|
||||
|
||||
textarea {
|
||||
background-color: #3c3f41;
|
||||
color: #dddddd
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 15
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: x-large;
|
||||
font-weight: bold;
|
||||
margin-top: 10;
|
||||
margin-bottom: 10
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
margin-top: 10;
|
||||
margin-bottom: 10
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: medium;
|
||||
font-weight: bold;
|
||||
margin-top: 10;
|
||||
margin-bottom: 10
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: small;
|
||||
font-weight: bold;
|
||||
margin-top: 10;
|
||||
margin-bottom: 10
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: x-small;
|
||||
font-weight: bold;
|
||||
margin-top: 10;
|
||||
margin-bottom: 10
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: xx-small;
|
||||
font-weight: bold;
|
||||
margin-top: 10;
|
||||
margin-bottom: 10
|
||||
}
|
||||
|
||||
li p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
td p {
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
menu li p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
menu li {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
menu {
|
||||
margin-left-ltr: 40;
|
||||
margin-right-rtl: 40;
|
||||
margin-top: 10;
|
||||
margin-bottom: 10
|
||||
}
|
||||
|
||||
dir li p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
dir li {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
dir {
|
||||
margin-left-ltr: 40;
|
||||
margin-right-rtl: 40;
|
||||
margin-top: 10;
|
||||
margin-bottom: 10
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left-ltr: 40;
|
||||
margin-right-rtl: 40;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
dd p {
|
||||
margin-left: 0;
|
||||
margin-rigth: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
dt {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-left: 0;
|
||||
margin-top: 10;
|
||||
margin-bottom: 10
|
||||
}
|
||||
|
||||
ol li {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
ol {
|
||||
margin-top: 10;
|
||||
margin-bottom: 10;
|
||||
margin-left-ltr: 50;
|
||||
margin-right-rtl: 50;
|
||||
list-style-type: decimal
|
||||
}
|
||||
|
||||
ol li p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
ul li {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 10;
|
||||
margin-bottom: 10;
|
||||
margin-left-ltr: 50;
|
||||
margin-right-rtl: 50;
|
||||
list-style-type: disc;
|
||||
-bullet-gap: 10
|
||||
}
|
||||
|
||||
ul li ul li {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
ul li ul {
|
||||
list-style-type: circle;
|
||||
margin-left-ltr: 25;
|
||||
margin-right-rtl: 25;
|
||||
}
|
||||
|
||||
ul li ul li ul li {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
ul li ul li ul {
|
||||
list-style-type: square;
|
||||
margin-left-ltr: 25;
|
||||
margin-right-rtl: 25
|
||||
}
|
||||
|
||||
ul li menu {
|
||||
list-style-type: circle;
|
||||
margin-left-ltr: 25;
|
||||
margin-right-rtl: 25;
|
||||
}
|
||||
|
||||
ul li p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
a {
|
||||
color: #589df6;
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
address {
|
||||
color: #589df6;
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
big {
|
||||
font-size: x-large
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: x-small
|
||||
}
|
||||
|
||||
samp {
|
||||
font-size: small;
|
||||
font-family: Monospaced
|
||||
}
|
||||
|
||||
cite {
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: small;
|
||||
font-family: Monospaced
|
||||
}
|
||||
|
||||
dfn {
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
i {
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
kbd {
|
||||
font-size: small;
|
||||
font-family: Monospaced
|
||||
}
|
||||
|
||||
s {
|
||||
text-decoration: line-through
|
||||
}
|
||||
|
||||
strike {
|
||||
text-decoration: line-through
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
sub {
|
||||
vertical-align: sub
|
||||
}
|
||||
|
||||
sup {
|
||||
vertical-align: sup
|
||||
}
|
||||
|
||||
tt {
|
||||
font-family: Monospaced
|
||||
}
|
||||
|
||||
u {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
var {
|
||||
font-weight: bold;
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
table {
|
||||
border-color: Gray;
|
||||
border-style: outset
|
||||
}
|
||||
|
||||
tr {
|
||||
text-align: left
|
||||
}
|
||||
|
||||
td {
|
||||
border-color: Gray;
|
||||
border-style: inset;
|
||||
padding-left: 3;
|
||||
padding-right: 3;
|
||||
padding-top: 3;
|
||||
padding-bottom: 3
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
border-color: Gray;
|
||||
border-style: inset;
|
||||
padding-left: 3;
|
||||
padding-right: 3;
|
||||
padding-top: 3;
|
||||
padding-bottom: 3
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-top: 5;
|
||||
margin-bottom: 5;
|
||||
margin-left: 35;
|
||||
margin-right: 35
|
||||
}
|
||||
|
||||
center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 5;
|
||||
margin-bottom: 5;
|
||||
font-family: Monospaced
|
||||
}
|
||||
|
||||
pre p {
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
caption {
|
||||
caption-side: top;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
table {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
nobr {
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
.scroll-bar{
|
||||
-fx-background-color: #3c3f41;
|
||||
-fx-background-radius: 2em;
|
||||
}
|
||||
.scroll-bar:horizontal .track,
|
||||
.scroll-bar:vertical .track {
|
||||
-fx-background-color: #3c3f41;
|
||||
-fx-border-color:transparent;
|
||||
-fx-background-radius: 2em;
|
||||
}
|
||||
.scroll-bar:vertical .track-background,
|
||||
.scroll-bar:horizontal .track-background {
|
||||
-fx-background-color: #3c3f41;
|
||||
}
|
||||
.scroll-bar:horizontal .thumb {
|
||||
-fx-background-color: #bbbbbb;
|
||||
-fx-background-insets: 4 0 4 0;
|
||||
-fx-background-radius: 2em;
|
||||
-fx-opacity: 0.2;
|
||||
}
|
||||
.scroll-bar:vertical .thumb {
|
||||
-fx-background-color: #bbbbbb;
|
||||
-fx-background-insets: 0 4 0 4;
|
||||
-fx-background-radius: 2em;
|
||||
-fx-opacity: 0.2;
|
||||
}
|
||||
|
||||
|
||||
.scroll-bar:vertical:corner-present {
|
||||
-fx-shape: " ";
|
||||
-fx-width: 0;
|
||||
}
|
||||
|
||||
.scroll-bar:horizontal:corner-present {
|
||||
-fx-shape: " ";
|
||||
-fx-width: 0;
|
||||
}
|
||||
|
||||
.scroll-bar-corner {
|
||||
-fx-background-color: #3c3f41;
|
||||
}
|
||||
.scroll-bar:horizontal .increment-arrow {
|
||||
-fx-shape: " "
|
||||
}
|
||||
.scroll-bar:vertical .increment-arrow {
|
||||
-fx-shape: " "
|
||||
}
|
||||
.scroll-bar:horizontal .decrement-arrow {
|
||||
-fx-shape: " "
|
||||
}
|
||||
.scroll-bar:vertical .decrement-arrow {
|
||||
-fx-shape: " "
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
.scroll-bar{
|
||||
-fx-background-color: #ffffff;
|
||||
-fx-background-radius: 2em;
|
||||
}
|
||||
.scroll-bar:horizontal .track,
|
||||
.scroll-bar:vertical .track {
|
||||
-fx-background-color: #ffffff;
|
||||
-fx-border-color:transparent;
|
||||
-fx-background-radius: 2em;
|
||||
}
|
||||
.scroll-bar:vertical .track-background,
|
||||
.scroll-bar:horizontal .track-background {
|
||||
-fx-background-color: #ffffff;
|
||||
}
|
||||
.scroll-bar:horizontal .thumb {
|
||||
-fx-background-color: #dddddd;
|
||||
-fx-background-insets: 4 0 4 0;
|
||||
-fx-background-radius: 2em;
|
||||
}
|
||||
.scroll-bar:vertical .thumb {
|
||||
-fx-background-color: #dddddd;
|
||||
-fx-background-insets: 0 4 0 4;
|
||||
-fx-background-radius: 2em;
|
||||
}
|
||||
|
||||
.scroll-bar:vertical:corner-present {
|
||||
-fx-shape: " ";
|
||||
-fx-width: 0;
|
||||
}
|
||||
|
||||
.scroll-bar:horizontal:corner-present {
|
||||
-fx-shape: " ";
|
||||
-fx-width: 0;
|
||||
}
|
||||
|
||||
.scroll-bar-corner {
|
||||
-fx-background-color: #ffffff;
|
||||
}
|
||||
.scroll-bar:horizontal .increment-arrow {
|
||||
-fx-shape: " "
|
||||
}
|
||||
.scroll-bar:vertical .increment-arrow {
|
||||
-fx-shape: " "
|
||||
}
|
||||
.scroll-bar:horizontal .decrement-arrow {
|
||||
-fx-shape: " "
|
||||
}
|
||||
.scroll-bar:vertical .decrement-arrow {
|
||||
-fx-shape: " "
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package com.jetbrains.edu.learning;
|
||||
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup;
|
||||
import com.intellij.openapi.extensions.ExtensionPointName;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface StudyActionProvider {
|
||||
ExtensionPointName<StudyActionProvider> EP_NAME = ExtensionPointName.create("Edu.studyActionProvider");
|
||||
|
||||
@Nullable
|
||||
DefaultActionGroup getActionGroup(@NotNull Project project);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.jetbrains.edu.learning;
|
||||
|
||||
import com.intellij.CommonBundle;
|
||||
import com.intellij.reference.SoftReference;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.PropertyKey;
|
||||
|
||||
import java.lang.ref.Reference;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class StudyBundle {
|
||||
|
||||
public static String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, @NotNull Object... params) {
|
||||
return CommonBundle.message(getBundle(), key, params);
|
||||
}
|
||||
|
||||
private static Reference<ResourceBundle> ourBundle;
|
||||
@NonNls
|
||||
private static final String BUNDLE = "StudyBundle";
|
||||
|
||||
private StudyBundle() {
|
||||
}
|
||||
|
||||
// Cached loading
|
||||
private static ResourceBundle getBundle() {
|
||||
ResourceBundle bundle = SoftReference.dereference(ourBundle);
|
||||
if (bundle == null) {
|
||||
bundle = ResourceBundle.getBundle(BUNDLE);
|
||||
ourBundle = new SoftReference<>(bundle);
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
+2
@@ -35,6 +35,7 @@ import com.jetbrains.edu.learning.actions.*;
|
||||
import com.jetbrains.edu.learning.editor.StudyEditorFactoryListener;
|
||||
import com.jetbrains.edu.learning.ui.StudyProgressToolWindowFactory;
|
||||
import com.jetbrains.edu.learning.ui.StudyToolWindowFactory;
|
||||
import javafx.application.Platform;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -60,6 +61,7 @@ public class StudyProjectComponent implements ProjectComponent {
|
||||
@Override
|
||||
public void projectOpened() {
|
||||
final Course course = StudyTaskManager.getInstance(myProject).getCourse();
|
||||
Platform.setImplicitExit(false);
|
||||
if (course != null && !course.isUpToDate()) {
|
||||
course.setUpToDate(true);
|
||||
updateCourse();
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.jetbrains.edu.learning;
|
||||
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup;
|
||||
import com.intellij.openapi.extensions.ExtensionPointName;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.util.containers.HashMap;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public interface StudyToolWindowConfigurator {
|
||||
ExtensionPointName<StudyToolWindowConfigurator> EP_NAME = ExtensionPointName.create("Edu.studyToolWindowConfigurator");
|
||||
|
||||
@NotNull
|
||||
DefaultActionGroup getActionGroup(Project project);
|
||||
|
||||
HashMap<String, JPanel> getAdditionalPanels(Project project);
|
||||
|
||||
boolean accept(@NotNull final Project project);
|
||||
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
package com.jetbrains.edu.learning.ui;
|
||||
|
||||
import com.intellij.icons.AllIcons;
|
||||
import com.intellij.ide.BrowserUtil;
|
||||
import com.intellij.ide.ui.LafManager;
|
||||
import com.intellij.ide.ui.LafManagerListener;
|
||||
import com.intellij.ide.ui.laf.darcula.DarculaLookAndFeelInfo;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.jetbrains.edu.learning.StudyBundle;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebHistory;
|
||||
import javafx.scene.web.WebView;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.w3c.dom.events.EventListener;
|
||||
import org.w3c.dom.events.EventTarget;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.net.URL;
|
||||
|
||||
class StudyBrowserWindow extends JFrame {
|
||||
private static final String EVENT_TYPE_CLICK = "click";
|
||||
private JFXPanel myPanel;
|
||||
private WebView myWebComponent;
|
||||
private StackPane myPane;
|
||||
|
||||
private WebEngine myEngine;
|
||||
private ProgressBar myProgressBar;
|
||||
private boolean refInNewBrowser;
|
||||
private boolean showProgress = true;
|
||||
|
||||
public StudyBrowserWindow() {
|
||||
setSize(new Dimension(900, 800));
|
||||
setLayout(new BorderLayout());
|
||||
setPanel(new JFXPanel());
|
||||
setTitle("Study Browser");
|
||||
LafManager.getInstance().addLafManagerListener(new CheckIOLafManagerListener());
|
||||
initComponents();
|
||||
}
|
||||
|
||||
public void setShowProgress(boolean showProgress) {
|
||||
this.showProgress = showProgress;
|
||||
}
|
||||
|
||||
public void openLinkInNewWindow(boolean refInNewBrowser) {
|
||||
this.refInNewBrowser = refInNewBrowser;
|
||||
}
|
||||
|
||||
private void updateLaf(boolean isDarcula) {
|
||||
if (isDarcula) {
|
||||
updateLafDarcula();
|
||||
}
|
||||
else {
|
||||
updateIntellijAndGTKLaf();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateIntellijAndGTKLaf() {
|
||||
Platform.runLater(() -> {
|
||||
final URL scrollBarStyleUrl = getClass().getResource("/style/javaFXBrowserScrollBar.css");
|
||||
myPane.getStylesheets().add(scrollBarStyleUrl.toExternalForm());
|
||||
myEngine.setUserStyleSheetLocation(null);
|
||||
myEngine.reload();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateLafDarcula() {
|
||||
Platform.runLater(() -> {
|
||||
final URL engineStyleUrl = getClass().getResource("/style/javaFXBrowserDarcula.css");
|
||||
final URL scrollBarStyleUrl = getClass().getResource("/style/javaFXBrowserDarculaScrollBar.css");
|
||||
myEngine.setUserStyleSheetLocation(engineStyleUrl.toExternalForm());
|
||||
myPane.getStylesheets().add(scrollBarStyleUrl.toExternalForm());
|
||||
myPane.setStyle("-fx-background-color: #3c3f41");
|
||||
myPanel.getScene().getStylesheets().add(engineStyleUrl.toExternalForm());
|
||||
});
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
Platform.runLater(() -> {
|
||||
myPane = new StackPane();
|
||||
myWebComponent = new WebView();
|
||||
myEngine = myWebComponent.getEngine();
|
||||
|
||||
|
||||
if (showProgress) {
|
||||
myProgressBar = makeProgressBarWithListener();
|
||||
myWebComponent.setVisible(false);
|
||||
myPane.getChildren().addAll(myWebComponent, myProgressBar);
|
||||
}
|
||||
else {
|
||||
myPane.getChildren().add(myWebComponent);
|
||||
}
|
||||
if (refInNewBrowser) {
|
||||
initHyperlinkListener();
|
||||
}
|
||||
Scene scene = new Scene(myPane);
|
||||
myPanel.setScene(scene);
|
||||
myPanel.setVisible(true);
|
||||
updateLaf(LafManager.getInstance().getCurrentLookAndFeel() instanceof DarculaLookAndFeelInfo);
|
||||
});
|
||||
|
||||
add(myPanel, BorderLayout.CENTER);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
}
|
||||
|
||||
|
||||
public void load(@NotNull final String url) {
|
||||
Platform.runLater(() -> {
|
||||
updateLookWithProgressBarIfNeeded();
|
||||
myEngine.load(url);
|
||||
});
|
||||
}
|
||||
|
||||
public void loadContent(@NotNull final String content) {
|
||||
Platform.runLater(() -> {
|
||||
updateLookWithProgressBarIfNeeded();
|
||||
myEngine.loadContent(content);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateLookWithProgressBarIfNeeded() {
|
||||
if (showProgress) {
|
||||
myProgressBar.setVisible(true);
|
||||
myWebComponent.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void initHyperlinkListener() {
|
||||
myEngine.getLoadWorker().stateProperty().addListener((ov, oldState, newState) -> {
|
||||
if (newState == Worker.State.SUCCEEDED) {
|
||||
final EventListener listener = makeHyperLinkListener();
|
||||
|
||||
addListenerToAllHyperlinkItems(listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addListenerToAllHyperlinkItems(EventListener listener) {
|
||||
final Document doc = myEngine.getDocument();
|
||||
final NodeList nodeList = doc.getElementsByTagName("a");
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
((EventTarget)nodeList.item(i)).addEventListener(EVENT_TYPE_CLICK, listener, false);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private EventListener makeHyperLinkListener() {
|
||||
return ev -> {
|
||||
String domEventType = ev.getType();
|
||||
if (domEventType.equals(EVENT_TYPE_CLICK)) {
|
||||
myEngine.setJavaScriptEnabled(true);
|
||||
myEngine.getLoadWorker().cancel();
|
||||
ev.preventDefault();
|
||||
|
||||
ApplicationManager.getApplication().invokeLater(() -> {
|
||||
final String href = ((Element)ev.getTarget()).getAttribute("href");
|
||||
final StudyBrowserWindow checkIOBrowserWindow = new StudyBrowserWindow();
|
||||
checkIOBrowserWindow.addBackAndOpenButtons();
|
||||
checkIOBrowserWindow.openLinkInNewWindow(false);
|
||||
checkIOBrowserWindow.setShowProgress(true);
|
||||
checkIOBrowserWindow.load(href);
|
||||
checkIOBrowserWindow.setVisible(true);
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void addBackAndOpenButtons() {
|
||||
ApplicationManager.getApplication().invokeLater(() -> {
|
||||
final JPanel panel = new JPanel();
|
||||
panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
|
||||
|
||||
final JButton backButton = makeGoButton(StudyBundle.message("browser.action.back"), AllIcons.Actions.Back, -1);
|
||||
final JButton forwardButton = makeGoButton(StudyBundle.message("browser.action.forward"), AllIcons.Actions.Forward, 1);
|
||||
final JButton openInBrowser = new JButton(AllIcons.Actions.Browser_externalJavaDoc);
|
||||
openInBrowser.addActionListener(e -> BrowserUtil.browse(myEngine.getLocation()));
|
||||
openInBrowser.setToolTipText(StudyBundle.message("browser.action.open.link"));
|
||||
addButtonsAvailabilityListeners(backButton, forwardButton);
|
||||
|
||||
panel.setMaximumSize(new Dimension(40, getPanel().getHeight()));
|
||||
panel.add(backButton);
|
||||
panel.add(forwardButton);
|
||||
panel.add(openInBrowser);
|
||||
|
||||
add(panel, BorderLayout.PAGE_START);
|
||||
});
|
||||
}
|
||||
|
||||
private void addButtonsAvailabilityListeners(JButton backButton, JButton forwardButton) {
|
||||
Platform.runLater(() -> myEngine.getLoadWorker().stateProperty().addListener((ov, oldState, newState) -> {
|
||||
if (newState == Worker.State.SUCCEEDED) {
|
||||
final WebHistory history = myEngine.getHistory();
|
||||
boolean isGoBackAvailable = history.getCurrentIndex() > 0;
|
||||
boolean isGoForwardAvailable = history.getCurrentIndex() < history.getEntries().size() - 1;
|
||||
ApplicationManager.getApplication().invokeLater(() -> {
|
||||
backButton.setEnabled(isGoBackAvailable);
|
||||
forwardButton.setEnabled(isGoForwardAvailable);
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private JButton makeGoButton(@NotNull final String toolTipText, @NotNull final Icon icon, final int direction) {
|
||||
final JButton button = new JButton(icon);
|
||||
button.setEnabled(false);
|
||||
button.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 1) {
|
||||
Platform.runLater(() -> myEngine.getHistory().go(direction));
|
||||
}
|
||||
}
|
||||
});
|
||||
button.setToolTipText(toolTipText);
|
||||
return button;
|
||||
}
|
||||
|
||||
|
||||
private ProgressBar makeProgressBarWithListener() {
|
||||
final ProgressBar progress = new ProgressBar();
|
||||
progress.progressProperty().bind(myWebComponent.getEngine().getLoadWorker().progressProperty());
|
||||
|
||||
myWebComponent.getEngine().getLoadWorker().stateProperty().addListener(
|
||||
(ov, oldState, newState) -> {
|
||||
if (myWebComponent.getEngine().getLocation().contains("http") && newState == Worker.State.SUCCEEDED) {
|
||||
myProgressBar.setVisible(false);
|
||||
myWebComponent.setVisible(true);
|
||||
}
|
||||
});
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
public JFXPanel getPanel() {
|
||||
return myPanel;
|
||||
}
|
||||
|
||||
private void setPanel(JFXPanel panel) {
|
||||
myPanel = panel;
|
||||
}
|
||||
|
||||
private class CheckIOLafManagerListener implements LafManagerListener {
|
||||
@Override
|
||||
public void lookAndFeelChanged(LafManager manager) {
|
||||
updateLaf(manager.getCurrentLookAndFeel() instanceof DarculaLookAndFeelInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
-89
@@ -21,8 +21,6 @@ import com.intellij.openapi.actionSystem.ActionToolbar;
|
||||
import com.intellij.openapi.actionSystem.DataProvider;
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager;
|
||||
import com.intellij.openapi.editor.colors.EditorColorsScheme;
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
||||
import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
|
||||
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
|
||||
@@ -30,40 +28,78 @@ import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.SimpleToolWindowPanel;
|
||||
import com.intellij.openapi.util.io.FileUtil;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.ui.BrowserHyperlinkListener;
|
||||
import com.intellij.ui.components.JBScrollPane;
|
||||
import com.intellij.ui.JBCardLayout;
|
||||
import com.intellij.ui.OnePixelSplitter;
|
||||
import com.intellij.util.containers.HashMap;
|
||||
import com.intellij.util.ui.JBUI;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import com.jetbrains.edu.EduNames;
|
||||
import com.jetbrains.edu.courseFormat.Course;
|
||||
import com.jetbrains.edu.courseFormat.Task;
|
||||
import com.jetbrains.edu.courseFormat.TaskFile;
|
||||
import com.jetbrains.edu.learning.StudyActionProvider;
|
||||
import com.jetbrains.edu.learning.StudyTaskManager;
|
||||
import com.jetbrains.edu.learning.StudyToolWindowConfigurator;
|
||||
import com.jetbrains.edu.learning.StudyUtils;
|
||||
import com.jetbrains.edu.learning.actions.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.text.html.HTMLDocument;
|
||||
import javax.swing.text.html.HTMLEditorKit;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
public class StudyToolWindow extends SimpleToolWindowPanel implements DataProvider, Disposable {
|
||||
|
||||
private static final String EMPTY_TASK_TEXT = "Please, open any task to see task description";
|
||||
private static final Logger LOG = Logger.getInstance(StudyToolWindow.class);
|
||||
private static final String EMPTY_TASK_TEXT = "Please, open any task to see task description";
|
||||
private static final String TASK_INFO_ID = "taskInfo";
|
||||
private final JBCardLayout myCardLayout;
|
||||
private final JPanel myContentPanel;
|
||||
private final OnePixelSplitter mySplitPane;
|
||||
private StudyBrowserWindow myBrowserWindow;
|
||||
|
||||
public StudyToolWindow(final Project project) {
|
||||
super(true, true);
|
||||
myCardLayout = new JBCardLayout();
|
||||
myContentPanel = new JPanel(myCardLayout);
|
||||
mySplitPane = new OnePixelSplitter(myVertical=true);
|
||||
|
||||
String taskText = getTaskText(project);
|
||||
if (taskText == null) return;
|
||||
|
||||
JPanel toolbarPanel = createToolbarPanel(project);
|
||||
setToolbar(toolbarPanel);
|
||||
|
||||
final JTextPane taskTextPane = createTaskTextPane();
|
||||
myContentPanel.add(TASK_INFO_ID, createTaskInfoPanel(taskText));
|
||||
mySplitPane.setFirstComponent(myContentPanel);
|
||||
addAdditionalPanels(project);
|
||||
myCardLayout.show(myContentPanel, TASK_INFO_ID);
|
||||
|
||||
setContent(mySplitPane);
|
||||
|
||||
final FileEditorManagerListener listener = new StudyFileEditorManagerListener(project, this);
|
||||
project.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, listener);
|
||||
}
|
||||
|
||||
private void addAdditionalPanels(Project project) {
|
||||
StudyToolWindowConfigurator configurator = getStudyToolWindowConfigurator(project);
|
||||
assert configurator != null;
|
||||
HashMap<String, JPanel> panels = configurator.getAdditionalPanels(project);
|
||||
for (Map.Entry<String, JPanel> entry: panels.entrySet()) {
|
||||
myContentPanel.add(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
}
|
||||
|
||||
public void showPanelById(@NotNull final String panelId) {
|
||||
myCardLayout.swipe(myContentPanel, panelId, JBCardLayout.SwipeDirection.AUTO);
|
||||
}
|
||||
|
||||
public void setButtomComponent(JComponent component) {
|
||||
mySplitPane.setSecondComponent(component);
|
||||
}
|
||||
|
||||
private static String getTaskText(@NotNull final Project project) {
|
||||
VirtualFile[] files = FileEditorManager.getInstance(project).getSelectedFiles();
|
||||
TaskFile taskFile = null;
|
||||
for (VirtualFile file : files) {
|
||||
@@ -73,48 +109,25 @@ public class StudyToolWindow extends SimpleToolWindowPanel implements DataProvid
|
||||
}
|
||||
}
|
||||
if (taskFile == null) {
|
||||
taskTextPane.setText(EMPTY_TASK_TEXT);
|
||||
setContent(taskTextPane);
|
||||
return;
|
||||
return EMPTY_TASK_TEXT;
|
||||
}
|
||||
final Task task = taskFile.getTask();
|
||||
|
||||
if (task != null) {
|
||||
final String taskText = getTaskText(task, task.getTaskDir(project));
|
||||
if (taskText == null) {
|
||||
return;
|
||||
}
|
||||
JBScrollPane scrollPane = new JBScrollPane(taskTextPane);
|
||||
taskTextPane.setText(taskText);
|
||||
taskTextPane.addHyperlinkListener(BrowserHyperlinkListener.INSTANCE);
|
||||
|
||||
setContent(scrollPane);
|
||||
|
||||
final FileEditorManagerListener listener = new StudyFileEditorManagerListener(project, taskTextPane);
|
||||
project.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, listener);
|
||||
return getTaskTextFromTask(task, task.getTaskDir(project));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static JTextPane createTaskTextPane() {
|
||||
final JTextPane taskTextPane = new JTextPane();
|
||||
taskTextPane.setContentType(new HTMLEditorKit().getContentType());
|
||||
final EditorColorsScheme editorColorsScheme = EditorColorsManager.getInstance().getGlobalScheme();
|
||||
int fontSize = editorColorsScheme.getEditorFontSize();
|
||||
final String fontName = editorColorsScheme.getEditorFontName();
|
||||
final Font font = new Font(fontName, Font.PLAIN, fontSize);
|
||||
String bodyRule = "body { font-family: " + font.getFamily() + "; " +
|
||||
"font-size: " + font.getSize() + "pt; }";
|
||||
((HTMLDocument)taskTextPane.getDocument()).getStyleSheet().addRule(bodyRule);
|
||||
taskTextPane.setEditable(false);
|
||||
if (!UIUtil.isUnderDarcula()) {
|
||||
taskTextPane.setBackground(EditorColorsManager.getInstance().getGlobalScheme().getDefaultBackground());
|
||||
}
|
||||
taskTextPane.setBorder(new EmptyBorder(15, 20, 0, 100));
|
||||
return taskTextPane;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
|
||||
private JPanel createTaskInfoPanel(String taskText) {
|
||||
myBrowserWindow = new StudyBrowserWindow();
|
||||
myBrowserWindow.setShowProgress(false);
|
||||
myBrowserWindow.openLinkInNewWindow(true);
|
||||
myBrowserWindow.addBackAndOpenButtons();
|
||||
myBrowserWindow.loadContent(taskText);
|
||||
JPanel panel = new JPanel();
|
||||
panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
|
||||
panel.add(myBrowserWindow.getPanel());
|
||||
return panel;
|
||||
}
|
||||
|
||||
private static JPanel createToolbarPanel(@NotNull final Project project) {
|
||||
@@ -125,18 +138,14 @@ public class StudyToolWindow extends SimpleToolWindowPanel implements DataProvid
|
||||
}
|
||||
|
||||
private static DefaultActionGroup getActionGroup(@NotNull final Project project) {
|
||||
StudyActionProvider[] extensions = StudyActionProvider.EP_NAME.getExtensions();
|
||||
DefaultActionGroup actionGroup = null;
|
||||
Course course = StudyTaskManager.getInstance(project).getCourse();
|
||||
if (course == null) {
|
||||
return createDefaultActionGroup(project);
|
||||
}
|
||||
for (StudyActionProvider actionProvider: extensions) {
|
||||
actionGroup = actionProvider.getActionGroup(project);
|
||||
if (actionGroup != null) break;
|
||||
}
|
||||
StudyToolWindowConfigurator configurator = getStudyToolWindowConfigurator(project);
|
||||
assert configurator != null;
|
||||
|
||||
return actionGroup == null ? createDefaultActionGroup(project) : actionGroup;
|
||||
return configurator.getActionGroup(project);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -152,14 +161,51 @@ public class StudyToolWindow extends SimpleToolWindowPanel implements DataProvid
|
||||
return group;
|
||||
}
|
||||
|
||||
static class StudyFileEditorManagerListener implements FileEditorManagerListener {
|
||||
private static final Logger LOG = Logger.getInstance(StudyFileEditorManagerListener.class);
|
||||
private Project myProject;
|
||||
private JTextPane myTaskTextPane;
|
||||
private void setTaskText(String text) {
|
||||
myBrowserWindow.loadContent(text);
|
||||
}
|
||||
|
||||
StudyFileEditorManagerListener(@NotNull final Project project, JTextPane taskTextPane) {
|
||||
@Nullable
|
||||
private static String getTaskTextFromTask(@Nullable final Task task, @Nullable final VirtualFile taskDirectory) {
|
||||
if (task == null) {
|
||||
return null;
|
||||
}
|
||||
String text = task.getText();
|
||||
if (text != null) {
|
||||
return text;
|
||||
}
|
||||
if (taskDirectory != null) {
|
||||
VirtualFile taskTextFile = taskDirectory.findChild(EduNames.TASK_HTML);
|
||||
if (taskTextFile != null) {
|
||||
try {
|
||||
return FileUtil.loadTextAndClose(taskTextFile.getInputStream());
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOG.info(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static StudyToolWindowConfigurator getStudyToolWindowConfigurator(@NotNull Project project) {
|
||||
StudyToolWindowConfigurator[] extensions = StudyToolWindowConfigurator.EP_NAME.getExtensions();
|
||||
for (StudyToolWindowConfigurator extension: extensions) {
|
||||
if (extension.accept(project)) {
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static class StudyFileEditorManagerListener implements FileEditorManagerListener {
|
||||
private Project myProject;
|
||||
private StudyToolWindow myStudyToolWindow;
|
||||
|
||||
StudyFileEditorManagerListener(@NotNull final Project project, StudyToolWindow studyToolWindow) {
|
||||
myProject = project;
|
||||
myTaskTextPane = taskTextPane;
|
||||
myStudyToolWindow = studyToolWindow;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -170,7 +216,7 @@ public class StudyToolWindow extends SimpleToolWindowPanel implements DataProvid
|
||||
|
||||
@Override
|
||||
public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
|
||||
myTaskTextPane.setText(EMPTY_TASK_TEXT);
|
||||
myStudyToolWindow.setTaskText(EMPTY_TASK_TEXT);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -194,35 +240,14 @@ public class StudyToolWindow extends SimpleToolWindowPanel implements DataProvid
|
||||
}
|
||||
|
||||
private void setTaskText(@Nullable final Task task, @Nullable final VirtualFile taskDirectory) {
|
||||
String text = getTaskText(task, taskDirectory);
|
||||
String text = getTaskTextFromTask(task, taskDirectory);
|
||||
if (text == null) {
|
||||
myTaskTextPane.setText(EMPTY_TASK_TEXT);
|
||||
myStudyToolWindow.setTaskText(EMPTY_TASK_TEXT);
|
||||
return;
|
||||
}
|
||||
myTaskTextPane.setText(text);
|
||||
myStudyToolWindow.setTaskText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getTaskText(@Nullable final Task task, @Nullable final VirtualFile taskDirectory) {
|
||||
if (task == null) {
|
||||
return null;
|
||||
}
|
||||
String text = task.getText();
|
||||
if (text != null) {
|
||||
return text;
|
||||
}
|
||||
if (taskDirectory != null) {
|
||||
VirtualFile taskTextFile = taskDirectory.findChild(EduNames.TASK_HTML);
|
||||
if (taskTextFile != null) {
|
||||
try {
|
||||
return FileUtil.loadTextAndClose(taskTextFile.getInputStream());
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOG.info(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<extensions defaultExtensionNs="Edu">
|
||||
<StudyExecutor implementationClass="com.jetbrains.edu.learning.PyStudyExecutor" language="Python"/>
|
||||
<StudyLanguageManager implementationClass="com.jetbrains.edu.learning.PyStudyLanguageManager" language="Python"/>
|
||||
<studyActionProvider implementation="com.jetbrains.edu.learning.PyStudyActionProvider" />
|
||||
<studyToolWindowConfigurator implementation="com.jetbrains.edu.learning.PyStudyToolWindowConfigurator" />
|
||||
</extensions>
|
||||
<actions>
|
||||
<action id="WelcomeScreen.PythonIntro" class="com.jetbrains.edu.learning.actions.PyStudyIntroductionCourseAction"
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package com.jetbrains.edu.learning;
|
||||
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.jetbrains.edu.courseFormat.Course;
|
||||
import com.jetbrains.edu.learning.actions.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class PyStudyActionProvider implements StudyActionProvider {
|
||||
|
||||
@Override
|
||||
public DefaultActionGroup getActionGroup(@NotNull Project project) {
|
||||
Course course = StudyTaskManager.getInstance(project).getCourse();
|
||||
if (course != null && course.getLanguage().equals("Python") && course.getCourseType().equals("PyCharm")) {
|
||||
final DefaultActionGroup group = new DefaultActionGroup();
|
||||
|
||||
group.add(StudyCheckAction.createCheckAction(course));
|
||||
group.add(new StudyPreviousStudyTaskAction());
|
||||
group.add(new StudyNextStudyTaskAction());
|
||||
group.add(new StudyRefreshTaskFileAction());
|
||||
group.add(new StudyShowHintAction());
|
||||
|
||||
group.add(new StudyRunAction());
|
||||
group.add(new StudyEditInputAction());
|
||||
return group;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.jetbrains.edu.learning;
|
||||
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.util.containers.HashMap;
|
||||
import com.jetbrains.edu.courseFormat.Course;
|
||||
import com.jetbrains.edu.learning.actions.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public class PyStudyToolWindowConfigurator implements StudyToolWindowConfigurator {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DefaultActionGroup getActionGroup(Project project) {
|
||||
Course course = StudyTaskManager.getInstance(project).getCourse();
|
||||
final DefaultActionGroup group = new DefaultActionGroup();
|
||||
group.add(StudyCheckAction.createCheckAction(course));
|
||||
group.add(new StudyPreviousStudyTaskAction());
|
||||
group.add(new StudyNextStudyTaskAction());
|
||||
group.add(new StudyRefreshTaskFileAction());
|
||||
group.add(new StudyShowHintAction());
|
||||
|
||||
group.add(new StudyRunAction());
|
||||
group.add(new StudyEditInputAction());
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<String, JPanel> getAdditionalPanels(Project project) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(@NotNull Project project) {
|
||||
Course course = StudyTaskManager.getInstance(project).getCourse();
|
||||
return course != null && course.getLanguage().equals("Python") && course.getCourseType().equals("PyCharm");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user