mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
61 Rotating Control Channel Support - P25 Decoder (#475)
* Initial work on add support for rotating control channels. * Creates multiple frequency editor control. * Updates channel source editor to allow user to select multi-frequency configuration and add multiple frequencies. * Adds full support for rotating control channels to the P25 channelizer. Users define multiple frequencies in the channel source configuration. The decoder monitors the state of the channel for a CONTROL state. If the state does not stay in the CONTROL state for a user-specified delay value, then the decoder rotates to the next frequency in the list. This behavior continues until the decoder state once again changes to CONTROL. This is NOT a 'scanner' feature.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -456,7 +456,7 @@ public class ProcessingChain implements Listener<ChannelEvent>
|
||||
|
||||
if(module instanceof IHeartbeatProvider)
|
||||
{
|
||||
((IHeartbeatProvider)module).getHeartbeatManager().addHeartbeatListener(mHeartbeatBroadcaster);
|
||||
((IHeartbeatProvider)module).addHeartbeatListener(mHeartbeatBroadcaster);
|
||||
}
|
||||
|
||||
if(module instanceof IMessageProvider)
|
||||
@@ -528,7 +528,7 @@ public class ProcessingChain implements Listener<ChannelEvent>
|
||||
|
||||
if(module instanceof IHeartbeatProvider)
|
||||
{
|
||||
((IHeartbeatProvider)module).getHeartbeatManager().removeHeartbeatListener(mHeartbeatBroadcaster);
|
||||
((IHeartbeatProvider)module).removeHeartbeatListener(mHeartbeatBroadcaster);
|
||||
}
|
||||
|
||||
if(module instanceof IMessageProvider)
|
||||
@@ -571,7 +571,7 @@ public class ProcessingChain implements Listener<ChannelEvent>
|
||||
mSourceEventBroadcaster.broadcast(SourceEvent.sampleRateChange(mSource.getSampleRate(), "Processing Chain Startup"));
|
||||
|
||||
//Broadcast the source center frequency / sample rate so that each of the modules can self-configure
|
||||
mSourceEventBroadcaster.broadcast(SourceEvent.frequencyChange(mSource.getFrequency(), "Processing Chain Startup"));
|
||||
mSourceEventBroadcaster.broadcast(SourceEvent.frequencyChange(mSource, mSource.getFrequency(), "Processing Chain Startup"));
|
||||
|
||||
//Setup the channel state to monitor source overflow conditions
|
||||
mSource.setOverflowListener(mChannelState);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -24,6 +24,7 @@ import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.alias.action.AliasActionManager;
|
||||
import io.github.dsheirer.audio.AudioModule;
|
||||
import io.github.dsheirer.channel.state.AlwaysUnsquelchedDecoderState;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMap;
|
||||
@@ -87,7 +88,10 @@ import io.github.dsheirer.module.decode.tait.Tait1200Decoder;
|
||||
import io.github.dsheirer.module.decode.tait.Tait1200DecoderState;
|
||||
import io.github.dsheirer.module.demodulate.am.AMDemodulatorModule;
|
||||
import io.github.dsheirer.module.demodulate.fm.FMDemodulatorModule;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import io.github.dsheirer.source.tuner.channel.ChannelRotationMonitor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -109,9 +113,10 @@ public class DecoderFactory
|
||||
*
|
||||
* @return list of configured decoders
|
||||
*/
|
||||
public static List<Module> getModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel)
|
||||
public static List<Module> getModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel,
|
||||
UserPreferences userPreferences)
|
||||
{
|
||||
List<Module> modules = getPrimaryModules(channelMapModel, channel, aliasModel);
|
||||
List<Module> modules = getPrimaryModules(channelMapModel, channel, aliasModel, userPreferences);
|
||||
modules.addAll(getAuxiliaryDecoders(channel.getAuxDecodeConfiguration()));
|
||||
return modules;
|
||||
}
|
||||
@@ -119,7 +124,8 @@ public class DecoderFactory
|
||||
/**
|
||||
* Constructs a primary decoder as specified in the decode configuration
|
||||
*/
|
||||
public static List<Module> getPrimaryModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel)
|
||||
public static List<Module> getPrimaryModules(ChannelMapModel channelMapModel, Channel channel,
|
||||
AliasModel aliasModel, UserPreferences userPreferences)
|
||||
{
|
||||
List<Module> modules = new ArrayList<Module>();
|
||||
|
||||
@@ -239,6 +245,15 @@ public class DecoderFactory
|
||||
}
|
||||
|
||||
modules.add(new P25AudioModule());
|
||||
|
||||
//Add a channel rotation monitor when we have multiple control channel frequencies specified
|
||||
if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency &&
|
||||
((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).hasMultipleFrequencies())
|
||||
{
|
||||
List<State> activeStates = new ArrayList<>();
|
||||
activeStates.add(State.CONTROL);
|
||||
modules.add(new ChannelRotationMonitor(activeStates, userPreferences));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown decoder type [" + decodeConfig.getDecoderType().toString() + "]");
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
@@ -26,6 +28,7 @@ import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.source.SourceConfigurationEditor;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
@@ -43,178 +46,176 @@ import java.awt.event.ActionListener;
|
||||
|
||||
public class P25DecoderEditor extends ValidatingEditor<Channel>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( P25DecoderEditor.class );
|
||||
private final static Logger mLog = LoggerFactory.getLogger(P25DecoderEditor.class);
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private JComboBox<P25DecoderLSM.Modulation> mComboModulation;
|
||||
private JCheckBox mIgnoreDataCalls;
|
||||
private JLabel mTrafficChannelPoolSizeLabel;
|
||||
private JSlider mTrafficChannelPoolSize;
|
||||
|
||||
public P25DecoderEditor()
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0,wrap 3", "[right][grow,fill][]", "" ) );
|
||||
|
||||
mComboModulation = new JComboBox<P25DecoderLSM.Modulation>();
|
||||
mComboModulation.setModel( new DefaultComboBoxModel<P25DecoderLSM.Modulation>(
|
||||
P25DecoderLSM.Modulation.values() ) );
|
||||
mComboModulation.setEnabled( false );
|
||||
mComboModulation.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
|
||||
add( new JLabel( "Modulation:" ) );
|
||||
add( mComboModulation );
|
||||
|
||||
mIgnoreDataCalls = new JCheckBox( "Ignore Data Calls" );
|
||||
mIgnoreDataCalls.setEnabled( false );
|
||||
mIgnoreDataCalls.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
|
||||
add( mIgnoreDataCalls );
|
||||
|
||||
mTrafficChannelPoolSize = new JSlider( JSlider.HORIZONTAL, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MINIMUM,
|
||||
DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MAXIMUM, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT );
|
||||
mTrafficChannelPoolSize.setEnabled( false );
|
||||
mTrafficChannelPoolSize.setMajorTickSpacing( 10 );
|
||||
mTrafficChannelPoolSize.setMinorTickSpacing( 5 );
|
||||
mTrafficChannelPoolSize.setPaintTicks( true );
|
||||
mTrafficChannelPoolSize.addChangeListener( new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged( ChangeEvent e )
|
||||
{
|
||||
mTrafficChannelPoolSizeLabel.setText( "Traffic Pool: " + mTrafficChannelPoolSize.getValue() );
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
|
||||
mTrafficChannelPoolSizeLabel = new JLabel( "Traffic Pool: " + mTrafficChannelPoolSize.getValue() + " " );
|
||||
|
||||
add( mTrafficChannelPoolSizeLabel );
|
||||
add( mTrafficChannelPoolSize );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if( hasItem() && isModified() )
|
||||
{
|
||||
DecodeConfigP25Phase1 p25 = new DecodeConfigP25Phase1();
|
||||
|
||||
p25.setModulation( (P25Decoder.Modulation)mComboModulation.getSelectedItem() );
|
||||
p25.setIgnoreDataCalls( mIgnoreDataCalls.isSelected() );
|
||||
p25.setTrafficChannelPoolSize( mTrafficChannelPoolSize.getValue() );
|
||||
|
||||
getItem().setDecodeConfiguration( p25 );
|
||||
}
|
||||
|
||||
setModified( false );
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate( Editor<Channel> editor ) throws EditorValidationException
|
||||
{
|
||||
/**
|
||||
* Enforce CQPSK modulation uses a Tuner Source for I/Q sample data
|
||||
*/
|
||||
if( editor instanceof SourceConfigurationEditor &&
|
||||
((P25Decoder.Modulation)mComboModulation.getSelectedItem()) == P25Decoder.Modulation.CQPSK )
|
||||
{
|
||||
SourceConfiguration config = ((SourceConfigurationEditor)editor).getSourceConfiguration();
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0,wrap 3", "[right][grow,fill][]", ""));
|
||||
|
||||
if( config != null && !( config instanceof SourceConfigTuner ) )
|
||||
{
|
||||
throw new EditorValidationException( editor,
|
||||
"<html><body width='175'><h3>LSM Simulcast</h3>"
|
||||
+ "<p>P25 LSM Simulcast decoder can only be used with "
|
||||
+ "a tuner source. Please change the Source to use a tuner"
|
||||
+ " or change the P25 Decoder to C4FM modulation</p>" );
|
||||
}
|
||||
}
|
||||
|
||||
if( editor instanceof AuxDecodeConfigurationEditor)
|
||||
{
|
||||
AuxDecodeConfiguration config = ((AuxDecodeConfigurationEditor)editor).getConfiguration();
|
||||
|
||||
if( config != null && !config.getAuxDecoders().isEmpty() )
|
||||
{
|
||||
throw new EditorValidationException( editor,
|
||||
"<html><body width='175'><h3>Auxilary Decoders</h3>"
|
||||
+ "<p>The auxiliary decoders work with analog audio and are not "
|
||||
+ "compatible with the P25 Decoder. Please uncheck any auxiliary "
|
||||
+ "decoders that you have selected for this channel</p>" );
|
||||
}
|
||||
}
|
||||
}
|
||||
mComboModulation = new JComboBox<P25DecoderLSM.Modulation>();
|
||||
mComboModulation.setModel(new DefaultComboBoxModel<P25DecoderLSM.Modulation>(
|
||||
P25DecoderLSM.Modulation.values()));
|
||||
mComboModulation.setEnabled(false);
|
||||
mComboModulation.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
|
||||
private void setControlsEnabled( boolean enabled )
|
||||
{
|
||||
if( mComboModulation.isEnabled() != enabled )
|
||||
{
|
||||
mComboModulation.setEnabled( enabled );
|
||||
}
|
||||
|
||||
if( mTrafficChannelPoolSize.isEnabled() != enabled )
|
||||
{
|
||||
mTrafficChannelPoolSize.setEnabled( enabled );
|
||||
}
|
||||
add(new JLabel("Modulation:"));
|
||||
add(mComboModulation);
|
||||
|
||||
if( mIgnoreDataCalls.isEnabled() != enabled )
|
||||
{
|
||||
mIgnoreDataCalls.setEnabled( enabled );
|
||||
}
|
||||
}
|
||||
mIgnoreDataCalls = new JCheckBox("Ignore Data Calls");
|
||||
mIgnoreDataCalls.setEnabled(false);
|
||||
mIgnoreDataCalls.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void setItem( Channel item )
|
||||
{
|
||||
super.setItem( item );
|
||||
|
||||
if( hasItem() )
|
||||
{
|
||||
setControlsEnabled( true );
|
||||
|
||||
DecodeConfiguration config = getItem().getDecodeConfiguration();
|
||||
|
||||
if( config instanceof DecodeConfigP25Phase1 )
|
||||
{
|
||||
DecodeConfigP25Phase1 p25 = (DecodeConfigP25Phase1)config;
|
||||
|
||||
mComboModulation.setSelectedItem( p25.getModulation() );
|
||||
mIgnoreDataCalls.setSelected( p25.getIgnoreDataCalls() );
|
||||
mTrafficChannelPoolSize.setValue( p25.getTrafficChannelPoolSize() );
|
||||
setModified( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
mComboModulation.setSelectedItem( P25Decoder.Modulation.C4FM );
|
||||
mIgnoreDataCalls.setSelected( false );
|
||||
mTrafficChannelPoolSize.setValue( DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT );
|
||||
setModified( true );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setControlsEnabled( false );
|
||||
setModified( false );
|
||||
}
|
||||
}
|
||||
add(mIgnoreDataCalls);
|
||||
|
||||
mTrafficChannelPoolSize = new JSlider(JSlider.HORIZONTAL, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MINIMUM,
|
||||
DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MAXIMUM, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT);
|
||||
mTrafficChannelPoolSize.setEnabled(false);
|
||||
mTrafficChannelPoolSize.setMajorTickSpacing(10);
|
||||
mTrafficChannelPoolSize.setMinorTickSpacing(5);
|
||||
mTrafficChannelPoolSize.setPaintTicks(true);
|
||||
mTrafficChannelPoolSize.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
mTrafficChannelPoolSizeLabel.setText("Traffic Pool: " + mTrafficChannelPoolSize.getValue());
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
|
||||
mTrafficChannelPoolSizeLabel = new JLabel("Traffic Pool: " + mTrafficChannelPoolSize.getValue() + " ");
|
||||
|
||||
add(mTrafficChannelPoolSizeLabel);
|
||||
add(mTrafficChannelPoolSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if(hasItem() && isModified())
|
||||
{
|
||||
DecodeConfigP25Phase1 p25 = new DecodeConfigP25Phase1();
|
||||
|
||||
p25.setModulation((P25Decoder.Modulation)mComboModulation.getSelectedItem());
|
||||
p25.setIgnoreDataCalls(mIgnoreDataCalls.isSelected());
|
||||
p25.setTrafficChannelPoolSize(mTrafficChannelPoolSize.getValue());
|
||||
|
||||
getItem().setDecodeConfiguration(p25);
|
||||
}
|
||||
|
||||
setModified(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Editor<Channel> editor) throws EditorValidationException
|
||||
{
|
||||
/**
|
||||
* Enforce CQPSK modulation uses a Tuner Source for I/Q sample data
|
||||
*/
|
||||
if(editor instanceof SourceConfigurationEditor)
|
||||
{
|
||||
SourceConfiguration config = ((SourceConfigurationEditor)editor).getSourceConfiguration();
|
||||
|
||||
if(!(config instanceof SourceConfigTuner || config instanceof SourceConfigTunerMultipleFrequency))
|
||||
{
|
||||
throw new EditorValidationException(editor,
|
||||
"<html><body width='175'><h3>LSM Simulcast</h3>"
|
||||
+ "<p>P25 LSM Simulcast decoder can only be used with "
|
||||
+ "a tuner source. Please change the Source to use a tuner"
|
||||
+ " or change the P25 Decoder to C4FM modulation</p>");
|
||||
}
|
||||
}
|
||||
else if(editor instanceof AuxDecodeConfigurationEditor)
|
||||
{
|
||||
AuxDecodeConfiguration config = ((AuxDecodeConfigurationEditor)editor).getConfiguration();
|
||||
|
||||
if(config != null && !config.getAuxDecoders().isEmpty())
|
||||
{
|
||||
throw new EditorValidationException(editor,
|
||||
"<html><body width='175'><h3>Auxilary Decoders</h3>"
|
||||
+ "<p>The auxiliary decoders work with analog audio and are not "
|
||||
+ "compatible with the P25 Decoder. Please uncheck any auxiliary "
|
||||
+ "decoders that you have selected for this channel</p>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setControlsEnabled(boolean enabled)
|
||||
{
|
||||
if(mComboModulation.isEnabled() != enabled)
|
||||
{
|
||||
mComboModulation.setEnabled(enabled);
|
||||
}
|
||||
|
||||
if(mTrafficChannelPoolSize.isEnabled() != enabled)
|
||||
{
|
||||
mTrafficChannelPoolSize.setEnabled(enabled);
|
||||
}
|
||||
|
||||
if(mIgnoreDataCalls.isEnabled() != enabled)
|
||||
{
|
||||
mIgnoreDataCalls.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(Channel item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
if(hasItem())
|
||||
{
|
||||
setControlsEnabled(true);
|
||||
|
||||
DecodeConfiguration config = getItem().getDecodeConfiguration();
|
||||
|
||||
if(config instanceof DecodeConfigP25Phase1)
|
||||
{
|
||||
DecodeConfigP25Phase1 p25 = (DecodeConfigP25Phase1)config;
|
||||
|
||||
mComboModulation.setSelectedItem(p25.getModulation());
|
||||
mIgnoreDataCalls.setSelected(p25.getIgnoreDataCalls());
|
||||
mTrafficChannelPoolSize.setValue(p25.getTrafficChannelPoolSize());
|
||||
setModified(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
mComboModulation.setSelectedItem(P25Decoder.Modulation.C4FM);
|
||||
mIgnoreDataCalls.setSelected(false);
|
||||
mTrafficChannelPoolSize.setValue(DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT);
|
||||
setModified(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setControlsEnabled(false);
|
||||
setModified(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user