/* * Copyright (C) 2005-2012 NAUMEN. All rights reserved. * * This file may be distributed and/or modified under the terms of the * GNU General Public License version 2 as published by the Free Software * Foundation and appearing in the file LICENSE.GPL included in the * packaging of this file. * */ package ru.naumen.servacc.ui; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.DragSource; import org.eclipse.swt.dnd.DragSourceAdapter; import org.eclipse.swt.dnd.DragSourceEvent; import org.eclipse.swt.dnd.DropTarget; import org.eclipse.swt.dnd.DropTargetAdapter; import org.eclipse.swt.dnd.DropTargetEvent; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.ShellEvent; import org.eclipse.swt.events.ShellListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; import org.eclipse.swt.widgets.TrayItem; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.naumen.servacc.Backend; import ru.naumen.servacc.GlobalThroughView; import ru.naumen.servacc.HTTPProxy; import ru.naumen.servacc.MessageListener; import ru.naumen.servacc.activechannel.ActiveChannelsRegistry; import ru.naumen.servacc.activechannel.i.ActiveChannelsObserver; import ru.naumen.servacc.activechannel.i.IActiveChannel; import ru.naumen.servacc.activechannel.i.IActiveChannelThrough; import ru.naumen.servacc.config2.Account; import ru.naumen.servacc.config2.Group; import ru.naumen.servacc.config2.HTTPAccount; import ru.naumen.servacc.config2.Path; import ru.naumen.servacc.config2.SSHAccount; import ru.naumen.servacc.config2.i.IConfig; import ru.naumen.servacc.config2.i.IConfigItem; import ru.naumen.servacc.exception.ServerAccessException; import ru.naumen.servacc.platform.GUIOptions; import ru.naumen.servacc.settings.ListProvider; import ru.naumen.servacc.util.Util; public class UIController implements GlobalThroughView, ActiveChannelsObserver { private static final Logger LOGGER = LoggerFactory.getLogger(UIController.class); private final Shell shell; private final MessageListener synchronousAlert; private final MessageListener asynchronousAlert; private final ActiveChannelsRegistry acRegistry; private Clipboard clipboard; private Backend backend; private ExecutorService executor; private HTTPProxy httpProxy; private ConfigLoader configLoader; private IConfig config; private FilteredTree filteredTree; private ToolItem toolitemConnect; private ToolItem toolitemPortForwarding; private ToolItem toolitemFTP; private ToolItem toolItemProxy; private ToolItem toolitemCopy; private Label globalThrough; private Button clearGlobalThrough; private TreeItemController root; private TreeItemController selection; private Timer refreshTimer; public UIController(Shell shell, GUIOptions guiOptions, Backend backend, ExecutorService executor, HTTPProxy httpProxy, ListProvider sourceListProvider, ActiveChannelsRegistry acRegistry) { this.acRegistry = acRegistry; this.acRegistry.setObserver(this); this.shell = shell; this.clipboard = new Clipboard(shell.getDisplay()); this.backend = backend; this.executor = executor; this.synchronousAlert = new SynchronousAlert(shell); this.asynchronousAlert = new AsynchronousProxy(synchronousAlert); this.configLoader = new ConfigLoader(shell, sourceListProvider, synchronousAlert, acRegistry); this.httpProxy = httpProxy; backend.setGlobalThroughView(this); createToolBar(); createFilteredTree(guiOptions.useSystemSearchWidget); createGlobalThroughWidget(); if (guiOptions.isTraySupported) { createTrayItem(); } else if (guiOptions.isAppMenuSupported) { createAppMenu(); } // set focus to search widget on startup filteredTree.focusOnFilterField(); } public void start() { shell.open(); reloadConfig(); } private void reloadConfig() { try { config = configLoader.loadConfig(); buildTree(config); updateTree(filteredTree.getText()); backend.refresh(config); } catch (Exception e) { LOGGER.error("Cannot reload config", e); showAlert(e.getLocalizedMessage()); } } private void showAlert(String text) { synchronousAlert.notify(text); } private void showAlertAsync(String message) { asynchronousAlert.notify(message); } private void createToolBar() { ToolBar toolbar = new ToolBar(shell, SWT.FLAT | SWT.RIGHT); toolitemConnect = new ToolItem(toolbar, SWT.PUSH); toolitemConnect.setText("Connect"); toolitemConnect.setImage(ImageCache.getImage("/icons/lightning.png")); toolitemConnect.setEnabled(false); toolitemPortForwarding = new ToolItem(toolbar, SWT.PUSH); toolitemPortForwarding.setText("Port Forwarding"); toolitemPortForwarding.setImage(ImageCache.getImage("/icons/arrow-curve.png")); toolitemPortForwarding.setEnabled(false); toolitemFTP = new ToolItem(toolbar, SWT.PUSH); toolitemFTP.setText("FTP"); toolitemFTP.setImage(ImageCache.getImage("/icons/drive-network.png")); toolitemFTP.setEnabled(false); toolItemProxy = new ToolItem(toolbar, SWT.PUSH); toolItemProxy.setText("HTTP Proxy"); toolItemProxy.setImage(ImageCache.getImage("/icons/earth.png")); toolItemProxy.setEnabled(false); toolitemCopy = new ToolItem(toolbar, SWT.PUSH); toolitemCopy.setText("Copy Password"); toolitemCopy.setImage(ImageCache.getImage("/icons/document-copy.png")); toolitemCopy.setEnabled(false); new ToolItem(toolbar, SWT.SEPARATOR); final ToolItem toolitemReloadConfig = new ToolItem(toolbar, SWT.PUSH); toolitemReloadConfig.setText("Reload Accounts"); toolitemReloadConfig.setImage(ImageCache.getImage("/icons/arrow-circle-double.png")); toolbar.pack(); // Events toolitemConnect.addSelectionListener(new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { } @Override public void widgetSelected(SelectionEvent e) { defaultActionRequested(getSelectedTreeItem()); } }); toolitemPortForwarding.addSelectionListener(new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { } @Override public void widgetSelected(SelectionEvent e) { portForwardingRequested(getSelectedTreeItem()); } }); toolitemFTP.addSelectionListener(new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { } @Override public void widgetSelected(SelectionEvent e) { ftpConnectionRequested(getSelectedTreeItem()); } }); toolItemProxy.addSelectionListener(new SelectionListener() { @Override public void widgetSelected(SelectionEvent selectionEvent) { httpProxySetupRequested(getSelectedTreeItem()); } @Override public void widgetDefaultSelected(SelectionEvent selectionEvent) { } }); toolitemCopy.addSelectionListener(new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { } @Override public void widgetSelected(SelectionEvent e) { passwordCopyRequested(getSelectedTreeItem()); } }); toolitemReloadConfig.addSelectionListener(new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { } @Override public void widgetSelected(SelectionEvent e) { reloadConfig(); } }); } private void createFilteredTree(boolean useSystemSearchWidget) { filteredTree = new FilteredTree(shell, useSystemSearchWidget); // Selection handling filteredTree.getTree().addSelectionListener(new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { Object item = e.item; if (item == null) { item = getSelectedTreeItem(); } defaultActionRequested((TreeItem) item); } @Override public void widgetSelected(SelectionEvent e) { TreeItem item = (TreeItem) e.item; if (item != null) { itemSelected(item); final IConfigItem configItem = getConfigTreeItem(item).getData(); toolitemConnect.setEnabled(configItem.isConnectable()); toolitemPortForwarding.setEnabled(configItem.isPortForwarder()); toolitemFTP.setEnabled(configItem.isFtpBrowseable()); toolItemProxy.setEnabled(configItem.isPortForwarder()); toolitemCopy.setEnabled(configItem.isAccount()); } } }); // Filter events filteredTree.getFilter().addModifyListener(event -> {scheduleRefresh(filteredTree.getText());}); shell.getDisplay().addFilter(SWT.KeyDown, event -> { // Focus on Ctrl+F if (event.stateMask == SWT.CTRL && event.keyCode == (int) 'f') { filteredTree.focusOnFilterField(); } }); // Drag source DragSource ds = new DragSource(filteredTree.getTree(), DND.DROP_MOVE); ds.setTransfer(new Transfer[]{ TextTransfer.getInstance() }); ds.addDragListener(new DragSourceAdapter() { @Override public void dragSetData(DragSourceEvent event) { event.doit = false; TreeItem[] treeSelection = filteredTree.getTree().getSelection(); if (treeSelection.length == 1) { TreeItemController tic = getConfigTreeItem(treeSelection[0]); if (tic.getData() instanceof SSHAccount) { event.data = ((SSHAccount) tic.getData()).getUniqueIdentity(); event.doit = true; } } } }); } private void createGlobalThroughWidget() { Composite widget = new Composite(shell, SWT.NONE); widget.setLayout(new GridLayout(3, false)); widget.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.GRAB_HORIZONTAL)); Label label = new Label(widget, SWT.NONE); label.setText("Connect all through:"); globalThrough = new Label(widget, SWT.NONE); globalThrough.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.GRAB_HORIZONTAL)); clearGlobalThrough = new Button(widget, SWT.NONE); clearGlobalThrough.setText("clear"); clearGlobalThrough.addSelectionListener(new SelectionListener() { @Override public void widgetSelected(SelectionEvent e) { backend.clearGlobalThrough(); } @Override public void widgetDefaultSelected(SelectionEvent e) { } }); backend.clearGlobalThrough(); // Drop target DropTarget dt = new DropTarget(widget, DND.DROP_MOVE); dt.setTransfer(new Transfer[]{ TextTransfer.getInstance() }); dt.addDropListener(new DropTargetAdapter() { @Override public void drop(DropTargetEvent event) { if (event.data != null) { backend.selectNewGlobalThrough((String) event.data, config); } } }); } @Override public void setGlobalThroughWidget(String globalThroughText) { final Color fg = shell.getDisplay().getSystemColor(SWT.COLOR_WIDGET_FOREGROUND); globalThrough.setText(globalThroughText); globalThrough.setForeground(fg); clearGlobalThrough.setVisible(true); } @Override public void clearGlobalThroughWidget() { final Color gray = shell.getDisplay().getSystemColor(SWT.COLOR_GRAY); globalThrough.setText("(drop an account here)"); globalThrough.setForeground(gray); clearGlobalThrough.setVisible(false); } private void createTrayItem() { TrayItem trayItem = new TrayItem(shell.getDisplay().getSystemTray(), SWT.NULL); trayItem.setImage(ImageCache.getImage("/prog.ico", 1)); trayItem.setVisible(true); // set up hide-to-tray behavior trayItem.addSelectionListener(new SelectionListener() { @Override public void widgetSelected(SelectionEvent e) { shell.setVisible(true); shell.setFocus(); } @Override public void widgetDefaultSelected(SelectionEvent e) { } }); shell.addShellListener(new ShellListener() { @Override public void shellIconified(ShellEvent e) { } @Override public void shellDeiconified(ShellEvent e) { } @Override public void shellDeactivated(ShellEvent e) { } @Override public void shellClosed(ShellEvent e) { e.doit = false; shell.setVisible(false); } @Override public void shellActivated(ShellEvent e) { } }); final Menu menu = new Menu(shell, SWT.POP_UP); trayItem.addListener(SWT.MenuDetect, event -> {menu.setVisible(true);}); addFileMenuActions(menu); } private void createAppMenu() { Menu menuBar = Display.getCurrent().getMenuBar(); if (menuBar == null) { menuBar = new Menu(shell, SWT.BAR); shell.setMenuBar(menuBar); } final MenuItem file = new MenuItem(menuBar, SWT.CASCADE); final Menu dropdown = new Menu(menuBar); file.setText("File"); file.setMenu(dropdown); addFileMenuActions(dropdown); } private void addFileMenuActions(Menu menu) { // tray menu items to encrypt/decrypt local accounts createEncryptMenuItem(menu); createDecryptMenuItem(menu); new MenuItem(menu, SWT.SEPARATOR); // tray menu item which is the only way to quit on windows MenuItem itemQuit = new MenuItem(menu, SWT.NULL); itemQuit.setText("Quit"); itemQuit.addListener(SWT.Selection, event -> {shell.dispose();}); } private void createEncryptMenuItem(final Menu menu) { final MenuItem itemEncrypt = new MenuItem(menu, SWT.NULL); itemEncrypt.setText("Encrypt Local Accounts"); itemEncrypt.addListener(SWT.Selection, event -> {configLoader.encryptLocalAccounts();}); } private void createDecryptMenuItem(final Menu menu) { final MenuItem itemDecrypt = new MenuItem(menu, SWT.NULL); itemDecrypt.setText("Decrypt Local Accounts"); itemDecrypt.addListener(SWT.Selection, event -> {configLoader.decryptLocalAccounts();}); } // Event handlers private void itemSelected(TreeItem item) { selection = getConfigTreeItem(item); } private void defaultActionRequested(TreeItem item) { final TreeItemController tic = getConfigTreeItem(item); try { final IConfigItem data = tic.getData(); if (data instanceof SSHAccount) { Future<?> connectionFuture = this.executor.submit(() -> { try { SSHAccount account = (SSHAccount) data; Path path = Path.find(config, account.getUniqueIdentity()); backend.openSSHAccount(account, path.path()); } catch (Exception e) { LOGGER.error("Cannot open SSH account", e); String message = e.getMessage() == null ? "Cannot open SSH account" : e.getMessage(); showAlertAsync(message); } }); this.executor.execute(new WaitForConnectionTask(item, (SSHAccount)data, connectionFuture)); } else if (data instanceof HTTPAccount) { Future<?> connectionFuture = this.executor.submit(() -> { try { backend.openHTTPAccount((HTTPAccount) data); } catch (Exception e) { LOGGER.error("Cannot open HTTP account", e); showAlertAsync(e.getMessage()); } }); this.executor.execute(new WaitForConnectionTask(item, (HTTPAccount)data, connectionFuture)); } else if (data instanceof Group || data instanceof IActiveChannel) { boolean expandedState = tic.isExpanded(); item.setExpanded(!expandedState); tic.setExpanded(!expandedState); scheduleRefresh(filteredTree.getText()); } else { throw new ServerAccessException("Unknown account type"); } } catch (Exception ex) { LOGGER.error("Unexpected error", ex); showAlert(ex.getMessage()); } } private void portForwardingRequested(TreeItem item) { TreeItemController tic = getConfigTreeItem(item); if (tic.getData() instanceof SSHAccount) { try { PortForwardingDialog dialog = new PortForwardingDialog(shell); if (dialog.show()) { backend.localPortForward((SSHAccount) tic.getData(), dialog.getLocalHost(), dialog.getLocalPort(), dialog.getRemoteHost(), dialog.getRemotePort()); } } catch (Exception e) { LOGGER.error("Cannot forward port", e); showAlert(e.getMessage()); } } } private void ftpConnectionRequested(TreeItem item) { TreeItemController tic = getConfigTreeItem(item); if (tic.getData() instanceof SSHAccount) { try { backend.browseViaFTP((SSHAccount) tic.getData()); } catch (Exception e) { LOGGER.error("Cannot open FTP connection", e); showAlert(e.getMessage()); } } } private void httpProxySetupRequested(TreeItem item) { TreeItemController tic = getConfigTreeItem(item); if (tic.getData() instanceof SSHAccount) { ProxySetupDialog dialog = new ProxySetupDialog(shell); dialog.show(); httpProxy.setProxyOn((SSHAccount) tic.getData(), dialog.getPort(), asynchronousAlert); // TODO: display proxy status somewhere } } private void passwordCopyRequested(TreeItem item) { if (getConfigTreeItem(item).getData().isAccount()) { TreeItemController tic = getConfigTreeItem(item); try { final String password = ((Account) tic.getData()).getPassword(); clipboard.setContents( new Object[]{ password }, new Transfer[]{ TextTransfer.getInstance() }); } catch (Exception e) { LOGGER.error("Cannot copy password", e); } } } // Build tree structure data from config private void buildTree(IConfig config) { root = new TreeItemController(); for (IConfigItem item : config.getChildren()) { buildBranch(root, item); } } private void buildBranch(TreeItemController parent, IConfigItem config) { TreeItemController newTreeItem = new TreeItemController(parent, config); if (config instanceof Group) { for (IConfigItem configItem : ((Group) config).getChildren()) { buildBranch(newTreeItem, configItem); } } else if (config instanceof IActiveChannelThrough) { for (IActiveChannel configItem : ((IActiveChannelThrough) config).getChildren()) { buildBranch(newTreeItem, configItem); } } parent.getChildren().add(newTreeItem); } // Fill tree with data private void updateTree(String filter) { List<String> filters = new ArrayList<>(); if (!Util.isEmptyOrNull(filter)) { for (String substr : filter.split(" ")) { substr = substr.trim(); if (substr.length() > 0) { filters.add(substr); } } } updateBranch(root, filters); Tree tree = filteredTree.getTree(); tree.setRedraw(false); tree.removeAll(); for (TreeItemController item : root.getChildren()) { if (item.isVisible()) { createTreeItem(tree, item); } } // traverse tree and update expanded state updateExpandedState(tree); tree.setRedraw(true); } private void updateBranch(TreeItemController item, Collection<String> filters) { if (!filters.isEmpty()) { if (item.matches(filters)) { item.setVisibility(true); item.raiseVisibility(); } else { item.setVisibility(false); } } else { item.setVisibility(true); } for (TreeItemController child : item.getChildren()) { updateBranch(child, filters); } } private void updateExpandedState(Tree tree) { for (TreeItem child : tree.getItems()) { updateExpandedState(child); } } private void updateExpandedState(TreeItem item) { TreeItemController data = getConfigTreeItem(item); if (!data.isVisible()) { return; } item.setExpanded(data.isExpanded()); // update selection if (data.equals(selection)) { item.getParent().setSelection(item); } for (TreeItem child : item.getItems()) { updateExpandedState(child); } } private void createTreeItem(Tree parent, TreeItemController tic) { TreeItem treeItem = new TreeItem(parent, SWT.NONE); setupTreeItem(treeItem, tic); } private void createTreeItem(TreeItem parent, TreeItemController tic) { TreeItem treeItem = new TreeItem(parent, SWT.NONE); setupTreeItem(treeItem, tic); } private void setupTreeItem(TreeItem treeItem, TreeItemController tic) { treeItem.setData(tic); treeItem.setText(tic.toString()); String imageName = tic.getImageName(); if (imageName != null) { treeItem.setImage(ImageCache.getImage(imageName)); } for (TreeItemController child : tic.getChildren()) { if (child.isVisible()) { createTreeItem(treeItem, child); } } } private static TreeItemController getConfigTreeItem(TreeItem item) { if (item != null) { Object data = item.getData(); if (data instanceof TreeItemController) { return (TreeItemController) data; } } return new TreeItemController(); } private TreeItem getSelectedTreeItem() { TreeItem[] treeSelection = filteredTree.getTree().getSelection(); if ((treeSelection.length) == 1) { return treeSelection[0]; } return null; } // Refresh tree private void scheduleRefresh(final String filter) { scheduleRefresh(filter, 300); } private void scheduleRefresh(final String filter, long delay) { if (refreshTimer != null) { refreshTimer.cancel(); } TimerTask refreshTask = new TimerTask() { @Override public void run() { Display.getDefault().asyncExec(() -> { updateTree(filter); refreshTimer.cancel(); }); } }; refreshTimer = new Timer(); refreshTimer.schedule(refreshTask, delay); } @Override public void activeChannelsChanged() { Display.getDefault().asyncExec(() -> { for (TreeItemController child : root.getChildren()) { if (acRegistry.equals(child.getData())) { child.getChildren().clear(); for (IConfigItem item : acRegistry.getChildren()) { buildBranch(child, item); } updateTree(filteredTree.getText()); return; } } }); } }