(*
    Tux Commander - UToolTips - ToolTips utils and classes (used as the tree hints)
    Copyright (C) 2008 Tomas Bzatek <tbzatek@users.sourceforge.net>
    This unit is based on an idea from Gnome File Manager by Miroslav Bajtos <mirco@matfyz.cz>
    Check for updates on tuxcmd.sourceforge.net

    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 2 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, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*)
unit UToolTips;

interface

uses glib2, gdk2, gtk2, pango;

procedure FileListTipsInstall(ATreeView: PGtkTreeView);
procedure FileListTipsEnable;
procedure FileListTipsDisable;
procedure FileListTipsHide;

implementation

uses SysUtils, DateUtils, Classes, GTKForms, GTKView, GTKControls, UMain, UEngines, UCore, UConfig,
  UCoreUtils;

var tip_window: PGtkWindow;
    tip_label: PGtkLabel;
    timer_id: gint;
    tips_enabled: boolean;
    data_row: PGtkTreePath;
    data_column: PGtkTreeViewColumn;
    data_panel: PGtkTreeView;
    tips_timer: PGTimer;


function event_handler(widget: PGtkWidget; event: PGdkEvent; user_data: gpointer): gboolean; cdecl; forward;
function on_leave_notify(widget: PGtkWidget; event: PGdkEventCrossing; user_data: gpointer): gboolean; cdecl; forward;
function show_tip_widget(user_data: gpointer): gboolean; cdecl; forward;
procedure create_widgets; forward;
function tip_event_handler(widget: PGtkWidget; event: PGdkEvent; user_data: gpointer): gboolean; cdecl; forward;
function tips_button_press_event(widget: PGtkWidget; event: PGdkEventButton; user_data: gpointer):gboolean; cdecl; forward;



procedure FileListTipsInstall(ATreeView: PGtkTreeView);
begin
  g_signal_connect_after(ATreeView, 'event-after', G_CALLBACK(@event_handler), nil);
  g_signal_connect(ATreeView, 'leave-notify-event', G_CALLBACK(@on_leave_notify), nil);
end;

procedure file_list_tips_show_tip(AListView: PGtkTreeView; row: PGtkTreePath; column: PGtkTreeViewColumn);
var Timeout: integer;
    is_active: gboolean;
    ms: gulong;
    sec: gdouble;
begin
  if not tips_enabled then Exit;
  FileListTipsHide;
  g_object_get(G_OBJECT(FMain.FWidget), 'is-active', @is_active, nil);
  if not is_active then Exit;
  if Assigned(data_row) and (data_row <> row) then gtk_tree_path_free(data_row);
  data_panel := AListView;
  data_row := row;
  data_column := column;
  Timeout := ConstFileListTipsDelay;
  if tips_timer <> nil then begin
    sec := g_timer_elapsed(tips_timer, @ms);
    if (sec < 1) and (ms < ConstFileListTipsDelayNeighbour * 1000) then Timeout := 1;
//    DebugMsg(['Time elapsed = ', Double(sec), ':', integer(ms div 1000)]);
  end;
  timer_id := gtk_timeout_add(Timeout, show_tip_widget, nil);
end;


function event_handler(widget: PGtkWidget; event: PGdkEvent; user_data: gpointer): gboolean; cdecl;
var view: PGtkTreeView;
    column: PGtkTreeViewColumn;
    path: PGtkTreePath;
begin
  Result := False;
  view := GTK_TREE_VIEW(widget);
  if event^.any.window <> gtk_tree_view_get_bin_window(view) then Exit;
  case event^._type of
    GDK_KEY_PRESS, GDK_SCROLL:
      begin
        FileListTipsHide;
        if tips_timer <> nil then begin
          g_timer_destroy(tips_timer);
          tips_timer := nil;
        end;
        Result := True;
      end;
    GDK_FOCUS_CHANGE: begin
                        Result := True;
                        if event^.focus_change._in <> 1 then FileListTipsHide;
                      end;  
    GDK_MOTION_NOTIFY: begin
                         Result := True;
                         if not gtk_tree_view_get_path_at_pos(view, Trunc(event^.motion.x), Trunc(event^.motion.y), path, column, nil, nil)
                           then begin
                                  FileListTipsHide;
                                  if tips_timer <> nil then begin
                                    g_timer_destroy(tips_timer);
                                    tips_timer := nil;
                                  end;
                                  Exit;
                                end;
                         if Assigned(path) and Assigned(column) then file_list_tips_show_tip(view, path, column);
                       end;
  end;
end;

procedure draw_tip_widget(panel: PGtkTreeView; const text: Pgchar; x, y: integer);
var requisition: TGtkRequisition;
    widget: PGtkWidget;
    w, scr_w, wx, wy: Integer;
    FontDesc: PPangoFontDescription;
    hadjustment: PGtkAdjustment;
begin
  try

  if not Assigned(tip_window) then create_widgets;
  if not ConfUseSystemFont then begin
    FontDesc := pango_font_description_from_string(PChar(ConfPanelFont));
    gtk_widget_modify_font(PGtkWidget(tip_label), FontDesc);
  end else gtk_widget_modify_font(PGtkWidget(tip_label), nil);

  gtk_label_set_markup(GTK_LABEL(tip_label), Text);
  widget := GTK_WIDGET(panel);
  hadjustment := gtk_tree_view_get_hadjustment(panel);
  x := x - Trunc(gtk_adjustment_get_value(hadjustment));

  gdk_window_get_origin(widget^.window, @wx, @wy);
  x := x + wx;
  y := y + wy;
  scr_w := gdk_screen_width;

  gtk_label_set_line_wrap(tip_label, False);
  gtk_widget_size_request(GTK_WIDGET(tip_window), @requisition);
  if x + requisition.width > scr_w {PGtkWidget(panel)^.allocation.width} then begin
    gtk_label_set_line_wrap(tip_label, True);
    gtk_widget_size_request(GTK_WIDGET(tip_window), @requisition);
  end;

  w := requisition.width;
  if (x + w) > scr_w then x := scr_w - w 
                     else if x < 0 then x := 0;
  gtk_window_move(tip_window, x, y);
  except
    on E: Exception do DebugMsg(['*** Exception raised in function draw_tip_widget: (', E.ClassName, '): ', E.Message]);
  end;
end;

function gtk_tooltips_paint_window(widget: PGtkWidget; event: PGdkEventExpose; user_data: gpointer): gboolean; cdecl;
begin
  gtk_paint_flat_box(widget^.style, widget^.window, GTK_STATE_NORMAL, GTK_SHADOW_OUT,
                     nil, widget, 'tooltip', 0, 0, -1, -1);
  Result := False;
end;


procedure create_widgets;
begin
  if Assigned(tip_window) then Exit;
  tip_window := GTK_WINDOW(gtk_window_new(GTK_WINDOW_POPUP));
  gtk_widget_set_app_paintable(GTK_WIDGET(tip_window), True);
  gtk_window_set_resizable(tip_window, False);
  gtk_widget_set_name(GTK_WIDGET(tip_window), 'gtk-tooltips');
  gtk_container_set_border_width(GTK_CONTAINER(tip_window), 4);
  gtk_widget_set_events(GTK_WIDGET(tip_window), GDK_POINTER_MOTION_MASK or GDK_BUTTON_PRESS_MASK or GDK_KEY_PRESS_MASK or GDK_FOCUS_CHANGE_MASK or GDK_SCROLL_MASK);
  g_signal_connect(tip_window, 'event', G_CALLBACK(@tip_event_handler), nil);
  g_signal_connect(tip_window, 'expose_event', G_CALLBACK(@gtk_tooltips_paint_window), tip_window);
  g_signal_connect(tip_window, 'button-press-event', G_CALLBACK(@tips_button_press_event), tip_window);

  tip_label := GTK_LABEL(gtk_label_new(nil));
  gtk_misc_set_alignment(GTK_MISC(tip_label), 0, 0.5);
  gtk_widget_show(GTK_WIDGET(tip_label));

  gtk_container_add(GTK_CONTAINER(tip_window), GTK_WIDGET(tip_label));
end;

procedure FileListTipsHide;
begin
  if not Assigned(tip_window) then Exit;
  if Assigned(data_row) then gtk_tree_path_free(data_row);
  data_row := nil;
  data_column := nil;

  if GTK_WIDGET_VISIBLE(tip_window) then begin
    gtk_widget_hide(GTK_WIDGET(tip_window));
    if tips_timer = nil then tips_timer := g_timer_new;
    g_timer_start(tips_timer);
//           gdk_beep();
//           DebugMsg(['FileListTipsHide, Setting last_hide_time to ', last_hide_time]);
  end;

  if timer_id <> 0 then begin
    gtk_timeout_remove(timer_id);
    timer_id := 0;
  end;
end;

function show_tip_widget(user_data: gpointer): gboolean; cdecl;
var Text: PChar;
    Data: PDataItem;
    AListView: TGTKListView;

  function ColumnFits(ColNo, ColumnWidth: integer): boolean;
  var Layout: PPangoLayout;
      FontDesc: PPangoFontDescription;
      w, h: Integer;
  begin
    Text := Data^.ColumnData[FMain.ColumnSortIDs[ColNo + 1] - 1];
    Layout := gtk_widget_create_pango_layout(AListView.FWidget, Text);
    if ConfDirsInBold and Data^.IsDir then begin
      Text := PChar(Format('<span weight="bold">%s</span>', [QuoteMarkupStr(Text)]));
      pango_layout_set_markup(Layout, Text, Length(Text));
    end;
    if not ConfUseSystemFont then begin
      FontDesc := pango_font_description_from_string(PChar(ConfPanelFont));
      pango_layout_set_font_description(Layout, FontDesc);
    end;
    pango_layout_get_pixel_size(Layout, @w, @h);
    g_object_unref(Layout);
    Result := ColumnWidth > w;
  end;

var Col, ColID: Integer;
    Rect: TGdkRectangle;
    iter: TGtkTreeIter;
    DataList: TList;
    TreePath: PGtkTreePath;
    IsFNameExtColumn, b: boolean;
begin
  Result := False;
  try

  if not gtk_tree_model_get_iter(gtk_tree_view_get_model(data_panel), @iter, data_row) then Exit;
  if Pointer(data_panel) = Pointer(FMain.LeftListView.FWidget) then begin
    DataList := LeftPanelData;
    AListView := FMain.LeftListView;
  end else begin
    DataList := RightPanelData;
    AListView := FMain.RightListView;
  end;

  if not Application.GTKVersion_2_0_5_Up then gtk_tree_model_get(gtk_tree_view_get_model(data_panel), @iter, 0, @Data, -1)
  else begin
    TreePath := gtk_tree_path_new_from_string(PChar(IntToStr(gtk_tree_path_get_indices(data_row)^)));
    if not Assigned(TreePath) then Exit;
    AListView.ConvertPathToChild(TreePath);
    if (TreePath = nil) or (gtk_tree_path_get_indices(TreePath)^ > DataList.Count - 1) then begin
      FileListTipsHide;
      Exit;
    end;
    Data := DataList[gtk_tree_path_get_indices(TreePath)^];
    gtk_tree_path_free(TreePath);
  end;
  if (Data = nil) or Data^.UpDir then begin
    FileListTipsHide;
    Exit;
  end;

  Col := gtk_tree_view_column_get_sort_column_id(data_column);
  ColID := FMain.ColumnSortIDs[Col + 1];
  IsFNameExtColumn := ((ColID = 1) and (ColID < AListView.Columns.Count - 1) and (FMain.ColumnSortIDs[Col + 2] = 3)) or
                      ((ColID = 3) and (ColID > 0) and (FMain.ColumnSortIDs[Col] = 1));
  gtk_tree_view_get_cell_area(data_panel, data_row, data_column, @rect);
  if (Col = 0) and ConfUseFileTypeIcons then begin
    Inc(Rect.x, ConfRowHeightReal + 2);
    Dec(Rect.width, ConfRowHeightReal + 2);
  end;

  //  Determine if the text fits into the cell area
  b := ColumnFits(Col, Rect.width - 10);
  if IsFNameExtColumn then begin
    gtk_tree_view_get_cell_area(data_panel, data_row, gtk_tree_view_get_column(data_panel, Col - Ord(ColID = 3) + Ord(ColID = 1)), @rect);
    if ((Col - Ord(ColID = 3) + Ord(ColID = 1)) = 0) and ConfUseFileTypeIcons then begin
      Inc(Rect.x, ConfRowHeightReal + 2);
      Dec(Rect.width, ConfRowHeightReal + 2);
    end;
    b := b and ColumnFits(Col - Ord(ColID = 3) + Ord(ColID = 1), Rect.width - 10);
  end;

  if b then begin
    FileListTipsHide;
    Exit;
  end;

  if IsFNameExtColumn then begin
    Text := PChar(Data^.FDisplayName);
    gtk_tree_view_get_cell_area(data_panel, data_row, gtk_tree_view_get_column(data_panel, Col - Ord(ColID = 3)), @rect);
    if ((Col - Ord(ColID = 3)) = 0) and ConfUseFileTypeIcons then begin
      Inc(Rect.x, ConfRowHeightReal + 2);
      Dec(Rect.width, ConfRowHeightReal + 2);
    end;
  end else Text := Data^.ColumnData[FMain.ColumnSortIDs[Col + 1] - 1];
  Text := PChar(QuoteMarkupStr(Text));

  if ConfDirsInBold and Data^.IsDir then Text := PChar(Format('<span weight="bold">%s</span>', [Text]));
  draw_tip_widget(data_panel, Text, Rect.x, Rect.y + Rect.height + 2);
//  if Assigned(text) then g_free(text);
  if not GTK_WIDGET_VISIBLE(GTK_WIDGET(tip_window)) then gtk_widget_show(GTK_WIDGET(tip_window));

  except
    on E: Exception do DebugMsg(['*** Exception raised in function show_tip_widget(user_data: gpointer): gboolean (', E.ClassName, '): ', E.Message]);
  end;
end;

procedure FileListTipsEnable;
begin
  tips_enabled := True and (not ConfDisableFileTips);
end;

procedure FileListTipsDisable;
begin
  tips_enabled := True;
  FileListTipsHide;
end;

procedure file_list_tips_hide_tip;
begin
  FileListTipsHide;
end;

function tip_event_handler(widget: PGtkWidget; event: PGdkEvent; user_data: gpointer): gboolean; cdecl;
begin
  Result := True;
  case event^._type of
    GDK_KEY_PRESS, {GDK_BUTTON_PRESS, }GDK_SCROLL, GDK_FOCUS_CHANGE{, GDK_MOTION_NOTIFY}:
      begin
        FileListTipsHide;
        if tips_timer <> nil then begin
          g_timer_destroy(tips_timer);
          tips_timer := nil;
        end;
        Exit;
      end;
  end;
  Result := False;
end;

function on_leave_notify(widget: PGtkWidget; event: PGdkEventCrossing; user_data: gpointer): gboolean; cdecl;
begin
  if Assigned(tip_window) and (not GTK_WIDGET_VISIBLE(tip_window))
    then begin
           FileListTipsHide;
           if tips_timer <> nil then begin
             g_timer_destroy(tips_timer);
             tips_timer := nil;
           end;
         end;
  Result := False;
end;

function tips_button_press_event(widget: PGtkWidget; event: PGdkEventButton; user_data: gpointer): gboolean; cdecl;
var FSelection: PGtkTreeSelection;
begin
  Result := True;
  if not Assigned(data_panel) then Exit;
  if not Assigned(data_row) then begin
    FileListTipsHide;
    if tips_timer <> nil then begin
      g_timer_destroy(tips_timer);
      tips_timer := nil;
    end;
    Exit;
  end;

  FMain.LastClick := Now;
  gtk_tree_view_set_cursor(data_panel, data_row, nil, False);
  FSelection := gtk_tree_view_get_selection(data_panel);
  gtk_tree_selection_select_path(FSelection, data_row);
  Result := False;

  FileListTipsHide;
  if tips_timer <> nil then begin
    g_timer_destroy(tips_timer);
    tips_timer := nil;
  end;

  gtk_widget_grab_focus(PGtkWidget(data_panel));
end;



initialization
  timer_id := 0;
  data_row := nil;
  tips_timer := nil;
end.
