/* XOWA: the XOWA Offline Wiki Application Copyright (C) 2012 gnosygnu@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package gplx.gfui; import gplx.*; import gplx.core.envs.Op_sys; import gplx.core.envs.Op_sys_; import gplx.core.threads.Thread_adp_; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Text; public class Swt_combo_ctrl extends Swt_text_w_border implements GxwElem, GxwComboBox, Swt_control, GfoEvMgrOwner { // REF: https://www.eclipse.org/forums/index.php/t/351029/; http://git.eclipse.org/c/platform/eclipse.platform.swt.git/tree/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet320.java private final Text swt_text; private final Swt_combo_list list; public Swt_combo_ctrl(Swt_control owner, Color color, Keyval_hash ctorArgs) { super(owner, color, new Keyval_hash()); Display display = owner.Under_control().getDisplay(); Shell shell = owner.Under_control().getShell(); this.swt_text = super.Under_text(); this.list = new Swt_combo_list(display, shell, this); swt_text.addListener(SWT.KeyDown, new Swt_combo_text__key_down(list)); swt_text.addListener(SWT.MouseUp, new Swt_combo_text__mouse_up(list)); Table swt_list = list.Under_table_as_swt(); swt_list.addListener(SWT.DefaultSelection, new Swt_combo_list__default_selection(this, list)); swt_list.addListener(SWT.KeyDown, new Swt_combo_list__default_selection(this, list)); swt_list.addListener(SWT.MouseDown, new Swt_combo_list__mouse_down(this, list)); // listeners to hide list-box when focus is transfered, shell is moved, text-box is clicked, etc Swt_combo_ctrl__list_hider_cmd list_hide_cmd = new Swt_combo_ctrl__list_hider_cmd(list); Swt_combo_ctrl__focus_out focus_out_lnr = new Swt_combo_ctrl__focus_out(display, this, list, list_hide_cmd); swt_text.addListener(SWT.FocusOut, focus_out_lnr); swt_list.addListener(SWT.FocusOut, focus_out_lnr); swt_text.addListener(SWT.FocusIn, new Swt_combo_text__focus_in(this, list)); swt_list.addListener(SWT.FocusIn, new Swt_combo_ctrl__focus_in(display, list, list_hide_cmd)); shell.addListener(SWT.Move, new Swt_combo_shell__move(list)); } @Override public GfoEvMgr EvMgr() {return ev_mgr;} private GfoEvMgr ev_mgr; public void EvMgr_(GfoEvMgr v) {ev_mgr = v;} @Override public Object SelectedItm() {return null;} @Override public void SelectedItm_set(Object v) {} @Override public void Sel_(int bgn, int end) {swt_text.setSelection(new Point(bgn, end));} @Override public String[] DataSource_as_str_ary() {return list.Items_str_ary();} @Override public void DataSource_set(Object... ary) {list.Items_((String[])ary);} @Override public String Text_fallback() {return text_fallback;} private String text_fallback = "";// preserve original-text when using cursor keys in list-box @Override public void Text_fallback_(String v) {text_fallback = v;} public void Text_fallback_restore() { if (String_.Len_eq_0(text_fallback)) return; // handle escape pressed after dropdown is visible, but down / up not pressed this.Text_(text_fallback); this.text_fallback = ""; } @Override public void Items__update(String[] ary) {list.Items_(ary);} @Override public void Items__size_to_fit(int count) {list.Resize_shell(count);} @Override public int List_sel_idx() {return list.Sel_idx();} @Override public void List_sel_idx_(int v) {list.Sel_idx_(v);} @Override public boolean List_visible() {return list.Visible();} @Override public void List_visible_(boolean v) {list.Visible_(v);} @Override public void Items__visible_rows_(int v) {list.Visible_rows = v;} @Override public void Items__jump_len_(int v) {list.Jump_len = v;} public Rectangle Bounds() {return super.Under_control().getBounds();} public String Text() {return swt_text.getText();} public void Text_(String v) {swt_text.setText(v);} public void Sel_all() { String text_text = swt_text.getText(); this.Sel_(0, String_.Len(text_text)); } } class Swt_combo_list { private final Display display; private final Shell owner_shell; private final Swt_combo_ctrl ctrl; private final Shell shell; private final Table table; private int list_len; public int Jump_len = 5; public int Visible_rows = 10; public Swt_combo_list(Display display, Shell owner_shell, Swt_combo_ctrl ctrl) { this.display = display; this.owner_shell = owner_shell; this.ctrl = ctrl; this.shell = new Shell(display, SWT.ON_TOP); shell.setLayout(new FillLayout()); this.table = new Table(shell, SWT.SINGLE); } public Table Under_table_as_swt() {return table;} public int Items_len() {return table.getItemCount();} public TableItem[] Items() {return table.getItems();} public TableItem Sel_itm(int i) {return table.getSelection()[i];} public void Items_text_(int idx, String v) {table.getItem(idx).setText(v);} public String[] Items_str_ary() {return items_str_ary;} private String[] items_str_ary = String_.Ary_empty; public void Items_(String[] new_ary) { int new_len = new_ary.length; if (new_len == 0) Visible_(Bool_.N); // if new_ary is empty, then hide list box; else, brief flicker as items are removed // remove all cur-items that are no longer needed b/c new_ary is smaller int cur_len = list_len; for (int i = new_len; i < cur_len; ++i) { table.remove(new_len); } // update new_ary; for (int i = 0; i < new_len; ++i) { TableItem item = null; if (i < cur_len) // existing item; reuse it item = table.getItem(i); else // no ite; create a new one item = new TableItem(table, SWT.NONE); String cur_text = item.getText(); String new_text = new_ary[i]; if (!String_.Eq(cur_text, new_text) && new_text != null) { item.setText(new_text); } } this.list_len = new_len; // resize list-shell to # of items int max_len = this.Visible_rows; if ( new_len == cur_len // do not resize if same # || new_len > max_len && cur_len > max_len // do not resize if new_len and cur_len are both off-screen ) {} this.items_str_ary = new_ary; // else // Resize_shell(list_len); } public int Sel_idx() {return table.getSelectionIndex();} public void Sel_idx_(int v) {table.setSelection(v);} public void Sel_idx_nudge(boolean fwd) {Sel_idx_adj(fwd, 1);} public void Sel_idx_jump(boolean fwd) {Sel_idx_adj(fwd, Jump_len);} private void Sel_idx_adj(boolean fwd, int adj) { if (!Visible()) Visible_(Bool_.Y); // these are called by cursor keys; always make visible int cur_idx = table.getSelectionIndex(); int new_idx = cur_idx; int idx_n = list_len - 1; if (fwd) { if (cur_idx == idx_n) new_idx = -1; else if (cur_idx == -1) new_idx = 0; else { new_idx = cur_idx + adj; if (new_idx >= idx_n) new_idx = idx_n; } } else { if (cur_idx == 0) new_idx = -1; else if (cur_idx == -1) new_idx = idx_n; else { new_idx = cur_idx - adj; if (new_idx < 0) new_idx = 0; } } Sel_idx_by_key(new_idx); } public void Sel_idx_by_key(int v) { table.setSelection(v); if (v == -1) { // nothing selected; restore orig ctrl.Text_fallback_restore(); } else { // something selected; transfer selected item to text String sel_text = table.getItem(v).getText(); ctrl.Text_(sel_text); ctrl.Sel_(sel_text.length(), sel_text.length()); } GfoEvMgr_.Pub(ctrl, GfuiComboBox.Evt__selected_changed); } public boolean Visible() {return shell.isVisible();} public void Visible_(boolean v) { if (v && list_len == 0) return; // never show if 0 items; occurs when users presses alt+down or down when in combo-box shell.setVisible(v); } public void Resize_shell(int len) { Rectangle text_bounds = display.map(owner_shell, null, ctrl.Bounds()); int adj = Op_sys.Cur().Tid() == Op_sys.Lnx.Tid() ? 9 : 2; // NOTE: magic-numbers from gnosygnu's Windows-7, OS-X and openSUSE; Linux # is not correct for different number of items, but looks acceptable for 25; int max_len = this.Visible_rows; int height = (table.getItemHeight() * (len > max_len ? max_len : len)) + adj; if (height != shell.getSize().y) // only resize if height is different shell.setBounds(text_bounds.x, text_bounds.y + text_bounds.height - 1, text_bounds.width, height); } } class Swt_combo_text__key_down implements Listener { // for list-box, highlight item or toggle visiblility based on keys private final Swt_combo_list list; public Swt_combo_text__key_down(Swt_combo_list list) { this.list = list; } @Override public void handleEvent(Event event) { try { int mask = event.stateMask; boolean no_modifier = false; boolean ctrl_modifier = false; boolean alt_modifier = false; if ((mask & SWT.ALT) == SWT.ALT) {alt_modifier = true;} else if ((mask & SWT.CTRL) == SWT.CTRL){ctrl_modifier = true;} else if ((mask & SWT.SHIFT) == SWT.SHIFT){} else no_modifier = true; switch (event.keyCode) { case SWT.ARROW_DOWN: if (event.stateMask == SWT.ALT) { list.Visible_(true); } else { list.Sel_idx_nudge(Bool_.Y); } event.doit = false; break; case SWT.ARROW_UP: if (event.stateMask == SWT.ALT) { list.Visible_(false); } else { list.Sel_idx_nudge(Bool_.N); } event.doit = false; break; case SWT.PAGE_DOWN: if (no_modifier) { list.Sel_idx_jump(Bool_.Y); event.doit = false; } else if (ctrl_modifier || alt_modifier) { list.Visible_(Bool_.N); event.doit = false; } break; case SWT.PAGE_UP: if (no_modifier) { list.Sel_idx_jump(Bool_.N); event.doit = false; } else if (ctrl_modifier || alt_modifier) { list.Visible_(Bool_.N); event.doit = false; } break; case SWT.CR: if (list.Visible() && list.Sel_idx() != -1) { list.Visible_(false); } break; case SWT.ESC: list.Sel_idx_by_key(-1); list.Visible_(false); break; } } catch (Exception e) { e.printStackTrace(); } } } class Swt_combo_text__mouse_up implements Listener { // if list-box is visible, left-click on text-box should hide list-box; note that left-click does not fire focus-in event; EX: focus html-box; click on url-box; focus-in event not fired private final Swt_combo_list list; public Swt_combo_text__mouse_up(Swt_combo_list list) { this.list = list; } @Override public void handleEvent(Event event) { if (event.button == 1) { // left-click if (list.Visible()) { list.Visible_(false); } } } } class Swt_combo_list__default_selection implements Listener { // transfer list-box's selected to text-box private final Swt_combo_ctrl ctrl; private final Swt_combo_list list; public Swt_combo_list__default_selection(Swt_combo_ctrl ctrl, Swt_combo_list list) { this.ctrl = ctrl; this.list = list; } @Override public void handleEvent(Event arg0) { ctrl.Text_(list.Sel_itm(0).getText()); list.Visible_(false); } } class Swt_combo_list__key_down implements Listener { // hide list-box if escape pressed private final Swt_combo_ctrl ctrl; private final Swt_combo_list list; public Swt_combo_list__key_down(Swt_combo_ctrl ctrl, Swt_combo_list list) { this.ctrl = ctrl; this.list = list; } @Override public void handleEvent(Event event) { if (event.keyCode == SWT.ESC) { ctrl.Text_fallback_restore(); list.Visible_(false); } } } class Swt_combo_list__mouse_down implements Listener { // left-click on list-box should transfer item to text-box and publish "accepted" event private final Swt_combo_ctrl ctrl; private final Swt_combo_list list; public Swt_combo_list__mouse_down(Swt_combo_ctrl ctrl, Swt_combo_list list) { this.ctrl = ctrl; this.list = list; } @Override public void handleEvent(Event event) { if (event.button == 1) { // left-click ctrl.Text_(list.Sel_itm(0).getText()); GfoEvMgr_.Pub(ctrl, GfuiComboBox.Evt__selected_changed); GfoEvMgr_.Pub(ctrl, GfuiComboBox.Evt__selected_accepted); } } } class Swt_combo_ctrl__focus_out implements Listener { // run hide-cmd when list-box when text-box / list-box loses focus private final Display display; private final Control text_as_swt, list_as_swt; private final Swt_combo_ctrl__list_hider_cmd list_hide_cmd; public Swt_combo_ctrl__focus_out(Display display, Swt_combo_ctrl ctrl, Swt_combo_list list, Swt_combo_ctrl__list_hider_cmd list_hide_cmd) { this.list_hide_cmd = list_hide_cmd; this.display = display; this.text_as_swt = ctrl.Under_text(); this.list_as_swt = list.Under_table_as_swt(); } @Override public void handleEvent(Event arg0) { if (display.isDisposed()) return; Control control = display.getFocusControl(); if (control == null || control == text_as_swt || control == list_as_swt) { list_hide_cmd.Active = true; display.asyncExec(list_hide_cmd); } } } class Swt_combo_ctrl__focus_in implements Listener { // cancel hide-cmd if list-box gains focus private final Display display; private final Swt_combo_ctrl__list_hider_cmd list_hide_cmd; private final Control list_as_swt; public Swt_combo_ctrl__focus_in(Display display, Swt_combo_list list, Swt_combo_ctrl__list_hider_cmd list_hide_cmd) { this.display = display; this.list_as_swt = list.Under_table_as_swt(); this.list_hide_cmd = list_hide_cmd; } @Override public void handleEvent(Event arg0) { if (display.isDisposed()) return; Control control = display.getFocusControl(); if (control == list_as_swt) list_hide_cmd.Active = false; } } class Swt_combo_text__focus_in implements Listener { // hide list-box when text-box is focused private final Swt_combo_ctrl ctrl; private final Swt_combo_list list; public Swt_combo_text__focus_in(Swt_combo_ctrl ctrl, Swt_combo_list list) { this.ctrl = ctrl; this.list = list; } @Override public void handleEvent(Event arg0) { if (list.Visible()) list.Visible_(false); else ctrl.Sel_all(); } } class Swt_combo_shell__move implements Listener { // hide list-box when shell is moved private final Swt_combo_list list; public Swt_combo_shell__move(Swt_combo_list list) {this.list = list;} @Override public void handleEvent(Event arg0) { list.Visible_(false); } } class Swt_combo_ctrl__list_hider_cmd implements Runnable { // hide list-box; can be "canceled" private final Swt_combo_list list; public boolean Active = true; public Swt_combo_ctrl__list_hider_cmd(Swt_combo_list list) { this.list = list; } @Override public void run() { if (Active) list.Visible_(false); } }