All Downloads are FREE. Search and download functionalities are using the official Maven repository.

bibliothek.gui.dock.title.AbstractMultiDockTitle Maven / Gradle / Ivy

The newest version!
/*
 * Bibliothek - DockingFrames
 * Library built on Java/Swing, allows the user to "drag and drop"
 * panels containing any Swing-Component the developer likes to add.
 * 
 * Copyright (C) 2011 Benjamin Sigg
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Benjamin Sigg
 * [email protected]
 * CH - Switzerland
 */
package bibliothek.gui.dock.title;

import java.awt.Color;
import java.awt.Component;
import java.awt.ContainerOrderFocusTraversalPolicy;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.InputEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.border.Border;
import javax.swing.event.MouseInputListener;

import bibliothek.gui.DockController;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.DockElement;
import bibliothek.gui.dock.action.DockAction;
import bibliothek.gui.dock.action.view.ViewTarget;
import bibliothek.gui.dock.disable.DisablingStrategy;
import bibliothek.gui.dock.event.DockHierarchyEvent;
import bibliothek.gui.dock.event.DockHierarchyListener;
import bibliothek.gui.dock.event.DockableListener;
import bibliothek.gui.dock.themes.ThemeManager;
import bibliothek.gui.dock.themes.basic.action.BasicTitleViewItem;
import bibliothek.gui.dock.themes.border.BorderModifier;
import bibliothek.gui.dock.themes.font.TitleFont;
import bibliothek.gui.dock.util.BackgroundAlgorithm;
import bibliothek.gui.dock.util.ConfiguredBackgroundPanel;
import bibliothek.gui.dock.util.DockProperties;
import bibliothek.gui.dock.util.DockUtilities;
import bibliothek.gui.dock.util.PropertyValue;
import bibliothek.gui.dock.util.Transparency;
import bibliothek.gui.dock.util.UIValue;
import bibliothek.gui.dock.util.color.AbstractDockColor;
import bibliothek.gui.dock.util.color.ColorManager;
import bibliothek.gui.dock.util.font.AbstractDockFont;
import bibliothek.gui.dock.util.font.FontManager;
import bibliothek.gui.dock.util.font.FontModifier;
import bibliothek.gui.dock.util.swing.OrientedLabel;
import bibliothek.util.Condition;
import bibliothek.util.FrameworkOnly;
import bibliothek.util.Path;

