mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-02-08 04:20:09 +00:00
Introjucer: new tab panel look and feel.
This commit is contained in:
parent
9fc29dad53
commit
515bfe90b2
12 changed files with 200 additions and 117 deletions
|
|
@ -38,7 +38,6 @@ namespace AppearanceColours
|
|||
static const ColourInfo colours[] =
|
||||
{
|
||||
{ "Main Window Bkgd", mainBackgroundColourId, true },
|
||||
{ "Project Panel Bkgd", projectPanelBackgroundColourId, true },
|
||||
{ "Treeview Highlight", treeviewHighlightColourId, false },
|
||||
|
||||
{ "Code Background", CodeEditorComponent::backgroundColourId, true },
|
||||
|
|
@ -460,7 +459,6 @@ Component* AppearanceSettings::createEditorWindow()
|
|||
IntrojucerLookAndFeel::IntrojucerLookAndFeel()
|
||||
{
|
||||
setColour (mainBackgroundColourId, Colour::greyLevel (0.8f));
|
||||
setColour (projectPanelBackgroundColourId, Colour::greyLevel (0.93f));
|
||||
setColour (treeviewHighlightColourId, Colour (0x401111ee));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,13 +78,72 @@ class IntrojucerLookAndFeel : public LookAndFeel
|
|||
public:
|
||||
IntrojucerLookAndFeel();
|
||||
|
||||
int getTabButtonOverlap (int tabDepth) { return -1; }
|
||||
int getTabButtonSpaceAroundImage() { return 1; }
|
||||
int getTabButtonBestWidth (TabBarButton& button, int tabDepth) { return 120; }
|
||||
|
||||
void createTabTextLayout (const TabBarButton& button, const Rectangle<int>& textArea, GlyphArrangement& textLayout)
|
||||
{
|
||||
Font font (textArea.getHeight() * 0.5f);
|
||||
font.setUnderline (button.hasKeyboardFocus (false));
|
||||
|
||||
textLayout.addFittedText (font, button.getButtonText().trim(),
|
||||
(float) textArea.getX(), (float) textArea.getY(), (float) textArea.getWidth(), (float) textArea.getHeight(),
|
||||
Justification::centred, 1);
|
||||
}
|
||||
|
||||
static Colour getTabBackgroundColour (TabBarButton& button)
|
||||
{
|
||||
Colour normalBkg (button.getTabBackgroundColour());
|
||||
Colour bkg (normalBkg.contrasting (0.15f));
|
||||
if (button.isFrontTab())
|
||||
bkg = bkg.overlaidWith (Colours::yellow.withAlpha (0.5f));
|
||||
|
||||
return bkg;
|
||||
}
|
||||
|
||||
void drawTabButton (TabBarButton& button, Graphics& g, bool isMouseOver, bool isMouseDown)
|
||||
{
|
||||
const Rectangle<int> activeArea (button.getActiveArea());
|
||||
|
||||
Colour bkg (getTabBackgroundColour (button));
|
||||
|
||||
g.setGradientFill (ColourGradient (bkg.brighter (0.1f), 0, (float) activeArea.getY(),
|
||||
bkg.darker (0.1f), 0, (float) activeArea.getBottom(), false));
|
||||
g.fillRect (activeArea);
|
||||
|
||||
g.setColour (button.getTabBackgroundColour().darker (0.3f));
|
||||
g.drawRect (activeArea);
|
||||
|
||||
GlyphArrangement textLayout;
|
||||
createTabTextLayout (button, button.getTextArea(), textLayout);
|
||||
|
||||
const float alpha = button.isEnabled() ? ((isMouseOver || isMouseDown) ? 1.0f : 0.8f) : 0.3f;
|
||||
g.setColour (bkg.contrasting().withMultipliedAlpha (alpha));
|
||||
textLayout.draw (g);
|
||||
}
|
||||
|
||||
Rectangle<int> getTabButtonExtraComponentBounds (const TabBarButton& button, Rectangle<int>& textArea, Component& comp)
|
||||
{
|
||||
GlyphArrangement textLayout;
|
||||
createTabTextLayout (button, textArea, textLayout);
|
||||
const int textWidth = (int) textLayout.getBoundingBox (0, -1, false).getWidth();
|
||||
const int extraSpace = jmax (0, textArea.getWidth() - (textWidth + comp.getWidth())) / 2;
|
||||
|
||||
textArea.removeFromRight (extraSpace);
|
||||
textArea.removeFromLeft (extraSpace);
|
||||
return textArea.removeFromRight (comp.getWidth());
|
||||
}
|
||||
|
||||
void drawTabAreaBehindFrontButton (TabbedButtonBar&, Graphics&, int, int) {}
|
||||
|
||||
void drawStretchableLayoutResizerBar (Graphics& g, int /*w*/, int /*h*/, bool /*isVerticalBar*/, bool isMouseOver, bool isMouseDragging)
|
||||
{
|
||||
if (isMouseOver || isMouseDragging)
|
||||
g.fillAll (Colours::grey.withAlpha (0.4f));
|
||||
g.fillAll (Colours::yellow.withAlpha (0.4f));
|
||||
}
|
||||
|
||||
Rectangle<int> getPropertyComponentContentPosition (PropertyComponent& component);
|
||||
Rectangle<int> getPropertyComponentContentPosition (PropertyComponent&);
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ const char* const sourceOrHeaderFileExtensions = "cpp;mm;m;c;cc;cxx;h;hpp;hxx";
|
|||
enum ColourIds
|
||||
{
|
||||
mainBackgroundColourId = 0x2340000,
|
||||
projectPanelBackgroundColourId = 0x2340001,
|
||||
treeviewHighlightColourId = 0x2340002,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -26,76 +26,6 @@
|
|||
#include "jucer_SourceCodeEditor.h"
|
||||
#include "../Application/jucer_OpenDocumentManager.h"
|
||||
|
||||
|
||||
//==============================================================================
|
||||
SourceCodeEditor::SourceCodeEditor (OpenDocumentManager::Document* document_)
|
||||
: DocumentEditorComponent (document_)
|
||||
{
|
||||
}
|
||||
|
||||
SourceCodeEditor::~SourceCodeEditor()
|
||||
{
|
||||
getAppSettings().appearance.settings.removeListener (this);
|
||||
|
||||
SourceCodeDocument* doc = dynamic_cast <SourceCodeDocument*> (getDocument());
|
||||
|
||||
if (doc != nullptr)
|
||||
doc->updateLastState (*editor);
|
||||
}
|
||||
|
||||
void SourceCodeEditor::createEditor (CodeDocument& codeDocument)
|
||||
{
|
||||
if (document->getFile().hasFileExtension (sourceOrHeaderFileExtensions))
|
||||
setEditor (new CppCodeEditorComponent (codeDocument));
|
||||
else
|
||||
setEditor (new CodeEditorComponent (codeDocument, nullptr));
|
||||
}
|
||||
|
||||
void SourceCodeEditor::setEditor (CodeEditorComponent* newEditor)
|
||||
{
|
||||
addAndMakeVisible (editor = newEditor);
|
||||
|
||||
#if JUCE_MAC
|
||||
Font font (13.0f);
|
||||
font.setTypefaceName ("Menlo");
|
||||
#else
|
||||
Font font (12.0f);
|
||||
font.setTypefaceName (Font::getDefaultMonospacedFontName());
|
||||
#endif
|
||||
editor->setFont (font);
|
||||
|
||||
editor->setTabSize (4, true);
|
||||
|
||||
updateColourScheme();
|
||||
getAppSettings().appearance.settings.addListener (this);
|
||||
}
|
||||
|
||||
void SourceCodeEditor::highlightLine (int lineNum, int characterIndex)
|
||||
{
|
||||
if (lineNum <= editor->getFirstLineOnScreen()
|
||||
|| lineNum >= editor->getFirstLineOnScreen() + editor->getNumLinesOnScreen() - 1)
|
||||
{
|
||||
editor->scrollToLine (jmax (0, jmin (lineNum - editor->getNumLinesOnScreen() / 3,
|
||||
editor->getDocument().getNumLines() - editor->getNumLinesOnScreen())));
|
||||
}
|
||||
|
||||
editor->moveCaretTo (CodeDocument::Position (&editor->getDocument(), lineNum - 1, characterIndex), false);
|
||||
}
|
||||
|
||||
void SourceCodeEditor::resized()
|
||||
{
|
||||
editor->setBounds (getLocalBounds());
|
||||
}
|
||||
|
||||
void SourceCodeEditor::updateColourScheme() { getAppSettings().appearance.applyToCodeEditor (*editor); }
|
||||
|
||||
void SourceCodeEditor::valueTreePropertyChanged (ValueTree&, const Identifier&) { updateColourScheme(); }
|
||||
void SourceCodeEditor::valueTreeChildAdded (ValueTree&, ValueTree&) { updateColourScheme(); }
|
||||
void SourceCodeEditor::valueTreeChildRemoved (ValueTree&, ValueTree&) { updateColourScheme(); }
|
||||
void SourceCodeEditor::valueTreeChildOrderChanged (ValueTree&) { updateColourScheme(); }
|
||||
void SourceCodeEditor::valueTreeParentChanged (ValueTree&) { updateColourScheme(); }
|
||||
void SourceCodeEditor::valueTreeRedirected (ValueTree&) { updateColourScheme(); }
|
||||
|
||||
//==============================================================================
|
||||
SourceCodeDocument::SourceCodeDocument (Project* project_, const File& file_)
|
||||
: modDetector (file_), project (project_)
|
||||
|
|
@ -167,3 +97,72 @@ void SourceCodeDocument::applyLastState (CodeEditorComponent& editor) const
|
|||
if (lastState != nullptr)
|
||||
lastState->restoreState (editor);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
SourceCodeEditor::SourceCodeEditor (OpenDocumentManager::Document* document_)
|
||||
: DocumentEditorComponent (document_)
|
||||
{
|
||||
}
|
||||
|
||||
SourceCodeEditor::~SourceCodeEditor()
|
||||
{
|
||||
getAppSettings().appearance.settings.removeListener (this);
|
||||
|
||||
SourceCodeDocument* doc = dynamic_cast <SourceCodeDocument*> (getDocument());
|
||||
|
||||
if (doc != nullptr)
|
||||
doc->updateLastState (*editor);
|
||||
}
|
||||
|
||||
void SourceCodeEditor::createEditor (CodeDocument& codeDocument)
|
||||
{
|
||||
if (document->getFile().hasFileExtension (sourceOrHeaderFileExtensions))
|
||||
setEditor (new CppCodeEditorComponent (codeDocument));
|
||||
else
|
||||
setEditor (new CodeEditorComponent (codeDocument, nullptr));
|
||||
}
|
||||
|
||||
void SourceCodeEditor::setEditor (CodeEditorComponent* newEditor)
|
||||
{
|
||||
addAndMakeVisible (editor = newEditor);
|
||||
|
||||
#if JUCE_MAC
|
||||
Font font (13.0f);
|
||||
font.setTypefaceName ("Menlo");
|
||||
#else
|
||||
Font font (12.0f);
|
||||
font.setTypefaceName (Font::getDefaultMonospacedFontName());
|
||||
#endif
|
||||
editor->setFont (font);
|
||||
|
||||
editor->setTabSize (4, true);
|
||||
|
||||
updateColourScheme();
|
||||
getAppSettings().appearance.settings.addListener (this);
|
||||
}
|
||||
|
||||
void SourceCodeEditor::highlightLine (int lineNum, int characterIndex)
|
||||
{
|
||||
if (lineNum <= editor->getFirstLineOnScreen()
|
||||
|| lineNum >= editor->getFirstLineOnScreen() + editor->getNumLinesOnScreen() - 1)
|
||||
{
|
||||
editor->scrollToLine (jmax (0, jmin (lineNum - editor->getNumLinesOnScreen() / 3,
|
||||
editor->getDocument().getNumLines() - editor->getNumLinesOnScreen())));
|
||||
}
|
||||
|
||||
editor->moveCaretTo (CodeDocument::Position (&editor->getDocument(), lineNum - 1, characterIndex), false);
|
||||
}
|
||||
|
||||
void SourceCodeEditor::resized()
|
||||
{
|
||||
editor->setBounds (getLocalBounds());
|
||||
}
|
||||
|
||||
void SourceCodeEditor::updateColourScheme() { getAppSettings().appearance.applyToCodeEditor (*editor); }
|
||||
|
||||
void SourceCodeEditor::valueTreePropertyChanged (ValueTree&, const Identifier&) { updateColourScheme(); }
|
||||
void SourceCodeEditor::valueTreeChildAdded (ValueTree&, ValueTree&) { updateColourScheme(); }
|
||||
void SourceCodeEditor::valueTreeChildRemoved (ValueTree&, ValueTree&) { updateColourScheme(); }
|
||||
void SourceCodeEditor::valueTreeChildOrderChanged (ValueTree&) { updateColourScheme(); }
|
||||
void SourceCodeEditor::valueTreeParentChanged (ValueTree&) { updateColourScheme(); }
|
||||
void SourceCodeEditor::valueTreeRedirected (ValueTree&) { updateColourScheme(); }
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ public:
|
|||
|
||||
int updateSize (int x, int y, int width)
|
||||
{
|
||||
int height = 36;
|
||||
int height = 38;
|
||||
|
||||
for (int i = 0; i < properties.size(); ++i)
|
||||
{
|
||||
|
|
@ -66,15 +66,14 @@ public:
|
|||
|
||||
void paint (Graphics& g)
|
||||
{
|
||||
g.setColour (Colours::white.withAlpha (0.3f));
|
||||
g.fillRect (0, 28, getWidth(), getHeight() - 38);
|
||||
const Colour bkg (findColour (mainBackgroundColourId));
|
||||
|
||||
g.setColour (Colours::black.withAlpha (0.4f));
|
||||
g.drawRect (0, 28, getWidth(), getHeight() - 38);
|
||||
g.setColour (Colours::white.withAlpha (0.35f));
|
||||
g.fillRect (0, 30, getWidth(), getHeight() - 38);
|
||||
|
||||
g.setFont (Font (14.0f, Font::bold));
|
||||
g.setColour (Colours::black);
|
||||
g.drawFittedText (getName(), 12, 0, getWidth() - 16, 26, Justification::bottomLeft, 1);
|
||||
g.setFont (Font (15.0f, Font::bold));
|
||||
g.setColour (bkg.contrasting (0.7f));
|
||||
g.drawFittedText (getName(), 12, 0, getWidth() - 16, 25, Justification::bottomLeft, 1);
|
||||
}
|
||||
|
||||
OwnedArray<PropertyComponent> properties;
|
||||
|
|
@ -96,11 +95,7 @@ public:
|
|||
|
||||
void paint (Graphics& g)
|
||||
{
|
||||
g.setTiledImageFill (ImageCache::getFromMemory (BinaryData::brushed_aluminium_png,
|
||||
BinaryData::brushed_aluminium_pngSize),
|
||||
0, 0, 1.0f);
|
||||
g.fillAll();
|
||||
drawRecessedShadows (g, getWidth(), getHeight(), 14);
|
||||
drawTexturedBackground (g);
|
||||
}
|
||||
|
||||
void resized()
|
||||
|
|
|
|||
|
|
@ -57,12 +57,12 @@ public:
|
|||
//==============================================================================
|
||||
void paint (Graphics& g)
|
||||
{
|
||||
g.fillAll (findColour (projectPanelBackgroundColourId));
|
||||
drawTexturedBackground (g);
|
||||
}
|
||||
|
||||
void resized()
|
||||
{
|
||||
list.setBounds (getLocalBounds().reduced (4, 2));
|
||||
list.setBounds (getLocalBounds().reduced (5, 4));
|
||||
}
|
||||
|
||||
int getNumRows()
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ public:
|
|||
|
||||
void resized()
|
||||
{
|
||||
Rectangle<int> r (getLocalBounds());
|
||||
Rectangle<int> r (getAvailableBounds());
|
||||
r.removeFromBottom (6);
|
||||
|
||||
if (saveAndOpenButton.isVisible())
|
||||
|
|
@ -98,6 +98,7 @@ ProjectContentComponent::ProjectContentComponent()
|
|||
treeSizeConstrainer.setMaximumWidth (500);
|
||||
|
||||
treeViewTabs.setOutline (0);
|
||||
treeViewTabs.getTabbedButtonBar().setMinimumTabScaleFactor (0.3);
|
||||
|
||||
JucerApplication::getApp().openDocumentManager.addListener (this);
|
||||
}
|
||||
|
|
@ -117,6 +118,23 @@ void ProjectContentComponent::paint (Graphics& g)
|
|||
g.fillAll (findColour (mainBackgroundColourId));
|
||||
}
|
||||
|
||||
void ProjectContentComponent::paintOverChildren (Graphics& g)
|
||||
{
|
||||
if (contentView != nullptr)
|
||||
{
|
||||
const int shadowSize = 15;
|
||||
const int x = contentView->getX();
|
||||
|
||||
ColourGradient cg (Colours::black.withAlpha (0.25f), (float) x, 0,
|
||||
Colours::transparentBlack, (float) (x - shadowSize), 0, false);
|
||||
cg.addColour (0.4, Colours::black.withAlpha (0.07f));
|
||||
cg.addColour (0.6, Colours::black.withAlpha (0.02f));
|
||||
|
||||
g.setGradientFill (cg);
|
||||
g.fillRect (x - shadowSize, 0, shadowSize, getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectContentComponent::resized()
|
||||
{
|
||||
Rectangle<int> r (getLocalBounds());
|
||||
|
|
@ -132,7 +150,7 @@ void ProjectContentComponent::resized()
|
|||
|
||||
void ProjectContentComponent::lookAndFeelChanged()
|
||||
{
|
||||
const Colour tabColour (findColour (projectPanelBackgroundColourId));
|
||||
const Colour tabColour (findColour (mainBackgroundColourId));
|
||||
|
||||
for (int i = treeViewTabs.getNumTabs(); --i >= 0;)
|
||||
treeViewTabs.setTabBackgroundColour (i, tabColour);
|
||||
|
|
@ -207,7 +225,7 @@ void ProjectContentComponent::setProject (Project* newProject)
|
|||
void ProjectContentComponent::createProjectTabs()
|
||||
{
|
||||
jassert (project != nullptr);
|
||||
const Colour tabColour (findColour (projectPanelBackgroundColourId));
|
||||
const Colour tabColour (findColour (mainBackgroundColourId));
|
||||
|
||||
treeViewTabs.addTab ("Files", tabColour, new FileTreeTab (*project), true);
|
||||
treeViewTabs.addTab ("Config", tabColour, new ConfigTreeTab (*project), true);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,8 @@ public:
|
|||
bool isCommandActive (const CommandID commandID);
|
||||
bool perform (const InvocationInfo& info);
|
||||
|
||||
void paint (Graphics& g);
|
||||
void paint (Graphics&);
|
||||
void paintOverChildren (Graphics&);
|
||||
void resized();
|
||||
void childBoundsChanged (Component* child);
|
||||
void lookAndFeelChanged();
|
||||
|
|
|
|||
|
|
@ -66,13 +66,13 @@ void JucerTreeViewBase::paintOpenCloseButton (Graphics& g, int width, int height
|
|||
else
|
||||
p.addTriangle (width * 0.25f, height * 0.25f, width * 0.8f, height * 0.5f, width * 0.25f, height * 0.75f);
|
||||
|
||||
g.setColour (getOwnerView()->findColour (projectPanelBackgroundColourId).contrasting (0.3f));
|
||||
g.setColour (getOwnerView()->findColour (mainBackgroundColourId).contrasting (0.3f));
|
||||
g.fillPath (p);
|
||||
}
|
||||
|
||||
Colour JucerTreeViewBase::getBackgroundColour() const
|
||||
{
|
||||
Colour background (getOwnerView()->findColour (projectPanelBackgroundColourId));
|
||||
Colour background (getOwnerView()->findColour (mainBackgroundColourId));
|
||||
|
||||
if (isSelected())
|
||||
background = background.overlaidWith (getOwnerView()->findColour (treeviewHighlightColourId));
|
||||
|
|
|
|||
|
|
@ -162,7 +162,12 @@ public:
|
|||
|
||||
void resized()
|
||||
{
|
||||
tree.setBounds (getLocalBounds());
|
||||
tree.setBounds (getAvailableBounds());
|
||||
}
|
||||
|
||||
Rectangle<int> getAvailableBounds() const
|
||||
{
|
||||
return Rectangle<int> (0, 2, getWidth() - 2, getHeight() - 2);
|
||||
}
|
||||
|
||||
TreeView tree;
|
||||
|
|
|
|||
|
|
@ -198,30 +198,39 @@ void drawComponentPlaceholder (Graphics& g, int w, int h, const String& text)
|
|||
g.drawFittedText (text, 2, 2, w - 4, h - 4, Justification::centredTop, 2);
|
||||
}
|
||||
|
||||
void drawRecessedShadows (Graphics& g, int w, int h, int shadowSize)
|
||||
static Image createTexturisedBackgroundTile()
|
||||
{
|
||||
ColourGradient cg (Colours::black.withAlpha (0.15f), 0, 0,
|
||||
Colours::transparentBlack, 0, (float) shadowSize, false);
|
||||
cg.addColour (0.4, Colours::black.withAlpha (0.07f));
|
||||
cg.addColour (0.6, Colours::black.withAlpha (0.02f));
|
||||
const Colour bkg (LookAndFeel::getDefaultLookAndFeel().findColour (mainBackgroundColourId));
|
||||
const int64 hash = bkg.getARGB() + 0x3474572a;
|
||||
|
||||
g.setGradientFill (cg);
|
||||
g.fillRect (0, 0, w, shadowSize);
|
||||
Image tile (ImageCache::getFromHashCode (hash));
|
||||
|
||||
cg.point1.setXY (0.0f, (float) h);
|
||||
cg.point2.setXY (0.0f, (float) h - shadowSize);
|
||||
g.setGradientFill (cg);
|
||||
g.fillRect (0, h - shadowSize, w, shadowSize);
|
||||
if (tile.isNull())
|
||||
{
|
||||
const Image original (ImageCache::getFromMemory (BinaryData::brushed_aluminium_png,
|
||||
BinaryData::brushed_aluminium_pngSize));
|
||||
|
||||
cg.point1.setXY (0.0f, 0.0f);
|
||||
cg.point2.setXY ((float) shadowSize, 0.0f);
|
||||
g.setGradientFill (cg);
|
||||
g.fillRect (0, 0, shadowSize, h);
|
||||
tile = Image (Image::RGB, original.getWidth(), original.getHeight(), false);
|
||||
|
||||
cg.point1.setXY ((float) w, 0.0f);
|
||||
cg.point2.setXY ((float) w - shadowSize, 0.0f);
|
||||
g.setGradientFill (cg);
|
||||
g.fillRect (w - shadowSize, 0, shadowSize, h);
|
||||
for (int y = 0; y < tile.getHeight(); ++y)
|
||||
{
|
||||
for (int x = 0; x < tile.getWidth(); ++x)
|
||||
{
|
||||
const float b = original.getPixelAt (x, y).getBrightness();
|
||||
tile.setPixelAt (x, y, bkg.withMultipliedBrightness (b + 0.4f));
|
||||
}
|
||||
}
|
||||
|
||||
ImageCache::addImageToCache (tile, hash);
|
||||
}
|
||||
|
||||
return tile;
|
||||
}
|
||||
|
||||
void drawTexturedBackground (Graphics& g)
|
||||
{
|
||||
g.setTiledImageFill (createTexturisedBackgroundTile(), 0, 0, 1.0f);
|
||||
g.fillAll();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ int indexOfLineStartingWith (const StringArray& lines, const String& text, int s
|
|||
void autoScrollForMouseEvent (const MouseEvent& e, bool scrollX = true, bool scrollY = true);
|
||||
|
||||
void drawComponentPlaceholder (Graphics& g, int w, int h, const String& text);
|
||||
void drawRecessedShadows (Graphics& g, int w, int h, int shadowSize);
|
||||
void drawTexturedBackground (Graphics& g);
|
||||
|
||||
void showUTF8ToolWindow();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue