/*
* Kontalk Java client
* Copyright (C) 2016 Kontalk Devteam <devteam@kontalk.org>
*
* 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 org.kontalk.view;
import javax.swing.Box;
import javax.swing.JFrame;
import javax.swing.text.NumberFormatter;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.alee.extended.filechooser.WebFileChooserField;
import com.alee.extended.panel.GroupPanel;
import com.alee.extended.panel.GroupingType;
import com.alee.laf.button.WebButton;
import com.alee.laf.checkbox.WebCheckBox;
import com.alee.laf.combobox.WebComboBox;
import com.alee.laf.label.WebLabel;
import com.alee.laf.panel.WebPanel;
import com.alee.laf.rootpane.WebDialog;
import com.alee.laf.separator.WebSeparator;
import com.alee.laf.tabbedpane.WebTabbedPane;
import com.alee.laf.text.WebFormattedTextField;
import com.alee.laf.text.WebTextArea;
import com.alee.laf.text.WebTextField;
import com.alee.managers.tooltip.TooltipManager;
import org.kontalk.crypto.PersonalKey;
import org.kontalk.misc.KonException;
import org.kontalk.model.Account;
import org.kontalk.model.Model;
import org.kontalk.persistence.Config;
import org.kontalk.system.Control.ViewControl;
import org.kontalk.util.Tr;
/**
* Dialog for showing and changing all application options.
* @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>}
*/
final class ConfigurationDialog extends WebDialog {
private static final Logger LOGGER = Logger.getLogger(ConfigurationDialog.class.getName());
private final Config mConf = Config.getInstance();
private final View mView;
private final Model mModel;
ConfigurationDialog(JFrame owner, View view, Model model) {
super(owner);
mView = view;
mModel = model;
this.setTitle(Tr.tr("Preferences"));
this.setSize(550, 470);
this.setResizable(false);
this.setModal(true);
this.setLayout(new BorderLayout(View.GAP_SMALL, View.GAP_SMALL));
WebTabbedPane tabbedPane = new WebTabbedPane(WebTabbedPane.LEFT);
tabbedPane.setFontSize(View.FONT_SIZE_NORMAL);
final MainPanel mainPanel = new MainPanel();
final NetworkPanel networkPanel = new NetworkPanel();
final AccountPanel accountPanel = new AccountPanel();
final PrivacyPanel privacyPanel = new PrivacyPanel();
final ViewPanel viewPanel = new ViewPanel();
tabbedPane.addTab(Tr.tr("Main"), mainPanel);
tabbedPane.addTab(Tr.tr("Network"), networkPanel);
tabbedPane.addTab(Tr.tr("Account"), accountPanel);
tabbedPane.addTab(Tr.tr("Privacy"), privacyPanel);
tabbedPane.addTab(Tr.tr("View"), viewPanel);
this.add(tabbedPane, BorderLayout.CENTER);
// buttons
WebButton cancelButton = new WebButton(Tr.tr("Cancel"));
cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
ConfigurationDialog.this.dispose();
}
});
WebButton saveButton = new WebButton(Tr.tr("Save"));
saveButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
mainPanel.saveConfiguration();
accountPanel.saveConfiguration();
privacyPanel.saveConfiguration();
networkPanel.saveConfiguration();
viewPanel.saveConfiguration();
// better save twice than never
mConf.saveToFile();
ConfigurationDialog.this.dispose();
}
});
GroupPanel buttonPanel = new GroupPanel(saveButton, cancelButton);
buttonPanel.setLayout(new FlowLayout(FlowLayout.TRAILING));
this.add(buttonPanel, BorderLayout.SOUTH);
}
private class MainPanel extends WebPanel {
private final WebCheckBox mTrayBox;
private final WebCheckBox mCloseTrayBox;
private final WebCheckBox mEnterSendsBox;
private final WebCheckBox mShowUserBox;
private final WebCheckBox mHideBlockedBox;
MainPanel() {
GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false);
groupPanel.setMargin(View.MARGIN_BIG);
groupPanel.add(new WebLabel(Tr.tr("Main Settings")).setBoldFont());
groupPanel.add(new WebSeparator(true, true));
mTrayBox = createCheckBox(Tr.tr("Show tray icon"),
"",
mConf.getBoolean(Config.MAIN_TRAY));
mTrayBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
mCloseTrayBox.setEnabled(e.getStateChange() == ItemEvent.SELECTED);
}
});
mCloseTrayBox = createCheckBox(Tr.tr("Close to tray"),
"",
mConf.getBoolean(Config.MAIN_TRAY_CLOSE));
mCloseTrayBox.setEnabled(mTrayBox.isSelected());
groupPanel.add(new GroupPanel(View.GAP_DEFAULT, mTrayBox, mCloseTrayBox));
mEnterSendsBox = createCheckBox(Tr.tr("Enter key sends"),
Tr.tr("Enter key sends text, Control+Enter adds new line - or vice versa"),
mConf.getBoolean(Config.MAIN_ENTER_SENDS));
groupPanel.add(new GroupPanel(mEnterSendsBox, new WebSeparator()));
mShowUserBox = createCheckBox(Tr.tr("Display yourself in contacts"),
Tr.tr("Display yourself in the contact list"),
mConf.getBoolean(Config.VIEW_USER_CONTACT));
groupPanel.add(new GroupPanel(mShowUserBox, new WebSeparator()));
mHideBlockedBox = createCheckBox(Tr.tr("Hide blocked contacts"),
Tr.tr("Hide blocked contacts in the contact list"),
mConf.getBoolean(Config.VIEW_HIDE_BLOCKED));
groupPanel.add(new GroupPanel(mHideBlockedBox, new WebSeparator()));
this.add(groupPanel);
}
private void saveConfiguration() {
mConf.setProperty(Config.MAIN_TRAY, mTrayBox.isSelected());
mConf.setProperty(Config.MAIN_TRAY_CLOSE, mCloseTrayBox.isSelected());
mView.updateTray();
mConf.setProperty(Config.MAIN_ENTER_SENDS, mEnterSendsBox.isSelected());
mView.setHotkeys();
mConf.setProperty(Config.VIEW_USER_CONTACT, mShowUserBox.isSelected());
mConf.setProperty(Config.VIEW_HIDE_BLOCKED, mHideBlockedBox.isSelected());
mView.updateContactList();
}
}
private class NetworkPanel extends WebPanel {
private final WebCheckBox mConnectStartupBox;
private final WebCheckBox mConnectRetryBox;
private final WebCheckBox mRequestAvatars;
private final WebComboBox mMaxImgSizeBox;
private final LinkedHashMap<Integer, String> mImgResizeMap;
public NetworkPanel() {
GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false);
groupPanel.setMargin(View.MARGIN_BIG);
groupPanel.add(new WebLabel(Tr.tr("Network Settings")).setBoldFont());
groupPanel.add(new WebSeparator(true, true));
mConnectStartupBox = createCheckBox(Tr.tr("Connect on startup"),
"",
mConf.getBoolean(Config.MAIN_CONNECT_STARTUP));
groupPanel.add(mConnectStartupBox);
mConnectRetryBox = createCheckBox(Tr.tr("Retry on connection failure"),
Tr.tr("Try automatic (re-)connect after connection failure"),
mConf.getBoolean(Config.NET_RETRY_CONNECT));
groupPanel.add(new GroupPanel(mConnectRetryBox, new WebSeparator()));
mRequestAvatars = createCheckBox(Tr.tr("Download profile pictures"),
Tr.tr("Download the profile pictures of your contacts"),
mConf.getBoolean(Config.NET_REQUEST_AVATARS));
groupPanel.add(new GroupPanel(mRequestAvatars, new WebSeparator()));
mImgResizeMap = new LinkedHashMap<>();
mImgResizeMap.put(-1, Tr.tr("Original"));
mImgResizeMap.put(300 * 1000, Tr.tr("Small (0.3MP)"));
mImgResizeMap.put(500 * 1000, Tr.tr("Medium (0.5MP)"));
mImgResizeMap.put(800 * 1000, Tr.tr("Large (0.8MP)"));
mMaxImgSizeBox = new WebComboBox(new ArrayList<>(mImgResizeMap.values()).toArray());
int maxImgIndex = new ArrayList<>(mImgResizeMap.keySet()).indexOf(
mConf.getInt(Config.NET_MAX_IMG_SIZE));
if (maxImgIndex >= 0)
mMaxImgSizeBox.setSelectedIndex(maxImgIndex);
TooltipManager.addTooltip(mMaxImgSizeBox, Tr.tr("Reduce size of images before sending"));
groupPanel.add(new GroupPanel(View.GAP_DEFAULT,
new WebLabel(Tr.tr("Resize image attachments:")),
mMaxImgSizeBox,
new WebSeparator()));
this.add(groupPanel);
}
private void saveConfiguration() {
mConf.setProperty(Config.MAIN_CONNECT_STARTUP, mConnectStartupBox.isSelected());
mConf.setProperty(Config.NET_RETRY_CONNECT, mConnectRetryBox.isSelected());
mConf.setProperty(Config.NET_REQUEST_AVATARS, mRequestAvatars.isSelected());
mConf.setProperty(Config.NET_MAX_IMG_SIZE,
new ArrayList<>(mImgResizeMap.keySet()).get(mMaxImgSizeBox.getSelectedIndex()));
}
}
private class AccountPanel extends WebPanel {
private final WebTextField mServerField;
private final WebFormattedTextField mPortField;
private final WebCheckBox mDisableCertBox;
private final WebTextField mUserIDField;
private final WebTextArea mFingerprintArea;
AccountPanel() {
GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false);
groupPanel.setMargin(View.MARGIN_BIG);
groupPanel.add(new WebLabel(Tr.tr("Account Configuration")).setBoldFont());
groupPanel.add(new WebSeparator(true, true));
// server text field
groupPanel.add(new WebLabel(Tr.tr("Server address:")));
WebPanel serverPanel = new WebPanel(false);
mServerField = new WebTextField(mConf.getString(Config.SERV_HOST));
mServerField.setInputPrompt(Config.DEFAULT_SERV_HOST);
mServerField.setInputPromptFont(mServerField.getFont().deriveFont(Font.ITALIC));
mServerField.setHideInputPromptOnFocus(false);
serverPanel.add(mServerField);
int port = mConf.getInt(Config.SERV_PORT, Config.DEFAULT_SERV_PORT);
NumberFormat format = new DecimalFormat("#####");
NumberFormatter formatter = new NumberFormatter(format);
formatter.setMinimum(1);
formatter.setMaximum(65535);
mPortField = new WebFormattedTextField(formatter);
mPortField.setColumns(4);
mPortField.setValue(port);
serverPanel.add(new GroupPanel(new WebLabel(" "+Tr.tr("Port:")), mPortField),
BorderLayout.EAST);
groupPanel.add(serverPanel);
mDisableCertBox = createCheckBox(Tr.tr("Disable certificate validation"),
Tr.tr("Disable SSL certificate server validation"),
!mConf.getBoolean(Config.SERV_CERT_VALIDATION));
groupPanel.add(new GroupPanel(mDisableCertBox, new WebSeparator()));
groupPanel.add(Box.createVerticalStrut(View.GAP_BIG));
groupPanel.add(new WebLabel(Tr.tr("Personal Key")).setBoldFont());
groupPanel.add(new WebSeparator(true, true));
mUserIDField = new ComponentUtils.LabelTextField(View.MAX_USER_ID_LENGTH, this);
groupPanel.add(new GroupPanel(View.GAP_DEFAULT,
new WebLabel(Tr.tr("User ID:")),
mUserIDField));
WebLabel fpLabel = new WebLabel(Tr.tr("Fingerprint:")+" ");
fpLabel.setAlignmentY(Component.TOP_ALIGNMENT);
GroupPanel fpLabelPanel = new GroupPanel(false, fpLabel, Box.createGlue());
mFingerprintArea = Utils.createFingerprintArea();
this.updateKey();
groupPanel.add(new GroupPanel(View.GAP_DEFAULT, fpLabelPanel, mFingerprintArea));
final WebButton passButton = new WebButton(getPassTitle(mModel.account()));
passButton.setEnabled(mModel.account().isPresent());
passButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
WebDialog passDialog = createPassDialog(
ConfigurationDialog.this,
mModel.account(),
mView.getControl());
passDialog.setVisible(true);
passButton.setText(getPassTitle(mModel.account()));
}
});
groupPanel.add(passButton);
WebButton importButton = new WebButton(Tr.tr("Import new Account"));
importButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
mView.showImportWizard(false);
AccountPanel.this.updateKey();
passButton.setText(getPassTitle(mModel.account()));
passButton.setEnabled(mModel.account().isPresent());
}
});
groupPanel.add(importButton);
this.add(groupPanel, BorderLayout.CENTER);
WebButton okButton = new WebButton(Tr.tr("Save & Connect"));
okButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
AccountPanel.this.saveConfiguration();
ConfigurationDialog.this.dispose();
mView.getControl().connect();
}
});
GroupPanel buttonPanel = new GroupPanel(okButton);
buttonPanel.setLayout(new FlowLayout(FlowLayout.TRAILING));
this.add(buttonPanel, BorderLayout.SOUTH);
}
private void updateKey() {
PersonalKey key = mModel.account().getPersonalKey().orElse(null);
String uid = key != null ? key.getUserId() : null;
mUserIDField.setText(uid != null ? uid : "- " + Tr.tr("No key loaded") + " -");
mUserIDField.setCaretPosition(0); // "scroll" back
if (uid != null)
TooltipManager.addTooltip(mUserIDField, uid);
mFingerprintArea.setText(key != null ? Utils.fingerprint(key.getFingerprint()) : "---");
}
private void saveConfiguration() {
mConf.setProperty(Config.SERV_HOST, mServerField.getText());
int port = Integer.parseInt(mPortField.getText());
mConf.setProperty(Config.SERV_PORT, port);
mConf.setProperty(Config.SERV_CERT_VALIDATION, !mDisableCertBox.isSelected());
}
}
private class PrivacyPanel extends WebPanel {
private final WebCheckBox mChatStateBox;
private final WebCheckBox mRosterNameBox;
private final WebCheckBox mSubscriptionBox;
PrivacyPanel() {
GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false);
groupPanel.setMargin(View.MARGIN_BIG);
groupPanel.add(new WebLabel(Tr.tr("Privacy Settings")).setBoldFont());
groupPanel.add(new WebSeparator(true, true));
mSubscriptionBox = createCheckBox(Tr.tr("Automatically grant authorization"),
Tr.tr("Automatically grant online status authorization requests from other users"),
mConf.getBoolean(Config.NET_AUTO_SUBSCRIPTION));
groupPanel.add(new GroupPanel(mSubscriptionBox, new WebSeparator()));
mChatStateBox = createCheckBox(Tr.tr("Send chatstate notification"),
Tr.tr("Send chat activity (typing,…) to other users"),
mConf.getBoolean(Config.NET_SEND_CHAT_STATE));
groupPanel.add(new GroupPanel(mChatStateBox, new WebSeparator()));
mRosterNameBox = createCheckBox(Tr.tr("Upload contact names"),
Tr.tr("Upload your contact names to the server for client synchronization"),
mConf.getBoolean(Config.NET_SEND_ROSTER_NAME));
groupPanel.add(new GroupPanel(mRosterNameBox, new WebSeparator()));
this.add(groupPanel);
}
private void saveConfiguration() {
mConf.setProperty(Config.NET_SEND_CHAT_STATE, mChatStateBox.isSelected());
mConf.setProperty(Config.NET_SEND_ROSTER_NAME, mRosterNameBox.isSelected());
mConf.setProperty(Config.NET_AUTO_SUBSCRIPTION, mSubscriptionBox.isSelected());
}
}
private class ViewPanel extends WebPanel {
private final WebCheckBox mBGBox;
private final WebFileChooserField mBGChooser;
private final WebComboBox mMessageFontSizeBox;
private final LinkedHashMap<String, Integer> mMessageFontSizeMap;
ViewPanel() {
GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false);
groupPanel.setMargin(View.MARGIN_BIG);
groupPanel.add(new WebLabel(Tr.tr("View Settings")).setBoldFont());
groupPanel.add(new WebSeparator(true, true));
String bgPath = mConf.getString(Config.VIEW_CHAT_BG);
mBGBox = createCheckBox(Tr.tr("Custom background:")+" ",
"",
!bgPath.isEmpty());
mBGBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
mBGChooser.setEnabled(e.getStateChange() == ItemEvent.SELECTED);
mBGChooser.getChooseButton().setEnabled(e.getStateChange() == ItemEvent.SELECTED);
}
});
TooltipManager.addTooltip(mBGBox, Tr.tr("Background image for all chats"));
mBGChooser = Utils.createImageChooser(bgPath);
mBGChooser.setEnabled(mBGBox.isSelected());
groupPanel.add(new GroupPanel(GroupingType.fillLast, mBGBox, mBGChooser));
mMessageFontSizeMap = new LinkedHashMap<>();
mMessageFontSizeMap.put(Tr.tr("Small"), 1);
mMessageFontSizeMap.put(Tr.tr("Normal"), -1);
mMessageFontSizeMap.put(Tr.tr("Large"), 2);
mMessageFontSizeMap.put(Tr.tr("Huge"), 3);
mMessageFontSizeBox = new WebComboBox(new ArrayList<>(mMessageFontSizeMap.keySet()).toArray());
int fontSizeIndex = new ArrayList<>(mMessageFontSizeMap.values()).indexOf(
mConf.getInt(Config.VIEW_MESSAGE_FONT_SIZE));
if (fontSizeIndex >= 0)
mMessageFontSizeBox.setSelectedIndex(fontSizeIndex);
TooltipManager.addTooltip(mMessageFontSizeBox, Tr.tr("Font size for message text and date"));
groupPanel.add(new GroupPanel(View.GAP_DEFAULT,
new WebLabel(Tr.tr("Font size for chat messages:")),
mMessageFontSizeBox,
new WebSeparator()));
this.add(groupPanel);
}
private void saveConfiguration() {
String bgPath;
if (mBGBox.isSelected() && !mBGChooser.getSelectedFiles().isEmpty()) {
bgPath = mBGChooser.getSelectedFiles().get(0).getAbsolutePath();
} else {
bgPath = "";
}
String oldBGPath = mConf.getString(Config.VIEW_CHAT_BG);
if (!bgPath.equals(oldBGPath)) {
mConf.setProperty(Config.VIEW_CHAT_BG, bgPath);
mView.reloadChatBG();
}
Integer value = mMessageFontSizeMap.get(mMessageFontSizeBox.getSelectedItem());
// first term should always be true
if (value != null && !value.equals(mConf.getInt(Config.VIEW_MESSAGE_FONT_SIZE))) {
mConf.setProperty(Config.VIEW_MESSAGE_FONT_SIZE, value);
mView.updateMessageLists();
}
}
}
private static WebCheckBox createCheckBox(String title, String tooltip, boolean selected) {
WebCheckBox checkBox = new WebCheckBox(Tr.tr(title));
checkBox.setAnimated(false);
checkBox.setSelected(selected);
if (!tooltip.isEmpty())
TooltipManager.addTooltip(checkBox, tooltip);
return checkBox;
}
private static String getPassTitle(Account account) {
return account.isPasswordProtected() ?
Tr.tr("Change key password") :
Tr.tr("Set key password");
}
private static WebDialog createPassDialog(WebDialog parent, Account account, ViewControl control) {
final WebDialog passDialog = new WebDialog(parent, getPassTitle(account), true);
passDialog.setLayout(new BorderLayout(View.GAP_DEFAULT, View.GAP_DEFAULT));
passDialog.setResizable(false);
final WebButton saveButton = new WebButton(Tr.tr("Save"));
boolean passSet = account.isPasswordProtected();
final ComponentUtils.PassPanel passPanel = new ComponentUtils.PassPanel(passSet) {
@Override
void onValidInput() {
saveButton.setEnabled(true);
}
@Override
void onInvalidInput() {
saveButton.setEnabled(false);
}
};
passDialog.add(passPanel, BorderLayout.CENTER);
saveButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
char[] oldPassword = passPanel.getOldPassword();
char[] newPassword = passPanel.getNewPassword().orElse(null);
if (newPassword == null) {
LOGGER.warning("can't get new password");
return;
}
try {
control.setAccountPassword(oldPassword, newPassword);
} catch(KonException ex) {
LOGGER.log(Level.WARNING, "can't set new password", ex);
if (ex.getError() == KonException.Error.CHANGE_PASS_COPY)
passPanel.showWrongPassword();
return;
}
passDialog.dispose();
}
});
WebButton cancelButton = new WebButton(Tr.tr("Cancel"));
cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
passDialog.dispose();
}
});
GroupPanel buttonPanel = new GroupPanel(2, saveButton, cancelButton);
buttonPanel.setLayout(new FlowLayout(FlowLayout.TRAILING));
passDialog.add(buttonPanel, BorderLayout.SOUTH);
passDialog.pack();
return passDialog;
}
}