Browse Source

Gui: Strip `about:/wiki/` from links [#823]

gnosygnu 4 years ago
parent
commit
0ef5058cfd

+ 4 - 23
150_gfui/src/gplx/gfui/kits/swts/Swt_html.java

@@ -185,25 +185,6 @@ public class Swt_html implements Gxw_html, Swt_control, FocusListener, Gfo_evt_m
 	, Browser_tid_mozilla 	= SWT.MOZILLA
 	, Browser_tid_webkit	= SWT.WEBKIT
 	;
-
-	private static final String URL_PREFIX_ABOUT = "about:";
-	private static final String URL_PREFIX_BLANK = "blank";
-	public static String NormalizeSwtUrl(String url) {
-		String rv = url;
-
-		// 2020-09-19|ISSUE#:799|strip "about:" from url due to SWT 4.16
-		rv = String_.Has_at_bgn(rv, URL_PREFIX_ABOUT)
-			? String_.Mid(rv, URL_PREFIX_ABOUT.length())
-			: rv;
-
-		// 2015-06-09|webkit prefixes "about:blank" to anchors; causes TOC to fail when clicking on links; EX:about:blank#TOC1
-		// 2020-09-22|removed webkit check due to SWT 4.16; `html_box.Browser_tid() == Swt_html.Browser_tid_webkit`
-		// still strip "blank"; note that SWT 4.16 changes anchors from "file:///#anchor" to "en.w/wiki/page/#anchor"
-		rv = String_.Has_at_bgn(rv, URL_PREFIX_BLANK)
-			? String_.Mid(rv, URL_PREFIX_BLANK.length())
-			: rv;
-		return rv;
-	}
 }
 class Swt_core_cmds_html extends Swt_core__basic {
 	public Swt_core_cmds_html(Swt_html html_box, Control control) {super(control);}
@@ -253,7 +234,7 @@ class Swt_html_lnr_status implements StatusTextListener {
 		if (html_box.Kit().Kit_mode__term()) return;	// shutting down raises status changed events; ignore, else SWT exception thrown; DATE:2014-05-29 
 
 		// 2020-09-22|ISSUE#:799|normalize URL due to SWT 4.16
-		String ev_text = Swt_html.NormalizeSwtUrl(ev.text);
+		String ev_text = Swt_html_utl.NormalizeSwtUrl(ev.text);
 
 		String load_by_url_path = html_box.Load_by_url_path();
 		if (load_by_url_path != null) ev_text = String_.Replace(ev_text, load_by_url_path, "");	// remove "C:/xowa/tab_1.html"
@@ -276,7 +257,7 @@ class Swt_html_lnr_location implements LocationListener {
 	@Override public void changing(LocationEvent arg) 	{Pub_evt(arg, Gfui_html.Evt_location_changing);}
 	private void Pub_evt(LocationEvent arg, String evt) {
 		// 2020-09-22|ISSUE#:799|normalize URL due to SWT 4.16
-		String location = Swt_html.NormalizeSwtUrl(arg.location);
+		String location = Swt_html_utl.NormalizeSwtUrl(arg.location);
 
 		// location_changing fires once when page is loaded -> ignore
 		if (String_.Eq(location, String_.Empty)) {
@@ -344,7 +325,7 @@ class Swt_html_lnr_wheel implements MouseWheelListener {
 //	public Swt_open_window_listener(Swt_html html_box) {this.html_box = html_box;}
 //	@Override public void open(WindowEvent arg0) {
 //		Tfds.Write();
-//	}	
+//	}
 //}
 /*
 NOTE_1:browser scrollbar and click
@@ -362,5 +343,5 @@ so, assume:
 
 two issues still occur with the workaround
 1) even if the scrollbar is not present, any click on the right-hand edge of the screen will be ignored
-2) click -> hold -> move mouse over to left -> release; the mouse up should be absorbed, but it is not due to position of release   
+2) click -> hold -> move mouse over to left -> release; the mouse up should be absorbed, but it is not due to position of release
 */

+ 40 - 0
150_gfui/src/gplx/gfui/kits/swts/Swt_html_utl.java

@@ -0,0 +1,40 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2020 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.gfui.kits.swts;
+
+import gplx.String_;
+
+public class Swt_html_utl {
+    private static final String URL_PREFIX_ABOUT = "about:";
+    private static final String URL_PREFIX_BLANK = "blank";
+
+    public static String NormalizeSwtUrl(String url) {
+        String rv = url;
+
+        // 2020-09-19|ISSUE#:799|strip "about:" from url due to SWT 4.16
+        rv = String_.Has_at_bgn(rv, URL_PREFIX_ABOUT)
+            ? String_.Mid(rv, URL_PREFIX_ABOUT.length())
+            : rv;
+
+        // 2015-06-09|webkit prefixes "about:blank" to anchors; causes TOC to fail when clicking on links; EX:about:blank#TOC1
+        // 2020-09-22|removed webkit check due to SWT 4.16; `html_box.Browser_tid() == Swt_html.Browser_tid_webkit`
+        // still strip "blank"; note that SWT 4.16 changes anchors from "file:///#anchor" to "en.w/wiki/page/#anchor"
+        rv = String_.Has_at_bgn(rv, URL_PREFIX_BLANK)
+            ? String_.Mid(rv, URL_PREFIX_BLANK.length())
+            : rv;
+        return rv;
+    }
+}

+ 297 - 262
400_xowa/src/gplx/xowa/guis/views/Xog_tab_mgr.java

@@ -1,6 +1,6 @@
 /*
 XOWA: the XOWA Offline Wiki Application
-Copyright (C) 2012-2017 gnosygnu@gmail.com
+Copyright (C) 2012-2020 gnosygnu@gmail.com
 
 XOWA is licensed under the terms of the General Public License (GPL) Version 3,
 or alternatively under the terms of the Apache License Version 2.0.
@@ -13,264 +13,299 @@ The terms of each license can be found in the source code repository:
 GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
 Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
 */
-package gplx.xowa.guis.views; import gplx.*; import gplx.xowa.*; import gplx.xowa.guis.*;
-import gplx.gfui.*; import gplx.gfui.kits.core.*; import gplx.gfui.draws.*; import gplx.gfui.controls.gxws.*; import gplx.gfui.controls.tabs.*; import gplx.gfui.controls.standards.*;
-import gplx.xowa.apps.apis.xowa.gui.browsers.*; import gplx.xowa.specials.*;
-import gplx.xowa.apps.urls.*;
-public class Xog_tab_mgr implements Gfo_evt_itm {
-	private Ordered_hash tab_regy = Ordered_hash_.New(); private int tab_uid = 0;
-	private boolean btns__hide_if_one; private int btns__height;
-	public Xog_tab_mgr(Xog_win_itm win) {
-		this.win = win;
-		ev_mgr = new Gfo_evt_mgr(this);
-	}
-	public Gfo_evt_mgr Evt_mgr() {return ev_mgr;} private Gfo_evt_mgr ev_mgr;
-	public Xog_win_itm Win() {return win;} private Xog_win_itm win;
-	public Gfui_tab_mgr Tab_mgr() {return tab_mgr;} private Gfui_tab_mgr tab_mgr;
-	public int Btns__min_chars() {return btns__min_chars;} private int btns__min_chars;
-	public int Btns__max_chars() {return btns__max_chars;} private int btns__max_chars;
-	public boolean Javascript_enabled() {return javascript_enabled;} private boolean javascript_enabled = true;
-	private byte page_load_mode;
-	public boolean Page_load_mode_is_url() {return page_load_mode == Gxw_html_load_tid_.Tid_url;}
-	public void Init_by_kit(Gfui_kit kit) {
-		tab_mgr = kit.New_tab_mgr("xowa.tab_mgr", win.Win_box());
-		active_tab = Xog_tab_itm_.Null;
-		Gfo_evt_mgr_.Sub_same_many(tab_mgr, this, Gfui_tab_mgr.Evt_tab_selected, Gfui_tab_mgr.Evt_tab_closed, Gfui_tab_mgr.Evt_tab_switched);			
-		win.App().Cfg().Bind_many_app(this, Cfg__page_load_mode
-		, Cfg__place_on_top, Cfg__height, Cfg__hide_if_one, Cfg__curved, Cfg__close_btn_visible, Cfg__unselected_close_btn_visible, Cfg__max_chars, Cfg__min_chars);
-	}
-	public Xog_tab_itm Active_tab() {return active_tab;} private Xog_tab_itm active_tab;
-	public Xog_tab_itm Active_tab_assert() {
-		if (active_tab == Xog_tab_itm_.Null) this.Tabs_new_dflt(true);
-		return active_tab;
-	}
-	public boolean Active_tab_is_null() {return active_tab == Xog_tab_itm_.Null;}
-	private void Btns_text_recalc() {
-		int len = this.Tabs_len();
-		for (int i = 0; i < len; i++) {
-			Xog_tab_itm tab_itm = this.Tabs_get_at(i);
-			tab_itm.Tab_name_();
-		}
-	}
-	public int Tabs_len() {return tab_regy.Count();}
-	public Xog_tab_itm Tabs_new_init(Xowe_wiki wiki, Xoae_page page) {return this.Tabs_new(true, true, wiki, page);}
-	public Xog_tab_itm Tabs_get_at(int i) {return (Xog_tab_itm)tab_regy.Get_at(i);}
-	public Xog_tab_itm Tabs_new_dflt() {return Tabs_new_dflt(false);}
-	public Xog_tab_itm Tabs_new_dflt(boolean focus) {
-		boolean active_tab_is_null = this.Active_tab_is_null();
-		Xowe_wiki cur_wiki = active_tab_is_null ? win.App().Usere().Wiki() : active_tab.Wiki();
-		Xoa_ttl ttl = Xoa_ttl.Parse(cur_wiki, Xow_special_meta_.Itm__default_tab.Ttl_bry());
-		Xoa_url url = cur_wiki.Utl__url_parser().Parse_by_urlbar_or_null(ttl.Full_db_as_str()); if (url == null) throw Err_.new_("url", "invalid url", "url", url);
-		Xog_tab_itm rv = Tabs_new(focus, active_tab_is_null, cur_wiki, Xoae_page.New(cur_wiki, ttl));
-		rv.Page_update_ui();
-		rv.Show_url_bgn(url);
-		return rv;
-	}
-	private Xog_tab_itm Tabs_new(boolean focus, boolean active_tab_is_null, Xowe_wiki wiki, Xoae_page page) {
-		String tab_key = "tab_" + Int_.To_str(tab_uid++); int tab_idx = tab_regy.Count();
-		Gfui_tab_itm_data tab_data = new Gfui_tab_itm_data(tab_key, tab_idx);
-		Xog_tab_itm rv = new Xog_tab_itm(this, tab_data, wiki, page);
-		Gfui_tab_itm tab_box = tab_mgr.Tabs_add(tab_data);
-		rv.Make_html_box(tab_uid, tab_box, win, tab_mgr);
-		rv.Html_itm().Js_enabled_(javascript_enabled);
-		tab_box.Subs_add(rv.Html_itm().Html_box());
-		tab_regy.Add(tab_key, rv);
-		if (	focus
-			||	active_tab_is_null // NOTE: must select 1st tab, else nothing will show in tab box
-			) {
-			tab_mgr.Tabs_select_by_idx(rv.Tab_idx());
-			active_tab = rv;
-		}
-		Tabs_hide_if_one_chk(false);
-		return rv;
-	}
-	public void Tabs_new_dupe(boolean focus) {
-		if (this.Active_tab_is_null()) return;
-		String url = active_tab.Page().Url().To_str();
-		Tabs_new_dflt(focus);
-		win.Page__navigate_by_url_bar(url);
-	}
-	public void Tabs_javascript_enabled_(boolean v) {
-		this.javascript_enabled = v;
-		int len = tab_regy.Count();
-		for (int i = 0; i < len; i++) {
-			Xog_tab_itm tab = Tabs_get_by_idx_or_warn(i);
-			tab.Html_itm().Js_enabled_(v);
-		}
-	}
-	private void Tabs_selected(String key) {
-		Xog_tab_itm tab = Tabs_get_by_key_or_warn(key); if (tab == null) return;
-		active_tab = tab;
-		Xoae_page page = tab.Page();
-		Xog_tab_itm_read_mgr.Update_selected_tab(win, page.Url(), page.Ttl());
-		tab.Html_itm().Tab_selected(page);
-	}
-	public void Tabs_close_cur() {
-		if (this.Active_tab_is_null()) return;
-		Tabs__pub_close(active_tab);
-		tab_mgr.Tabs_close_by_idx(active_tab.Tab_idx());
-		Xog_tab_itm cur_tab = this.Active_tab();			// get new current tab for line below
-		if (cur_tab != null) cur_tab.Html_box().Focus();	// NOTE: needed to focus tab box else tab button will be focused; DATE:2014-07-13
-	}
-	public void Tabs_close_others() {this.Tabs_close_to_bgn(); this.Tabs_close_to_end();}
-	public void Tabs_close_to_bgn() {if (Active_tab_is_null()) return; Tabs_close_rng(0							, active_tab.Tab_idx());}
-	public void Tabs_close_to_end() {if (Active_tab_is_null()) return; Tabs_close_rng(active_tab.Tab_idx() + 1	, tab_regy.Count());}
-	public void Tabs_close_rng(int bgn, int end) {
-		for (int i = bgn; i < end; i++) {
-			Xog_tab_itm tab = Tabs_get_at(bgn);
-			if (!Tabs__pub_close(tab)) return;
-		}
-		for (int i = bgn; i < end; i++)
-			tab_mgr.Tabs_close_by_idx(bgn);	// NOTE: close at bgn, not at i, b/c each close will remove a tab from collection
-	}
-	public boolean Tabs__pub_close_all() {return Tabs__pub_close_rng(0, this.Tabs_len());}
-	public boolean Tabs__pub_close_rng(int bgn, int end) {
-		boolean rv = true;
-		for (int i = bgn; i < end; i++) {
-			Xog_tab_itm tab = Tabs_get_at(i);
-			boolean close_allowed = Tabs__pub_close(tab);
-			if (!close_allowed) rv = false;
-		}
-		return rv;
-	}
-	public boolean Tabs__pub_close(Xog_tab_itm tab) {
-		return tab.Page().Tab_data().Close_mgr().When_close(tab, Xoa_url.Null);
-	}
-	public void Tabs_close_undo() {
-		if (closed_undo_list.Count() == 0) return;
-		String url = (String)List_adp_.Pop(closed_undo_list);
-		Tabs_new_dflt(true);
-		win.Page__navigate_by_url_bar(url);
-	}
-	private List_adp closed_undo_list = List_adp_.New();
-	private void Tabs_closed(String key) {
-		Xog_tab_itm itm = Tabs_get_by_key_or_warn(key); if (itm == null) return;
-		itm.Html_box().Html_dispose();
-		closed_undo_list.Add(itm.Page().Url().To_str());
-		tab_regy.Del(key);
-		if (tab_regy.Count() == 0) {
-			active_tab = Xog_tab_itm_.Null;
-			Xog_tab_itm_read_mgr.Update_selected_tab_blank(win);
-		}
-		else
-			Tabs_recalc_idx();
-		Tabs_hide_if_one_chk(false);
-	}
-	private Xog_tab_itm Tabs_get_by_key_or_warn(String key) {
-		Xog_tab_itm rv = (Xog_tab_itm)tab_regy.Get_by(key); if (rv == null) win.App().Usr_dlg().Warn_many("", "", "tab.selected could not find tab; key={0}", key);
-		return rv;
-	}
-	private Xog_tab_itm Tabs_get_by_idx_or_warn(int idx) {
-		Xog_tab_itm rv = (Xog_tab_itm)tab_regy.Get_at(idx); if (rv == null) win.App().Usr_dlg().Warn_many("", "", "tab.selected could not find tab; idx={0}", idx);
-		return rv;
-	}
-	private void Tabs_recalc_idx() {
-		int len = tab_regy.Count();
-		for (int i = 0; i < len; i++) {
-			Xog_tab_itm itm = Tabs_get_by_idx_or_warn(i);
-			itm.Tab_idx_(i);
-		}
-	}
-	public void Tabs_select(boolean fwd) {
-		if (this.Active_tab_is_null()) return;
-		int new_idx = TabBox_.Cycle(fwd, active_tab.Tab_idx(), tab_regy.Count());
-		tab_mgr.Tabs_select_by_idx(new_idx);
-	}
-	public void Tabs_select_by_idx(int v) {
-		if (v < 0 || v >= tab_regy.Count()) return;
-		tab_mgr.Tabs_select_by_idx(v);
-	}
-	public void Tabs_move(boolean fwd) {
-		if (this.Active_tab_is_null()) return;
-		int src_idx = active_tab.Tab_idx();
-		int trg_idx = TabBox_.Cycle(fwd, src_idx, tab_regy.Count());
-		tab_mgr.Tabs_switch(src_idx, trg_idx);
-	}
-	private void Tabs_switched(String src_key, String trg_key) {
-		Xog_tab_itm src_itm = Tabs_get_by_key_or_warn(src_key);
-		Xog_tab_itm trg_itm = Tabs_get_by_key_or_warn(trg_key);
-		src_itm.Switch_mem(trg_itm);
-		active_tab = trg_itm;	// NOTE: src_itm initiated switch, but trg_itm is now active b/c everything in src_itm has now been reparented to trg_itm; DATE:2014-05-12
-	}
-	public void Tabs_new_link(boolean focus, String link) {	// handle empty link
-		if (String_.Len_eq_0(link)) {
-			if (this.Active_tab_is_null()) return;
-			link = gplx.langs.htmls.encoders.Gfo_url_encoder_.Http_url.Decode_str(active_tab.Html_itm().Html_selected_get_active_or_selection());	// NOTE: must decode else url-encoded special pages don't work; EX:home/wiki/Special:XowaCfg%3Fgrp%3Dxowa.html.css; DATE:2017-01-02
-		}
-		if (String_.Len_eq_0(link)) {win.App().Usr_dlg().Prog_many("", "", "no link or text selected"); return;}
-		Tabs_new_link(link, focus);
-	}
-	public void Tabs_new_link(String link, boolean focus) {
-		Xowe_wiki wiki = active_tab.Wiki();
-		Xog_tab_itm new_tab = Tabs_new(focus, false, wiki, Xoae_page.New(wiki, active_tab.Page().Ttl()));	// NOTE: do not use ttl from link, else middle-clicking pages with anchors won't work; DATE:2015-05-03
-		Xoa_url url = wiki.Utl__url_parser().Parse_by_urlbar_or_null(link);	if (url == null) return; // NOTE: link must be of form domain/wiki/page; DATE:2014-05-27			
-		new_tab.Show_url_bgn(url);
-		if (focus)
-			tab_mgr.Tabs_select_by_idx(new_tab.Tab_idx());
-	}
-	private void Tabs_hide_if_one_chk(boolean force) {
-		if (btns__hide_if_one || force) {// run code only if enabled or forced
-			if (tab_regy.Count() == 1) {
-				int desired_height = btns__hide_if_one ? 0 : btns__height;
-				if (tab_mgr.Btns_height() != desired_height)
-					tab_mgr.Btns_height_(desired_height);
-			}
-			else {
-				if (tab_mgr.Btns_height() != btns__height)
-					tab_mgr.Btns_height_(btns__height);
-			}
-		}
-	}
-	public Object Invk(GfsCtx ctx, int ikey, String k, GfoMsg m) {
-		if		(ctx.Match(k, Invk_tabs_new_dflt__at_dflt__focus_y))						Tabs_new_dflt(Bool_.Y);
-		else if	(ctx.Match(k, Invk_tabs_new_link__at_dflt__focus_n))						Tabs_new_link(Bool_.N, m.ReadStrOr("v", null));
-		else if	(ctx.Match(k, Invk_tabs_new_link__at_dflt__focus_y))						Tabs_new_link(Bool_.Y, m.ReadStrOr("v", null));
-		else if	(ctx.Match(k, Gfui_tab_mgr.Evt_tab_selected))								Tabs_selected(m.ReadStr("key"));
-		else if	(ctx.Match(k, Gfui_tab_mgr.Evt_tab_closed))									Tabs_closed(m.ReadStr("key"));
-		else if	(ctx.Match(k, Gfui_tab_mgr.Evt_tab_switched))								Tabs_switched(m.ReadStr("src"), m.ReadStr("trg"));
-		else if	(ctx.Match(k, Invk_tabs_close_cur))											Tabs_close_cur();
-		else if	(ctx.Match(k, Invk_tabs_select_bwd))										Tabs_select(Bool_.N);
-		else if	(ctx.Match(k, Invk_tabs_select_fwd))										Tabs_select(Bool_.Y);
-		else if	(ctx.Match(k, Invk_tabs_switch_cur_bwd))									Tabs_move(Bool_.N);
-		else if	(ctx.Match(k, Invk_tabs_switch_cur_fwd))									Tabs_move(Bool_.Y);
-
-		else if	(ctx.Match(k, Cfg__place_on_top))											tab_mgr.Btns_place_on_top_(m.ReadYn("v"));
-		else if	(ctx.Match(k, Cfg__height))													{btns__height = m.ReadInt("v"); tab_mgr.Btns_height_(btns__height);}
-		else if	(ctx.Match(k, Cfg__hide_if_one))											{btns__hide_if_one = m.ReadYn("v"); Tabs_hide_if_one_chk(true);}
-		else if	(ctx.Match(k, Cfg__curved))													tab_mgr.Btns_curved_(m.ReadYn("v"));
-		else if	(ctx.Match(k, Cfg__close_btn_visible))										tab_mgr.Btns_close_visible_(m.ReadYn("v"));
-		else if	(ctx.Match(k, Cfg__unselected_close_btn_visible))							tab_mgr.Btns_unselected_close_visible_(m.ReadYn("v"));
-		else if	(ctx.Match(k, Cfg__max_chars))												{btns__max_chars = m.ReadInt("v"); Btns_text_recalc();}
-		else if	(ctx.Match(k, Cfg__min_chars))												{btns__min_chars = m.ReadInt("v"); Btns_text_recalc();}
-
-		else if	(ctx.Match(k, Cfg__javascript_enabled))										Tabs_javascript_enabled_(m.ReadYnOrY("v"));	// NOTE: must be "OrY" else broken cfg.db will break cfg_maint; DATE:2016-12-15
-		else if	(ctx.Match(k, Cfg__page_load_mode))											Page_load_mode_(m.ReadStr("v"));
-		else	return Gfo_invk_.Rv_unhandled;
-		return this;
-	}
-	private void Page_load_mode_(String v) {
-		page_load_mode = Gxw_html_load_tid_.Xto_tid(v);
-		// Gfo_evt_mgr_.Pub_val(this, Evt_load_tid_changed, load_tid);
-	}
-
-	public static final String
-	  Invk_tabs_select_fwd		= "tabs_select_fwd"		, Invk_tabs_select_bwd = "tabs_select_bwd"
-	, Invk_tabs_switch_cur_fwd	= "tabs_switch_cur_fwd"	, Invk_tabs_switch_cur_bwd = "tabs_switch_cur_bwd"
-	, Invk_tabs_new_dflt__at_dflt__focus_y = "tabs_new_dflt__at_dflt__focus_y"
-	, Invk_tabs_new_link__at_dflt__focus_n = "tabs_new_link__at_dflt__focus_n"
-	, Invk_tabs_new_link__at_dflt__focus_y = "tabs_new_link__at_dflt__focus_y"
-    , Invk_tabs_close_cur		= "tabs_close_cur"
-	;
-	private static final    String 
-	  Cfg__place_on_top						= "xowa.gui.tabs.place_on_top"
-	, Cfg__height							= "xowa.gui.tabs.height"
-	, Cfg__hide_if_one						= "xowa.gui.tabs.hide_if_one"
-	, Cfg__curved							= "xowa.gui.tabs.curved"
-	, Cfg__close_btn_visible				= "xowa.gui.tabs.close_btn_visible"
-	, Cfg__unselected_close_btn_visible		= "xowa.gui.tabs.unselected_close_btn_visible"
-	, Cfg__max_chars						= "xowa.gui.tabs.max_chars"
-	, Cfg__min_chars						= "xowa.gui.tabs.min_chars"
-	, Cfg__javascript_enabled				= "xowa.gui.html_box.javascript_enabled"
-	, Cfg__page_load_mode					= "xowa.gui.html_box.page_load_mode"
-	;
-}
+package gplx.xowa.guis.views;
+
+import gplx.Bool_;
+import gplx.Err_;
+import gplx.GfoMsg;
+import gplx.Gfo_evt_itm;
+import gplx.Gfo_evt_mgr;
+import gplx.Gfo_evt_mgr_;
+import gplx.Gfo_invk_;
+import gplx.GfsCtx;
+import gplx.Int_;
+import gplx.List_adp;
+import gplx.List_adp_;
+import gplx.Ordered_hash;
+import gplx.Ordered_hash_;
+import gplx.String_;
+import gplx.gfui.controls.gxws.Gxw_html_load_tid_;
+import gplx.gfui.controls.standards.Gfui_tab_itm;
+import gplx.gfui.controls.standards.Gfui_tab_itm_data;
+import gplx.gfui.controls.standards.Gfui_tab_mgr;
+import gplx.gfui.controls.tabs.TabBox_;
+import gplx.gfui.kits.core.Gfui_kit;
+import gplx.gfui.kits.swts.Swt_html_utl;
+import gplx.xowa.Xoa_ttl;
+import gplx.xowa.Xoa_url;
+import gplx.xowa.Xoae_page;
+import gplx.xowa.Xowe_wiki;
+import gplx.xowa.htmls.hrefs.Xoh_href_;
+import gplx.xowa.specials.Xow_special_meta_;
+
+public class Xog_tab_mgr implements Gfo_evt_itm {
+	private Ordered_hash tab_regy = Ordered_hash_.New(); private int tab_uid = 0;
+	private boolean btns__hide_if_one; private int btns__height;
+	public Xog_tab_mgr(Xog_win_itm win) {
+		this.win = win;
+		ev_mgr = new Gfo_evt_mgr(this);
+	}
+	public Gfo_evt_mgr Evt_mgr() {return ev_mgr;} private Gfo_evt_mgr ev_mgr;
+	public Xog_win_itm Win() {return win;} private Xog_win_itm win;
+	public Gfui_tab_mgr Tab_mgr() {return tab_mgr;} private Gfui_tab_mgr tab_mgr;
+	public int Btns__min_chars() {return btns__min_chars;} private int btns__min_chars;
+	public int Btns__max_chars() {return btns__max_chars;} private int btns__max_chars;
+	public boolean Javascript_enabled() {return javascript_enabled;} private boolean javascript_enabled = true;
+	private byte page_load_mode;
+	public boolean Page_load_mode_is_url() {return page_load_mode == Gxw_html_load_tid_.Tid_url;}
+	public void Init_by_kit(Gfui_kit kit) {
+		tab_mgr = kit.New_tab_mgr("xowa.tab_mgr", win.Win_box());
+		active_tab = Xog_tab_itm_.Null;
+		Gfo_evt_mgr_.Sub_same_many(tab_mgr, this, Gfui_tab_mgr.Evt_tab_selected, Gfui_tab_mgr.Evt_tab_closed, Gfui_tab_mgr.Evt_tab_switched);			
+		win.App().Cfg().Bind_many_app(this, Cfg__page_load_mode
+		, Cfg__place_on_top, Cfg__height, Cfg__hide_if_one, Cfg__curved, Cfg__close_btn_visible, Cfg__unselected_close_btn_visible, Cfg__max_chars, Cfg__min_chars);
+	}
+	public Xog_tab_itm Active_tab() {return active_tab;} private Xog_tab_itm active_tab;
+	public Xog_tab_itm Active_tab_assert() {
+		if (active_tab == Xog_tab_itm_.Null) this.Tabs_new_dflt(true);
+		return active_tab;
+	}
+	public boolean Active_tab_is_null() {return active_tab == Xog_tab_itm_.Null;}
+	private void Btns_text_recalc() {
+		int len = this.Tabs_len();
+		for (int i = 0; i < len; i++) {
+			Xog_tab_itm tab_itm = this.Tabs_get_at(i);
+			tab_itm.Tab_name_();
+		}
+	}
+	public int Tabs_len() {return tab_regy.Count();}
+	public Xog_tab_itm Tabs_new_init(Xowe_wiki wiki, Xoae_page page) {return this.Tabs_new(true, true, wiki, page);}
+	public Xog_tab_itm Tabs_get_at(int i) {return (Xog_tab_itm)tab_regy.Get_at(i);}
+	public Xog_tab_itm Tabs_new_dflt() {return Tabs_new_dflt(false);}
+	public Xog_tab_itm Tabs_new_dflt(boolean focus) {
+		boolean active_tab_is_null = this.Active_tab_is_null();
+		Xowe_wiki cur_wiki = active_tab_is_null ? win.App().Usere().Wiki() : active_tab.Wiki();
+		Xoa_ttl ttl = Xoa_ttl.Parse(cur_wiki, Xow_special_meta_.Itm__default_tab.Ttl_bry());
+		Xoa_url url = cur_wiki.Utl__url_parser().Parse_by_urlbar_or_null(ttl.Full_db_as_str()); if (url == null) throw Err_.new_("url", "invalid url", "url", url);
+		Xog_tab_itm rv = Tabs_new(focus, active_tab_is_null, cur_wiki, Xoae_page.New(cur_wiki, ttl));
+		rv.Page_update_ui();
+		rv.Show_url_bgn(url);
+		return rv;
+	}
+	private Xog_tab_itm Tabs_new(boolean focus, boolean active_tab_is_null, Xowe_wiki wiki, Xoae_page page) {
+		String tab_key = "tab_" + Int_.To_str(tab_uid++); int tab_idx = tab_regy.Count();
+		Gfui_tab_itm_data tab_data = new Gfui_tab_itm_data(tab_key, tab_idx);
+		Xog_tab_itm rv = new Xog_tab_itm(this, tab_data, wiki, page);
+		Gfui_tab_itm tab_box = tab_mgr.Tabs_add(tab_data);
+		rv.Make_html_box(tab_uid, tab_box, win, tab_mgr);
+		rv.Html_itm().Js_enabled_(javascript_enabled);
+		tab_box.Subs_add(rv.Html_itm().Html_box());
+		tab_regy.Add(tab_key, rv);
+		if (	focus
+			||	active_tab_is_null // NOTE: must select 1st tab, else nothing will show in tab box
+			) {
+			tab_mgr.Tabs_select_by_idx(rv.Tab_idx());
+			active_tab = rv;
+		}
+		Tabs_hide_if_one_chk(false);
+		return rv;
+	}
+	public void Tabs_new_dupe(boolean focus) {
+		if (this.Active_tab_is_null()) return;
+		String url = active_tab.Page().Url().To_str();
+		Tabs_new_dflt(focus);
+		win.Page__navigate_by_url_bar(url);
+	}
+	public void Tabs_javascript_enabled_(boolean v) {
+		this.javascript_enabled = v;
+		int len = tab_regy.Count();
+		for (int i = 0; i < len; i++) {
+			Xog_tab_itm tab = Tabs_get_by_idx_or_warn(i);
+			tab.Html_itm().Js_enabled_(v);
+		}
+	}
+	private void Tabs_selected(String key) {
+		Xog_tab_itm tab = Tabs_get_by_key_or_warn(key); if (tab == null) return;
+		active_tab = tab;
+		Xoae_page page = tab.Page();
+		Xog_tab_itm_read_mgr.Update_selected_tab(win, page.Url(), page.Ttl());
+		tab.Html_itm().Tab_selected(page);
+	}
+	public void Tabs_close_cur() {
+		if (this.Active_tab_is_null()) return;
+		Tabs__pub_close(active_tab);
+		tab_mgr.Tabs_close_by_idx(active_tab.Tab_idx());
+		Xog_tab_itm cur_tab = this.Active_tab();			// get new current tab for line below
+		if (cur_tab != null) cur_tab.Html_box().Focus();	// NOTE: needed to focus tab box else tab button will be focused; DATE:2014-07-13
+	}
+	public void Tabs_close_others() {this.Tabs_close_to_bgn(); this.Tabs_close_to_end();}
+	public void Tabs_close_to_bgn() {if (Active_tab_is_null()) return; Tabs_close_rng(0							, active_tab.Tab_idx());}
+	public void Tabs_close_to_end() {if (Active_tab_is_null()) return; Tabs_close_rng(active_tab.Tab_idx() + 1	, tab_regy.Count());}
+	public void Tabs_close_rng(int bgn, int end) {
+		for (int i = bgn; i < end; i++) {
+			Xog_tab_itm tab = Tabs_get_at(bgn);
+			if (!Tabs__pub_close(tab)) return;
+		}
+		for (int i = bgn; i < end; i++)
+			tab_mgr.Tabs_close_by_idx(bgn);	// NOTE: close at bgn, not at i, b/c each close will remove a tab from collection
+	}
+	public boolean Tabs__pub_close_all() {return Tabs__pub_close_rng(0, this.Tabs_len());}
+	public boolean Tabs__pub_close_rng(int bgn, int end) {
+		boolean rv = true;
+		for (int i = bgn; i < end; i++) {
+			Xog_tab_itm tab = Tabs_get_at(i);
+			boolean close_allowed = Tabs__pub_close(tab);
+			if (!close_allowed) rv = false;
+		}
+		return rv;
+	}
+	public boolean Tabs__pub_close(Xog_tab_itm tab) {
+		return tab.Page().Tab_data().Close_mgr().When_close(tab, Xoa_url.Null);
+	}
+	public void Tabs_close_undo() {
+		if (closed_undo_list.Count() == 0) return;
+		String url = (String)List_adp_.Pop(closed_undo_list);
+		Tabs_new_dflt(true);
+		win.Page__navigate_by_url_bar(url);
+	}
+	private List_adp closed_undo_list = List_adp_.New();
+	private void Tabs_closed(String key) {
+		Xog_tab_itm itm = Tabs_get_by_key_or_warn(key); if (itm == null) return;
+		itm.Html_box().Html_dispose();
+		closed_undo_list.Add(itm.Page().Url().To_str());
+		tab_regy.Del(key);
+		if (tab_regy.Count() == 0) {
+			active_tab = Xog_tab_itm_.Null;
+			Xog_tab_itm_read_mgr.Update_selected_tab_blank(win);
+		}
+		else
+			Tabs_recalc_idx();
+		Tabs_hide_if_one_chk(false);
+	}
+	private Xog_tab_itm Tabs_get_by_key_or_warn(String key) {
+		Xog_tab_itm rv = (Xog_tab_itm)tab_regy.Get_by(key); if (rv == null) win.App().Usr_dlg().Warn_many("", "", "tab.selected could not find tab; key={0}", key);
+		return rv;
+	}
+	private Xog_tab_itm Tabs_get_by_idx_or_warn(int idx) {
+		Xog_tab_itm rv = (Xog_tab_itm)tab_regy.Get_at(idx); if (rv == null) win.App().Usr_dlg().Warn_many("", "", "tab.selected could not find tab; idx={0}", idx);
+		return rv;
+	}
+	private void Tabs_recalc_idx() {
+		int len = tab_regy.Count();
+		for (int i = 0; i < len; i++) {
+			Xog_tab_itm itm = Tabs_get_by_idx_or_warn(i);
+			itm.Tab_idx_(i);
+		}
+	}
+	public void Tabs_select(boolean fwd) {
+		if (this.Active_tab_is_null()) return;
+		int new_idx = TabBox_.Cycle(fwd, active_tab.Tab_idx(), tab_regy.Count());
+		tab_mgr.Tabs_select_by_idx(new_idx);
+	}
+	public void Tabs_select_by_idx(int v) {
+		if (v < 0 || v >= tab_regy.Count()) return;
+		tab_mgr.Tabs_select_by_idx(v);
+	}
+	public void Tabs_move(boolean fwd) {
+		if (this.Active_tab_is_null()) return;
+		int src_idx = active_tab.Tab_idx();
+		int trg_idx = TabBox_.Cycle(fwd, src_idx, tab_regy.Count());
+		tab_mgr.Tabs_switch(src_idx, trg_idx);
+	}
+	private void Tabs_switched(String src_key, String trg_key) {
+		Xog_tab_itm src_itm = Tabs_get_by_key_or_warn(src_key);
+		Xog_tab_itm trg_itm = Tabs_get_by_key_or_warn(trg_key);
+		src_itm.Switch_mem(trg_itm);
+		active_tab = trg_itm;	// NOTE: src_itm initiated switch, but trg_itm is now active b/c everything in src_itm has now been reparented to trg_itm; DATE:2014-05-12
+	}
+	public void Tabs_new_link(boolean focus, String link) {	// handle empty link
+		if (String_.Len_eq_0(link)) {
+			if (this.Active_tab_is_null()) return;
+			link = active_tab.Html_itm().Html_selected_get_active_or_selection();
+			// 2020-12-16|ISSUE#:823|Open in new tab creates links like `about:/wiki/PAGE_NAME` or `about:/site/WIKI_NAME/wiki/PAGE_NAME`
+			link = Swt_html_utl.NormalizeSwtUrl(link);
+			if (link.startsWith(Xoh_href_.Str__site)) {
+				link = link.substring(Xoh_href_.Str__site.length());
+			}
+			else if (link.startsWith(Xoh_href_.Str__wiki)) {
+				link = active_tab.Wiki().Domain_str() + link;
+			}
+			link = gplx.langs.htmls.encoders.Gfo_url_encoder_.Http_url.Decode_str(link);	// NOTE: must decode else url-encoded special pages don't work; EX:home/wiki/Special:XowaCfg%3Fgrp%3Dxowa.html.css; DATE:2017-01-02
+		}
+		if (String_.Len_eq_0(link)) {win.App().Usr_dlg().Prog_many("", "", "no link or text selected"); return;}
+		Tabs_new_link(link, focus);
+	}
+	public void Tabs_new_link(String link, boolean focus) {
+		Xowe_wiki wiki = active_tab.Wiki();
+		Xog_tab_itm new_tab = Tabs_new(focus, false, wiki, Xoae_page.New(wiki, active_tab.Page().Ttl()));	// NOTE: do not use ttl from link, else middle-clicking pages with anchors won't work; DATE:2015-05-03
+		Xoa_url url = wiki.Utl__url_parser().Parse_by_urlbar_or_null(link);	if (url == null) return; // NOTE: link must be of form domain/wiki/page; DATE:2014-05-27			
+		new_tab.Show_url_bgn(url);
+		if (focus)
+			tab_mgr.Tabs_select_by_idx(new_tab.Tab_idx());
+	}
+	private void Tabs_hide_if_one_chk(boolean force) {
+		if (btns__hide_if_one || force) {// run code only if enabled or forced
+			if (tab_regy.Count() == 1) {
+				int desired_height = btns__hide_if_one ? 0 : btns__height;
+				if (tab_mgr.Btns_height() != desired_height)
+					tab_mgr.Btns_height_(desired_height);
+			}
+			else {
+				if (tab_mgr.Btns_height() != btns__height)
+					tab_mgr.Btns_height_(btns__height);
+			}
+		}
+	}
+	public Object Invk(GfsCtx ctx, int ikey, String k, GfoMsg m) {
+		if		(ctx.Match(k, Invk_tabs_new_dflt__at_dflt__focus_y))						Tabs_new_dflt(Bool_.Y);
+		else if	(ctx.Match(k, Invk_tabs_new_link__at_dflt__focus_n))						Tabs_new_link(Bool_.N, m.ReadStrOr("v", null));
+		else if	(ctx.Match(k, Invk_tabs_new_link__at_dflt__focus_y))						Tabs_new_link(Bool_.Y, m.ReadStrOr("v", null));
+		else if	(ctx.Match(k, Gfui_tab_mgr.Evt_tab_selected))								Tabs_selected(m.ReadStr("key"));
+		else if	(ctx.Match(k, Gfui_tab_mgr.Evt_tab_closed))									Tabs_closed(m.ReadStr("key"));
+		else if	(ctx.Match(k, Gfui_tab_mgr.Evt_tab_switched))								Tabs_switched(m.ReadStr("src"), m.ReadStr("trg"));
+		else if	(ctx.Match(k, Invk_tabs_close_cur))											Tabs_close_cur();
+		else if	(ctx.Match(k, Invk_tabs_select_bwd))										Tabs_select(Bool_.N);
+		else if	(ctx.Match(k, Invk_tabs_select_fwd))										Tabs_select(Bool_.Y);
+		else if	(ctx.Match(k, Invk_tabs_switch_cur_bwd))									Tabs_move(Bool_.N);
+		else if	(ctx.Match(k, Invk_tabs_switch_cur_fwd))									Tabs_move(Bool_.Y);
+
+		else if	(ctx.Match(k, Cfg__place_on_top))											tab_mgr.Btns_place_on_top_(m.ReadYn("v"));
+		else if	(ctx.Match(k, Cfg__height))													{btns__height = m.ReadInt("v"); tab_mgr.Btns_height_(btns__height);}
+		else if	(ctx.Match(k, Cfg__hide_if_one))											{btns__hide_if_one = m.ReadYn("v"); Tabs_hide_if_one_chk(true);}
+		else if	(ctx.Match(k, Cfg__curved))													tab_mgr.Btns_curved_(m.ReadYn("v"));
+		else if	(ctx.Match(k, Cfg__close_btn_visible))										tab_mgr.Btns_close_visible_(m.ReadYn("v"));
+		else if	(ctx.Match(k, Cfg__unselected_close_btn_visible))							tab_mgr.Btns_unselected_close_visible_(m.ReadYn("v"));
+		else if	(ctx.Match(k, Cfg__max_chars))												{btns__max_chars = m.ReadInt("v"); Btns_text_recalc();}
+		else if	(ctx.Match(k, Cfg__min_chars))												{btns__min_chars = m.ReadInt("v"); Btns_text_recalc();}
+
+		else if	(ctx.Match(k, Cfg__javascript_enabled))										Tabs_javascript_enabled_(m.ReadYnOrY("v"));	// NOTE: must be "OrY" else broken cfg.db will break cfg_maint; DATE:2016-12-15
+		else if	(ctx.Match(k, Cfg__page_load_mode))											Page_load_mode_(m.ReadStr("v"));
+		else	return Gfo_invk_.Rv_unhandled;
+		return this;
+	}
+	private void Page_load_mode_(String v) {
+		page_load_mode = Gxw_html_load_tid_.Xto_tid(v);
+		// Gfo_evt_mgr_.Pub_val(this, Evt_load_tid_changed, load_tid);
+	}
+
+	public static final String
+	  Invk_tabs_select_fwd		= "tabs_select_fwd"		, Invk_tabs_select_bwd = "tabs_select_bwd"
+	, Invk_tabs_switch_cur_fwd	= "tabs_switch_cur_fwd"	, Invk_tabs_switch_cur_bwd = "tabs_switch_cur_bwd"
+	, Invk_tabs_new_dflt__at_dflt__focus_y = "tabs_new_dflt__at_dflt__focus_y"
+	, Invk_tabs_new_link__at_dflt__focus_n = "tabs_new_link__at_dflt__focus_n"
+	, Invk_tabs_new_link__at_dflt__focus_y = "tabs_new_link__at_dflt__focus_y"
+    , Invk_tabs_close_cur		= "tabs_close_cur"
+	;
+	private static final    String 
+	  Cfg__place_on_top						= "xowa.gui.tabs.place_on_top"
+	, Cfg__height							= "xowa.gui.tabs.height"
+	, Cfg__hide_if_one						= "xowa.gui.tabs.hide_if_one"
+	, Cfg__curved							= "xowa.gui.tabs.curved"
+	, Cfg__close_btn_visible				= "xowa.gui.tabs.close_btn_visible"
+	, Cfg__unselected_close_btn_visible		= "xowa.gui.tabs.unselected_close_btn_visible"
+	, Cfg__max_chars						= "xowa.gui.tabs.max_chars"
+	, Cfg__min_chars						= "xowa.gui.tabs.min_chars"
+	, Cfg__javascript_enabled				= "xowa.gui.html_box.javascript_enabled"
+	, Cfg__page_load_mode					= "xowa.gui.html_box.page_load_mode"
+	;
+}