OverView.vala
/*
Copyright (C) 2012 Johan Mattsson
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/>.
*/
using Cairo;
namespace Supplement {
/** A display with all glyphs present in this font. */
public class OverView : FontDisplay {
Allocation allocation;
int rows = 0;
int items_per_row = 0;
uint32 first_character = 0;
int64 first_visible = 0;
uint32 selected = 0;
double view_offset_y = 0;
double view_offset_x = 0;
int nail_height = 0;
int nail_width = 0;
double nail_zoom_height = 0;
double nail_zoom_width = 0;
public signal void open_glyph_signal (string c);
GlyphRange glyph_range;
List<GlyphCollection> visible_characters = new List<GlyphCollection> ();
List<unichar> zoom_list = new List<unichar> ();
int zoom_list_index = 0;
List<GlyphCollection> deleted_glyphs = new List<GlyphCollection> ();
Scrollbar scrollbar;
bool all_avail = true;
public OverView (GlyphRange? range = null) {
GlyphRange gr;
if (range == null) {
gr = new GlyphRange ();
set_glyph_range (gr);
}
reset_zoom ();
this.open_glyph_signal.connect ((t, s) => {
TabBar tabs = MainWindow.get_tab_bar ();
Toolbox tools = MainWindow.get_toolbox ();
bool selected = tabs.select_char (s);
unichar new_char = s.get_char (0);
Font f = Supplement.get_current_font ();
if (!selected) {
GlyphCollection? fg = f.get_glyph_collection (s);
Glyph g = (fg == null) ? new Glyph (s, new_char) : ((!) fg).get_current ();
ZoomTool z = (ZoomTool) tools.get_tool ("zoom_tool");
stdout.printf (@"Open '%s' %u (%s)\n", s, new_char, Font.to_hex (new_char));
z.store_current_view ();
f.add_glyph (g);
tabs.add_tab (g);
MainWindow.get_glyph_canvas ().set_current_glyph (g);
g.close_path ();
g.default_zoom ();
z.store_current_view ();
}
});
scrollbar = new Scrollbar ();
scrollbar.signal_scroll.connect ((delta, delta_last, absolute) => {
int64 adjustement = (int64) (-delta_last * get_height ());
int64 pixels = (int64) (-5.0 * delta_last * nail_height);
if (absolute <= 0.002) {
first_character = 0;
first_visible = 0;
selected = 0;
scroll_top ();
} else if (absolute >= 1) {
scroll_bottom ();
} else {
scroll_to (absolute);
}
redraw_area (0, 0, allocation.width, allocation.height);
});
update_scrollbar ();
}
void scroll_bottom () {
Font f;
if (all_avail) {
f = Supplement.get_current_font ();
scroll_to_position (f.length () - items_per_row * (rows - 1));
} else {
scroll_to_position (glyph_range.length () - items_per_row * (rows - 1));
}
}
public double get_height () {
double l;
Font f;
if (rows == 0) {
return 0;
}
if (all_avail) {
f = Supplement.get_current_font ();
l = f.length ();
} else {
l = glyph_range.length ();
}
return 2.0 * nail_height * (l / rows);
}
public bool selected_char_is_visible () {
return first_visible <= selected <= first_visible + items_per_row * rows;
}
public override void scroll_wheel_up (double x, double y) {
key_up ();
close_menus ();
redraw_area (0, 0, allocation.width, allocation.height);
}
public override void scroll_wheel_down (double x, double y) {
key_down ();
close_menus ();
redraw_area (0, 0, allocation.width, allocation.height);
}
public override void selected_canvas () {
redraw_area (0, 0, allocation.width, allocation.height);
KeyBindings.singleton.set_require_modifier (true);
}
public override void zoom_min () {
int minw = 26 * 2;
int minh = 46 * 2;
if (zoom_value_in_range (minw, minh)) {
nail_width = minw;
nail_height = minh;
nail_zoom_height = minw;
nail_zoom_width = minh;
adjust_scroll ();
redraw_area (0, 0, allocation.width, allocation.height);
} else {
warn_if_reached ();
}
}
public override void reset_zoom () {
nail_height = 210;
nail_width = 150;
nail_zoom_height = nail_height;
nail_zoom_width = nail_width;
adjust_scroll ();
redraw_area (0, 0, allocation.width, allocation.height);
}
public override void zoom_max () {
if (zoom_value_in_range (allocation.width, allocation.height)) {
nail_width = allocation.width;
nail_height = allocation.height;
nail_zoom_height = allocation.height;
nail_zoom_width = allocation.width;
adjust_scroll ();
redraw_area (0, 0, allocation.width, allocation.height);
} else {
warn_if_reached ();
}
}
public override void zoom_in () {
double w = (1.1 * nail_width);
double h = (1.1 * nail_height);
int nw = (int) w;
int nh = (int) h;
if (zoom_value_in_range (nw, nh)) {
nail_width = nw;
nail_height = nh;
nail_zoom_height = h;
nail_zoom_width = w;
adjust_scroll ();
redraw_area (0, 0, allocation.width, allocation.height);
}
}
public override void zoom_out () {
double w = (nail_width / 1.1);
double h = (nail_height / 1.1);
int nw = (int) w;
int nh = (int) h;
if (zoom_value_in_range (nw, nh)) {
nail_width = nw;
nail_height = nh;
nail_zoom_height = h;
nail_zoom_width = w;
adjust_scroll ();
redraw_area (0, 0, allocation.width, allocation.height);
}
}
private bool zoom_value_in_range (int w, int h) {
return (25 < w <= allocation.width && 45 < h <= allocation.height);
}
public override void store_current_view () {
if (zoom_list_index + 1 < zoom_list.length ()) {
unowned List<unichar> n = zoom_list.nth (zoom_list_index);
while (n != zoom_list.last ()) zoom_list.delete_link (zoom_list.last ());
}
zoom_list.append (selected);
zoom_list_index = (int) zoom_list.length () - 1;
if (zoom_list.length () > 50) zoom_list.delete_link (zoom_list.first ());
}
public override void restore_last_view ()
requires (zoom_list.length () > 0)
{
if (zoom_list_index - 1 < 0 || zoom_list.length () == 0)
return;
zoom_list_index--;
}
public override void next_view ()
requires (zoom_list.length () > 0)
{
if (zoom_list_index + 1 >= zoom_list.length ())
return;
zoom_list_index++;
}
public override string get_name () {
return "Overview";
}
public void display_all_available_glyphs () {
all_avail = true;
first_character = 0;
first_visible = 0;
selected = 0;
}
private unowned List<GlyphCollection> get_visible_glyphs () {
return visible_characters;
}
public void draw_caption (int row, double width, Context cr, double y, uint64 index_begin) {
string character_string;
double left_margin, x, caption_y; // for glyph caption
uint64 index = index_begin;
int i = row * items_per_row;
Font f = Supplement.get_current_font ();
cr.save ();
cr.set_line_width (1);
x = 0;
cr.set_font_size (14);
left_margin = 0;
for (int j = 0; j < items_per_row; j++) {
if (all_avail) {
if (! (0 <= index < f.length ())) {
break;
}
character_string = ((!) f.get_glyph_indice ((uint32) index)).get_name ();
} else {
if (!(0 <= index < glyph_range.get_length ())) {
break;
}
character_string = glyph_range.get_char ((uint32) index);
}
cr.save ();
draw_thumbnail (cr, character_string, x, y);
cr.restore ();
cr.save ();
if (index == selected) {
cr.set_source_rgba (142/255.0, 158/255.0, 190/255.0, 1);
} else {
cr.set_source_rgba (164/255.0, 185/255.0, 215/255.0, 1);
}
cr.set_line_join (LineJoin.ROUND);
cr.set_line_width(12);
cr.rectangle (x, y + nail_height - 20, nail_width - 30, 10);
cr.stroke ();
cr.restore ();
draw_menu (cr, character_string, x, y);
caption_y = y + 10 + nail_height - 20;
cr.move_to (x, caption_y);
cr.show_text (character_string);
x += nail_width;
index++;
i++;
}
cr.stroke();
}
private void draw_menu (Context cr, string name, double x, double y) {
GlyphCollection? gl = Supplement.get_current_font ().get_glyph_collection (name);
GlyphCollection g;
DropMenu menu;
if (gl == null) {
return;
}
g = (!) gl;
menu = g.get_version_list ();
menu.set_position (x + nail_width - 40, y + nail_height - 21);
menu.draw_icon (cr);
menu.draw_menu (cr);
visible_characters.append (g);
}
private bool draw_thumbnail (Context cr, string name, double x, double y) {
Glyph? gl = Supplement.get_current_font ().get_glyph (name);
Glyph g;
double gx, gy;
double x1, x2, y1, y2;
double scale = nail_zoom_width / 150.0;
double w, h;
w = nail_width - 19;
h = nail_height - 22;
scale = nail_zoom_height / (210.0 - 15.0);
if (gl == null) {
return false;
}
g = (!) gl;
g.boundries (out x1, out y1, out x2, out y2);
gx = 0;
gy = h / scale - 40;
Surface s = new Surface.similar (cr.get_target (), Content.COLOR_ALPHA, (int) w, (int) h);
Context c = new Context (s);
/*
c.set_source_rgb (200/255.0, 200/255.0, 229/255.0);
c.rectangle (0, 0, w, h);
c.fill ();
*/
c.scale (scale, scale);
Svg.draw_svg_path (c, g.get_svg_data (), gx, gy, 1.0);
cr.save ();
cr.set_source_surface (s, x - 6, y);
cr.paint ();
cr.restore ();
return true;
}
// 150 * 190 px
public override void draw (Allocation allocation, Context cr) {
double width;
double y;
uint64 t;
int i;
int n_items;
while (visible_characters.length () > 9) {
visible_characters.remove_link (visible_characters.first ());
}
this.allocation = allocation;
cr.save ();
cr.set_source_rgba (1, 1, 1, 1);
cr.rectangle (0, 0, allocation.width, allocation.height + nail_height);
cr.fill ();
cr.restore ();
if (Supplement.experimental) {
scrollbar.draw (cr, allocation);
}
n_items = ((allocation.width - 40) / nail_width);
rows = (allocation.height / nail_height);
if (items_per_row != n_items) {
if (items_per_row == 0) {
items_per_row = n_items;
first_visible = first_character;
key_down ();
key_up ();
} else {
items_per_row = n_items;
first_visible = first_character;
}
}
width = nail_width * items_per_row;
view_offset_x = (allocation.width - width) / 2.0;
cr.translate(view_offset_x, view_offset_y);
y = 0;
t = first_visible;
i = 0;
for (int j = 0; j < rows + 1; j++) {
draw_caption (i, width, cr, y, t);
t += items_per_row;
y += nail_height;
i++;
}
draw_menus (allocation, cr);
}
private void draw_menus (Allocation allocation, Context cr) {
}
private void close_menus () {
}
public void scroll_rows (int row_adjustment) {
for (int i = 0; i < row_adjustment; i++) {
scroll (-nail_height);
}
for (int i = 0; i > row_adjustment; i--) {
scroll (nail_height);
}
}
public void scroll_adjustment (double pixel_adjustment) {
double l;
Font f;
if (all_avail) {
f = Supplement.get_current_font ();
l = f.length ();
} else {
l = glyph_range.length ();
}
if (first_visible <= 0) {
return;
}
if (first_visible + rows * items_per_row >= l) {
return;
}
scroll ((int64) pixel_adjustment);
}
void default_position () {
scroll_top ();
scroll_rows (1);
}
void scroll_to_position (int64 r) {
int64 l;
Font f;
if (all_avail) {
f = Supplement.get_current_font ();
l = f.length ();
} else {
l = glyph_range.length ();
}
// bottom
if (r > l - items_per_row * rows) {
return;
}
if (r < 0) {
scroll_top ();
return;
}
default_position ();
first_visible = r;
selected = (uint32) r;
adjust_scroll ();
}
public void scroll_to (double percent) {
int64 r;
double l;
Font f;
if (all_avail) {
f = Supplement.get_current_font ();
l = f.length ();
} else {
l = glyph_range.length ();
}
l /= rows;
r = (int64) (percent * l * items_per_row);
scroll_to_position (r);
}
private void scroll (int64 pixel_adjustment) {
if (first_visible < 0 && pixel_adjustment < 0) {
scroll_top ();
return;
}
view_offset_y += pixel_adjustment;
if (view_offset_y >= 0) {
while (view_offset_y > nail_height) {
view_offset_y -= nail_height;
first_visible -= items_per_row;
}
first_visible -= items_per_row;
view_offset_y -= nail_height;
} else if (view_offset_y < -nail_height) {
view_offset_y = 0;
first_visible += items_per_row;
}
}
public void scroll_top () {
first_visible = first_character;
view_offset_y = 0;
}
private int selected_offset_y (unichar item) {
int64 di;
int64 a, b;
if (unlikely (items_per_row == 0)) return 0;
a = (int64) item;
b = (int64) first_visible;
di = (a - b);
return (int) (view_offset_y + ((di / items_per_row) * nail_height));
}
/** Make selected item fully visible if it partly off screen. */
private void adjust_scroll () {
int off = selected_offset_y (selected);
if (off < 0) {
close_menus ();
scroll (-1 * off);
} else if (off > allocation.height - nail_height) {
scroll ((allocation.height - nail_height) - off);
}
}
public void key_down () {
double l;
Font f;
if (all_avail) {
f = Supplement.get_current_font ();
l = f.length ();
} else {
l = glyph_range.length ();
}
update_scrollbar ();
if (l == 0) return;
if (at_bottom ()) {
int len = (int) l;
int s = (int) selected;
if (len - items_per_row >= s) {
selected += items_per_row;
} else if (s < len) {
selected++;
}
if (selected >= len) {
selected = len - 1;
}
adjust_scroll ();
return;
}
selected += items_per_row;
if (selected > rows * items_per_row + first_visible) {
scroll (-nail_height);
}
adjust_scroll ();
}
public void key_up () {
update_scrollbar ();
if (selected < first_character) {
warn_if_reached ();
}
if (selected < first_character + items_per_row) {
scroll_top ();
return;
}
if (selected - items_per_row >= first_character) {
selected -= items_per_row;
if (selected < first_visible) {
scroll (-nail_height);
}
}
if (first_visible < first_character) {
selected = first_character;
}
adjust_scroll ();
if (first_visible < first_character) {
scroll_top ();
}
}
public void open_current_glyph () {
key_press (Key.ENTER);
}
public void select_next_glyph () {
key_press (Key.RIGHT);
}
public void key_right () {
uint len;
Font f;
if (all_avail) {
f = Supplement.get_current_font ();
len = f.length ();
} else {
len = glyph_range.length ();
}
if (len == 0) return;
selected++;
if (selected >= len) {
selected = len - 1;
}
if (selected_offset_y (selected) > allocation.height - nail_height) {
selected -= items_per_row;
key_down ();
}
}
public void key_left () {
if (selected > first_character) {
selected--;
if (selected_offset_y (selected) < 0) {
selected -= items_per_row;
key_up ();
}
}
}
public string get_selected_char () {
Font f;
Glyph? g;
if (all_avail) {
f = Supplement.get_current_font ();
g = f.get_glyph_indice (selected);
return_val_if_fail (g != null, "".dup ());
return ((!) g).get_name ();
}
return glyph_range.get_char (selected);
}
public override void key_press (uint keyval) {
redraw_area (0, 0, allocation.width, allocation.height);
close_menus ();
if (KeyBindings.modifier == CTRL) {
return;
}
switch (keyval) {
case Key.ENTER:
open_glyph_signal (get_selected_char ());
return;
case Key.UP:
key_up ();
return;
case Key.RIGHT:
key_right ();
return;
case Key.LEFT:
key_left ();
return;
case Key.DOWN:
key_down ();
return;
case Key.PG_UP:
for (int i = 0; i++ < rows; i++) {
key_up ();
}
return;
case Key.PG_DOWN:
for (int i = 0; i++ < rows; i++) {
key_down ();
}
return;
case Key.DEL:
delete_selected_glyph ();
return;
}
scroll_to_char (keyval);
}
public void delete_selected_glyph () {
delete_glyph (glyph_range.get_char (selected));
}
public void delete_glyph (string g) {
GlyphCollection? gc;
Font f = Supplement.get_current_font ();
gc = f.get_glyph_collection (g);
if (gc != null) {
deleted_glyphs.append ((!) gc);
}
f.delete_glyph (g);
MainWindow.get_tab_bar ().close_by_name (g);
set_glyph_range (glyph_range);
f.touch ();
}
public override void undo () {
Font f = Supplement.get_current_font ();
if (deleted_glyphs.length () == 0) {
return;
}
f.add_glyph_collection (deleted_glyphs.last ().data);
deleted_glyphs.remove_link (deleted_glyphs.last ());
}
public void scroll_to_char (unichar c) {
GlyphRange gr = glyph_range;
GlyphRange full = new GlyphRange ();
if (is_modifier_key (c)) {
return;
}
full.use_full_unicode_range ();
set_glyph_range (full);
selected = c;
set_glyph_range (gr);
}
public override void motion_notify (double x, double y) {
if (x > allocation.x - 10) {
scrollbar.motion_notify ((int) x, (int) y);
}
}
public override void button_release (int button, double ex, double ey) {
if (ex > allocation.x - 10) {
scrollbar.button_release (button, ex, ey);
}
}
private void selection_click (uint button, double ex, double ey) {
double x = view_offset_x;
double y = view_offset_y;
unichar current = selected;
// click in margin
int n_items = (allocation.width - 40 / nail_width);
double m = (allocation.width - 40 - (n_items * nail_width)) / 2.0;
bool menu_action = false;
Font f;
uint len;
redraw_area (0, 0, allocation.width, allocation.height);
if (all_avail) {
f = Supplement.get_current_font ();
len = f.length ();
} else {
len = glyph_range.length ();
}
foreach (GlyphCollection g in get_visible_glyphs ()) {
bool a;
a = g.get_version_list ().menu_item_action (ex - view_offset_x, ey - view_offset_y); // select one item on the menu
if (a) {
return;
}
a = g.get_version_list ().menu_icon_action (ex - view_offset_x, ey - view_offset_y); // open menu
if (a) {
menu_action = true;
}
}
if (ex < m) {
ex = m + 1;
}
if (ex > allocation.width - m) {
ex = allocation.width - m - 1;
}
if (ey < 5) {
ey = 5;
}
if (ey > allocation.height - 5) {
ey = allocation.height - 5;
}
// empty set
if (len == 0) return;
// scroll to the selected glyph
selected = (uint32) first_visible;
while (y < ey < allocation.height) {
selected += items_per_row;
y += nail_height;
}
selected -= items_per_row;
while (x < ex < allocation.width) {
selected++;
x += nail_width;
}
selected--;
if (at_bottom () && selected >= len) {
selected = len - 1;
}
adjust_scroll ();
if (current == selected && !menu_action) {
open_glyph_signal (get_selected_char ());
}
adjust_scroll ();
adjust_scroll ();
}
public override void button_press (uint button, double x, double y) {
if (x > allocation.width - 10) {
if (Supplement.experimental) {
scrollbar.button_press (button, x, y);
}
} else {
selection_click (button, x, y);
}
}
/** Returns true if overview shows the last character. */
private bool at_bottom () {
Font f;
double t = rows * items_per_row + items_per_row + first_visible;
if (all_avail) {
f = Supplement.get_current_font ();
return t >= f.length ();
}
return t >= glyph_range.length ();
}
public void set_glyph_range (GlyphRange range) {
GlyphRange? current = glyph_range;
string c = "";
bool done = false;
uint32 i;
uint32 len = 0;
if (current != null) {
c = glyph_range.get_char (selected);
}
all_avail = false;
glyph_range = range;
// Todo: optimized search when full range is selected.
// scroll to the selected character
if (c == "") {
c = "a";
}
if (items_per_row == 0) {
return;
}
scroll_top ();
// Skip scroll if this is a unassigned character
if (c.char_count () > 1) {
return;
}
while (true) {
for (i = (uint32) first_visible; i < first_visible + items_per_row * (rows + 1); i++) {
if (range.get_char (i) == c) {
selected = i;
done = true;
break;
}
}
if (done) break;
if (at_bottom ()) break;
scroll_rows (rows);
}
if (unlikely (!done)) {
MainWindow.get_toolbox ().select_tool_by_name ("custom_character_set");
warning (@"No glyph \"$c\"\n");
return;
}
adjust_scroll ();
if (at_bottom ()) {
while (c != glyph_range.get_char (selected) && glyph_range.get_length () >= items_per_row) {
key_right ();
if (selected >= glyph_range.get_length () - items_per_row) {
break;
}
}
len = glyph_range.get_length ();
for (i = selected; i < len; i++) {
if (c == glyph_range.get_char (i)) {
break;
}
}
if (i == len) {
selected = first_character;
scroll_top ();
}
}
update_scrollbar ();
redraw_area (0, 0, allocation.width, allocation.height);
}
public void update_scrollbar () {
double len;
Font f;
if (all_avail) {
f = Supplement.get_current_font ();
len = f.length ();
} else {
len = glyph_range.get_length ();
}
scrollbar.set_handle_size (items_per_row * rows / len);
scrollbar.set_handle_position (first_visible / len);
}
}
}