/**
 * An abstract implementation of {@link DockTitle}. This title can have an icon and some text, but it does not
 * show buttons for {@link DockAction}s.
* Clients should make use of {@link AbstractDockTitle} which is also the super-class for all the {@link DockTitle}s that * are used by the framework. * @author Benjamin Sigg */ @FrameworkOnly public abstract class AbstractMultiDockTitle extends ConfiguredBackgroundPanel implements DockTitle { /** Insets of the size 1,2,1,2 */ private static final Insets DEFAULT_INSETS_HORIZONTAL = new Insets( 0, 1, 0, 1 ); /** Insets of the size 2,1,2,1 */ private static final Insets DEFAULT_INSETS_VERTICAL = new Insets( 1, 0, 1, 0 ); /** The {@link Dockable} for which this title is shown */ private Dockable dockable; /** A label for the title-text */ private OrientedLabel label = new OrientedLabel(){ @Override protected void updateFonts(){ AbstractMultiDockTitle.this.updateFonts(); } }; /** * A listener added to the owned {@link Dockable}. The listener changes the * title-text and the icon of this title. */ private Listener listener = new Listener(); /** The creator of this title */ private DockTitleVersion origin; /** true if this title is currently selected, false otherwise */ private boolean active = false; /** true if this title is currently bound to a {@link Dockable} */ private boolean bound = false; /** Whether the layout should be horizontal or vertical */ private Orientation orientation = Orientation.FREE_HORIZONTAL; /** The icon which is shown on this title */ private Icon icon; /** disabled version of {@link #icon} */ private Icon disabledIcon; /** number of pixels to paint between icon and text */ private int iconTextGap = 0; /** the colors used by this title */ private List colors = new ArrayList(); /** the fonts used by this title */ private List fonts = new ArrayList(); /** the fonts which are used automatically */ private List conditionalFonts; /** the background of this title */ private Background background = new Background(); /** the current border, can be null */ private TitleBorder border; /** whether this title is disabled */ private boolean disabled = false; /** all the mouse listeners of this title */ private List mouseInputListeners = new ArrayList(); /** tells how to paint the text on this title */ private PropertyValue orientationConverter = new PropertyValue( DockTitle.ORIENTATION_STRATEGY ){ protected void valueChanged( OrientationToRotationStrategy oldValue, OrientationToRotationStrategy newValue ){ if( oldValue != null ){ oldValue.removeListener( orientationListener ); oldValue.uninstall( AbstractMultiDockTitle.this ); } if( newValue != null ){ newValue.install( AbstractMultiDockTitle.this ); newValue.addListener( orientationListener ); } updateLabelRotation(); } }; /** a listener added to the current {@link OrientationToRotationStrategy} represented by {@link #orientationConverter} */ private OrientationToRotationStrategyListener orientationListener = new OrientationToRotationStrategyListener(){ public void rotationChanged( Dockable dockable, DockTitle title ){ if( title == AbstractMultiDockTitle.this || title == null ){ if( dockable == AbstractMultiDockTitle.this.dockable || dockable == null ){ updateLabelRotation(); } } } }; /** * Standard constructor * @param dockable The Dockable whose title this will be * @param origin The version which was used to create this title */ public AbstractMultiDockTitle( Dockable dockable, DockTitleVersion origin ){ super( Transparency.SOLID ); init( dockable, origin ); } /** * Constructor which does not do anything. Subclasses should call * {@link #init(Dockable, DockTitleVersion)} to initialize * the title. */ protected AbstractMultiDockTitle(){ super( Transparency.SOLID ); } /** * Initializer called by the constructor. * @param dockable The Dockable whose title this will be * @param origin The version which was used to create this title */ protected void init( Dockable dockable, DockTitleVersion origin ){ this.dockable = dockable; this.origin = origin; label.setBackground( background ); setBackground( background ); setLayout( null ); add( label ); setActive( false ); setFocusTraversalPolicyProvider( true ); setFocusTraversalPolicy( new ContainerOrderFocusTraversalPolicy(){ @Override protected boolean accept( Component component ) { return component != AbstractMultiDockTitle.this && super.accept( component ); } }); setOpaque( false ); } /** * Sets the number of pixels to paint between icon and text. * @param iconTextGap the number of pixels to paint */ public void setIconTextGap( int iconTextGap ){ this.iconTextGap = iconTextGap; revalidate(); } /** * Gets the number of pixels to paint between icon and text * @return the number of pixels */ public int getIconTextGap(){ return iconTextGap; } /** * Adds a color to the list of colors, this title will ensure that * color gets connected to a {@link ColorManager} as soon * as this title is bound. * @param color the new color */ protected void addColor( AbstractDockColor color ){ colors.add( color ); if( bound ){ color.connect( getDockable().getController() ); } } /** * Removes a color from this title * @param color the color to remove */ protected void removeColor( AbstractDockColor color ){ colors.remove( color ); color.connect( null ); } /** * Adds a font to the list of fonts, this title will ensure that * font gets connected to a {@link FontManager} as soon * as this title is bound. * @param font the new font */ protected void addFont( AbstractDockFont font ){ fonts.add( font ); if( bound ){ font.connect( getDockable().getController() ); } } /** * Removes a font from this title. * @param font the font to remove */ protected void removeFont( AbstractDockFont font ){ fonts.remove( font ); font.connect( null ); } /** * Adds a new conditional font to this title, the conditional font will * be applied to {@link #setFont(Font)} when its condition * is met. If there is more than one font whose condition is met, then the * first one that was registered is used. * @param id the id of the font which is to be used * @param kind what kind of title this is * @param condition the condition to met * @param backup to be used when there is not font set in the {@link FontManager} */ protected void addConditionalFont( String id, Path kind, Condition condition, FontModifier backup ){ ConditionalFont font = new ConditionalFont( id, kind, condition, backup ); addFont( font ); if( conditionalFonts == null ) conditionalFonts = new ArrayList(); conditionalFonts.add( font ); updateFonts(); } /** * Removes all fonts which were set using {@link #addConditionalFont(String, Path, Condition, FontModifier)} */ protected void removeAllConditionalFonts(){ if( conditionalFonts != null ){ for( ConditionalFont font : conditionalFonts ){ removeFont( font ); } conditionalFonts = null; updateFonts(); } } /** * Gets the location and the size of the icon. * @return the bounds or null if no icon is registered */ public Rectangle getIconBounds(){ if( icon == null ) return null; Insets insets = titleInsets(); if( orientation.isVertical() ){ int width = getWidth() - insets.left - insets.right; return new Rectangle( insets.left + (width - icon.getIconWidth())/2, insets.top, icon.getIconWidth(), icon.getIconHeight() ); } else{ int height = getHeight() - insets.top - insets.bottom; return new Rectangle( insets.left, insets.top + (height - icon.getIconHeight()) / 2, icon.getIconWidth(), icon.getIconHeight() ); } } /** * Tells this title whether it should be disabled or not. This method is called when the {@link DisablingStrategy} * changes. A disabled title should react to any {@link InputEvent}, and should be painted differently than an * enabled title. * @param disabled whether this title is disabled * @see #isDisabled() */ protected void setDisabled( boolean disabled ){ if( this.disabled != disabled ){ this.disabled = disabled; label.setEnabled( !disabled ); setEnabled( !disabled ); if( disabled ){ for( MouseInputListener listener : mouseInputListeners ){ doRemoveMouseInputListener( listener ); } } else{ for( MouseInputListener listener : mouseInputListeners ){ doAddMouseInputListener( listener ); } } } } /** * Tells whether this title is disabled, a disabled title does not react to any user input. * @return whether the title is disabled * @see #setDisabled(boolean) */ protected boolean isDisabled(){ return disabled; } @Override public void paintBackground( Graphics g ){ if( getTransparency() != Transparency.TRANSPARENT ){ paintBackground( g, this ); } } /** * Paints the whole background of this title. The default implementation * just fills the background with the background color of component. * @param g the graphics context used to paint * @param component the {@link Component} which represents this title */ protected void paintBackground( Graphics g, JComponent component ){ g.setColor( component.getBackground() ); g.fillRect( 0, 0, component.getWidth(), component.getHeight() ); } @Override public void paintForeground( Graphics g ){ paintForeground( g, this ); } /** * Paints the whole foreground of this title. The default implementation only paints an icon. * @param g the graphics context to use * @param component the {@link Component} which represents this title */ protected void paintForeground( Graphics g, JComponent component ){ paintIcon( g, component ); } /** * Paints the icon of this title. * @param g the graphics context to use * @param component the {@link Component} which represents this title */ protected void paintIcon( Graphics g, JComponent component ){ Icon icon = this.icon; if( icon != null ){ if( isDisabled() ){ if( disabledIcon == null ){ disabledIcon = DockUtilities.disabledIcon( component, icon ); } icon = disabledIcon; } if( icon != null ){ Insets insets = titleInsets(); if( orientation.isVertical() ){ int width = getWidth() - insets.left - insets.right; icon.paintIcon( this, g, insets.left + (width - icon.getIconWidth())/2, insets.top ); } else{ int height = getHeight() - insets.top - insets.bottom; icon.paintIcon( this, g, insets.left, insets.top + (height - icon.getIconHeight()) / 2 ); } } } } /** * Sets the icon of this title. The icon is shown on the top or the left * edge. * @param icon the icon, can be null */ protected void setIcon( Icon icon ){ this.icon = icon; disabledIcon = null; revalidate(); repaint(); } /** * Gets the icon of this title. * @return the icon or null * @see #setIcon(Icon) */ protected Icon getIcon(){ return icon; } /** * Grants access to the {@link OrientedLabel} which paints the title text. * @return the label, never null */ protected OrientedLabel getLabel(){ return label; } /** * Sets the text of this title. The text either painted horizontally or * vertically. * @param text the text or null */ protected void setText( String text ){ label.setText( text ); repaint(); } /** * Gets the text which is shown on this title. * @return the text */ protected String getText(){ return label.getText(); } /** * Sets the tooltip that will be shown on this title. * @param text the new tooltip, can be null */ protected void setTooltip( String text ){ setToolTipText( text ); label.setToolTipText( text ); } public void setOrientation( Orientation orientation ) { this.orientation = orientation; updateLabelRotation(); revalidate(); } private void updateLabelRotation(){ label.setRotation( orientationConverter.getValue().convert( getOrientation(), this )); } /** * Gets the current orientation. * @return the orientation * @see #setOrientation(bibliothek.gui.dock.title.DockTitle.Orientation) */ public Orientation getOrientation() { return orientation; } public DockTitleVersion getOrigin() { return origin; } @Override public void setForeground( Color fg ) { super.setForeground( fg ); if( label != null ) label.setForeground( fg ); } @Override public void setBackground( Color fg ) { super.setBackground( fg ); if( label != null ) label.setBackground( fg ); } @Override public void setFont( Font font ) { super.setFont( font ); if( label != null ) label.setFont( font ); } public void setFontModifier( FontModifier modifier ) { label.setFontModifier( modifier ); } @Override public Dimension getMinimumSize() { if( icon != null ) return new Dimension( icon.getIconWidth(), icon.getIconHeight() ); Dimension preferred = getPreferredSize(); int min = Math.min( preferred.width, preferred.height ); return new Dimension( min, min ); } /** * Sets the border and the {@link BorderModifier} that should be used by this title. * @param key the identifier of the modifier or null * @param border the default title, can be null */ public void setBorder( String key, Border border ){ if( this.border != null ){ if( key == null || !this.border.id.equals( key )){ this.border.setController( null ); this.border = null; } } if( this.border == null && key != null ){ this.border = new TitleBorder( key ); if( bound ){ this.border.setController( getOrigin().getController() ); } } if( this.border == null ){ setBorder( border ); } else{ this.border.setBorder( border ); } } /** * Gets the insets that have to be applied between the border and the * content (icon, text, actions) of this title. Subclasses may use this method to * create free space in which they can paint additional items. * @return the insets, not null */ protected Insets getInnerInsets(){ if( getOrientation().isHorizontal() ) return DEFAULT_INSETS_HORIZONTAL; else return DEFAULT_INSETS_VERTICAL; } /** * Gets the {@link Insets} of this title, tells how much space is not to be covered * by children {@link Component}s. * @return the insets, never null */ protected Insets titleInsets(){ Insets insets = getInsets(); if( insets == null ){ return getInnerInsets(); } else{ insets = new Insets( insets.top, insets.left, insets.bottom, insets.right ); } Insets inner = getInnerInsets(); insets.top += inner.top; insets.bottom += inner.bottom; insets.left += inner.left; insets.right += inner.right; return insets; } @Override public void doLayout(){ super.doLayout(); doTitleLayout(); } /** * Updates the layout (position and size of all children) of this title. */ protected void doTitleLayout(){ Insets insets = titleInsets(); int x = insets.left; int y = insets.top; int width = getWidth() - insets.left - insets.right; int height = getHeight() - insets.top - insets.bottom; if( orientation.isHorizontal() ){ if( icon != null ){ x += icon.getIconWidth() + iconTextGap; width -= icon.getIconWidth() + iconTextGap; } label.setBounds( x, y, width, height ); } else{ if( icon != null ){ y += icon.getIconWidth() + iconTextGap; height -= icon.getIconWidth() + iconTextGap; } label.setBounds( x, y, width, height ); } } public void addMouseInputListener( MouseInputListener listener ) { mouseInputListeners.add( listener ); if( !isDisabled() ){ doAddMouseInputListener( listener ); } } private void doAddMouseInputListener( MouseInputListener listener ){ addMouseListener( listener ); addMouseMotionListener( listener ); label.addMouseListener( listener ); label.addMouseMotionListener( listener ); } public void removeMouseInputListener( MouseInputListener listener ) { mouseInputListeners.remove( listener ); if( !isDisabled() ){ doRemoveMouseInputListener( listener ); } } private void doRemoveMouseInputListener( MouseInputListener listener ){ removeMouseListener( listener ); removeMouseMotionListener( listener ); label.removeMouseListener( listener ); label.removeMouseMotionListener( listener ); } public Point getPopupLocation( Point click, boolean popupTrigger ){ if( popupTrigger ) return click; Rectangle icon = getIconBounds(); if( icon != null ){ if( icon.contains( click )){ if( getOrientation().isHorizontal() ) return new Point( icon.x, icon.y + icon.height ); else return new Point( icon.x + icon.width, icon.y ); } } return null; } public Dockable getDockable() { return dockable; } public DockElement getElement() { return getDockable(); } public boolean isUsedAsTitle() { return true; } public boolean shouldTransfersFocus(){ return true; } public boolean shouldFocus(){ return true; } /** * Sets whether this title should be painted as focused or not. * @param active true if the {@link Dockable} of this title * has the focus. */ public void setActive( boolean active ) { this.active = active; } public void changed( DockTitleEvent event ) { if( event instanceof ActivityDockTitleEvent ){ setActive( ((ActivityDockTitleEvent)event).isActive() ); } } public boolean isActive(){ return active; } @Override public Dimension getPreferredSize() { Dimension preferred; if( (getText() == null || getText().length() == 0 ) ){ preferred = new Dimension( 5, 5 ); } else{ preferred = label.getPreferredSize(); } Insets insets = titleInsets(); if( orientation.isHorizontal() ){ int width = 0; int height = 0; if( icon != null ){ width = icon.getIconWidth(); height = icon.getIconHeight(); } height = Math.max( height, preferred.height ); width += preferred.width; if( icon == null ) width = Math.max( width, 2*height ); preferred = new Dimension( width + iconTextGap + insets.left + insets.right, height + insets.top + insets.bottom ); } else{ int width = 0; int height = 0; if( icon != null ){ width = icon.getIconWidth(); height = icon.getIconHeight(); } width = Math.max( width, preferred.width ); height += preferred.height; if( icon == null ) height = Math.max( height, 2*width ); preferred = new Dimension( width + insets.left + insets.right, height + iconTextGap + insets.top + insets.bottom ); } return preferred; } /** * Creates a new item for action which will be shown on this title. * @param action The action which will be triggered by the button * @param dockable The {@link Dockable} which will be affected by the action * @return the new graphical representation of the action */ protected BasicTitleViewItem createItemFor( DockAction action, Dockable dockable ){ return dockable.getController().getActionViewConverter().createView( action, ViewTarget.TITLE, dockable ); } public void bind() { if( bound ) throw new IllegalArgumentException( "Do not call bound twice!" ); bound = true; DockController controller = getDockable().getController(); dockable.addDockableListener( listener ); if( controller != null ){ for( AbstractDockColor color : colors ) color.connect( controller ); for( AbstractDockFont font : fonts ) font.connect( controller ); orientationConverter.setProperties( controller ); } background.setController( controller ); if( border != null ){ border.setController( controller ); } updateText(); updateIcon(); updateTooltip(); revalidate(); } public void unbind() { if( !bound ) throw new IllegalArgumentException( "Do not call unbind twice" ); bound = false; dockable.removeDockableListener( listener ); for( AbstractDockColor color : colors ) color.connect( null ); for( AbstractDockFont font : fonts ) font.connect( null ); orientationConverter.setProperties( (DockProperties)null ); if( border != null ){ border.setController( null ); } setText( "" ); setIcon( null ); setTooltip( null ); background.setController( null ); } /** * Called when the icon of this title should be updated. This title * never calls {@link #setIcon(Icon)} directly, it always calls this method * which then calls {@link #setIcon(Icon)} (the only exception: on * unbinding the icon is set to null) */ protected void updateIcon(){ setIcon( dockable.getTitleIcon() ); } /** * Called when the text of this title should be updated. This title * never calls {@link #setText(String)} directly, it always calls this method * which then calls {@link #setText(String)} (the only exception: on * unbinding the text is set to null) */ protected void updateText(){ setText( dockable.getTitleText() ); } /** * Called when the tooltip of this title should be updated. This * title never calls {@link #setTooltip(String)} directly, it always * calls this method which then calls {@link #setTooltip(String)} (the * only exception: on unbinding the tooltip is set to null) */ protected void updateTooltip(){ setTooltip( dockable.getTitleToolTip() ); } /** * Tells whether this title is bound to a {@link Dockable} or not. * @return true if the title is {@link #bind() bound}, false * {@link #unbind() otherwise} */ public boolean isBound(){ return bound; } /** * Checks the state of this title and may replace the font of the title. */ protected void updateFonts(){ if( conditionalFonts != null ){ FontModifier modifier = null; for( ConditionalFont font : conditionalFonts ){ if( font.getState() ){ modifier = font.value(); break; } } setFontModifier( modifier ); } } /** * A font that is only used when a condition is met. * @author Benjamin Sigg */ private class ConditionalFont extends TitleFont{ private Condition condition; public ConditionalFont( String id, Path kind, Condition condition, FontModifier backup ){ super( id, AbstractMultiDockTitle.this, kind, backup ); this.condition = condition; } /** * Gets whether the condition is met or not. * @return true if this font should be used */ public boolean getState(){ return condition.getState(); } @Override protected void changed( FontModifier oldValue, FontModifier newValue ) { updateFonts(); } } /** * A listener to the {@link Dockable} of this title. * @author Benjamin Sigg */ private class Listener implements DockableListener, DockHierarchyListener{ public void titleIconChanged( Dockable dockable, Icon oldIcon, Icon newIcon ) { updateIcon(); updateText(); } public void titleTextChanged( Dockable dockable, String oldTitle, String newTitle ) { updateIcon(); updateText(); } public void titleToolTipChanged( Dockable dockable, String oldTooltip, String newTooltip ) { updateTooltip(); } public void titleUnbound( Dockable dockable, DockTitle title ) { // do nothing } public void titleBound( Dockable dockable, DockTitle title ) { // do nothing } public void titleExchanged( Dockable dockable, DockTitle title ) { // do nothing } public void controllerChanged( DockHierarchyEvent event ) { DockController controller = event.getDockable().getController(); for( AbstractDockColor color : colors ) color.connect( controller ); } public void hierarchyChanged( DockHierarchyEvent event ) { // do nothing } } /** * Represents the background of this {@link DockTitle}. * @author Benjamin Sigg */ private class Background extends BackgroundAlgorithm implements DockTitleBackgroundComponent{ public Background(){ super( DockTitleBackgroundComponent.KIND, ThemeManager.BACKGROUND_PAINT + ".title" ); } public DockTitle getTitle(){ return AbstractMultiDockTitle.this; } public Component getComponent(){ return getTitle().getComponent(); } } /** * Represents this title as {@link UIValue} to get a {@link BorderModifier} * @author Benjamin Sigg */ private class TitleBorder implements TitleDockBorder{ /** the identifier of this border */ private String id; /** the current modifier */ private BorderModifier modifier; /** the source of all values */ private DockController controller; /** the default border */ private Border border; /** * Creates a new wrapper * @param id the identifier of this {@link UIValue} */ public TitleBorder( String id ){ this.id = id; } public DockTitle getTitle(){ return AbstractMultiDockTitle.this; } public void set( BorderModifier value ){ if( value != modifier ){ modifier = value; update(); } } /** * Sets the default border * @param border the default border, can be null */ public void setBorder( Border border ){ if( this.border != border ){ this.border = border; update(); } } private void update(){ if( modifier == null ){ AbstractMultiDockTitle.this.setBorder( border ); } else{ AbstractMultiDockTitle.this.setBorder( modifier.modify( border ) ); } } /** * Sets the controller to observe for a value. * @param controller the controller, can be null */ public void setController( DockController controller ){ if( this.controller != null ){ this.controller.getThemeManager().remove( this ); } this.controller = controller; if( this.controller == null ){ set( null ); } else{ this.controller.getThemeManager().add( id, TitleDockBorder.KIND, ThemeManager.BORDER_MODIFIER_TYPE, this ); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy