(* Tux Commander - UToolTips - ToolTips utils and classes (used as the tree hints) Copyright (C) 2008 Tomas Bzatek This unit is based on an idea from Gnome File Manager by Miroslav Bajtos 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(tip_window: PGtkWidget): gboolean; cdecl; begin gtk_paint_flat_box(tip_window^.style, tip_window^.window, GTK_STATE_NORMAL, GTK_SHADOW_OUT, nil, GTK_WIDGET(tip_window), '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_swapped(tip_window, 'expose_event', G_CALLBACK(@gtk_tooltips_paint_window), tip_window); g_signal_connect_swapped(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('%s', [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('%s', [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.